Spring 授权服务器是一个旨在实现 OAuth 2.1 和 OpenID Connect 1.0 规范以及其他相关标准的框架。它基于 Spring Security 构建,为创建符合 OpenID Connect 1.0 和 OAuth2 授权服务器解决方案的身份提供者提供了安全、轻量级和可定制的基础。
功能列表
简短回答
Spring Security 是一个功能强大且高度可定制的身份验证和访问控制框架。它是保护基于 Spring 的应用程序的事实标准。
本质上,Spring Security 本质上是 servlet 过滤器的集合,旨在通过强大的身份验证和授权功能来增强您的 Web 应用程序。
Spring Security 还与 Spring Web MVC 或 Spring Boot 等框架很好地配合,支持 OAuth2 和 SAML 等标准。它自动生成登录和注销接口,并保护您的应用程序免受 CSRF 等常见安全漏洞的影响。
嗯,这不是很有帮助,不是吗?
让我们深入研究网络安全,以掌握其安全工作流程的要点。
要成为Spring Security专家,首先要掌握这三个核心概念:
注意 - 不要绕过此部分;它为所有 Spring Security 功能奠定了基础。
您需要在线访问您的银行账户以查看余额或进行交易。通常这是使用用户名和密码完成的
用户:“我是 John Doe。我的用户名是:johndoe1985。”
银行系统:“请验证您的身份。您的密码是多少?”
用户:“我的密码是:secureB@nk2023。”
银行系统:“欢迎,John Doe。这是您的帐户概览。”
对于基本应用程序,仅进行身份验证就足够了:用户登录后,他们将被授予访问应用程序所有区域的权限。
但是,在大多数应用程序中,都有权限或角色在发挥作用。
用户:“让我来玩一下该交易......”
银行系统:“等一下,我需要先检查您的权限……是的,John Doe 先生,您的权限级别正确。尽情享受吧。”
用户:“我转1M哈哈哈……开玩笑开玩笑”
现在,让我们探索 Servlet 过滤器。它们与身份验证和授权有何关系?
为什么使用 Servlet 过滤器?
每个 Spring Web 应用程序都围绕一个 servlet 展开:值得信赖的 DispatcherServlet。它的主要作用是将传入的 HTTP 请求(例如来自浏览器的请求)路由到适当的 @Controller 或 @RestController 进行处理。
事情是这样的:DispatcherServlet 本身没有任何内置的安全功能,并且您可能不想直接在 @Controller 中处理原始 HTTP Basic Auth 标头。理想情况下,在请求到达您的 @Controllers
之前就应该处理身份验证和授权幸运的是,在 Java Web 环境中,您可以通过在 servlet 之前放置过滤器来实现这一点。这意味着您可以考虑创建一个 SecurityFilter 并将其设置在 Tomcat(servlet 容器/应用程序服务器)中,以在每个传入的 HTTP 请求到达您的 servlet 之前拦截和处理它。
SecurityFilter 大约有 4 个任务
在实践中,我们会将一个过滤器分解为多个过滤器,然后您可以将它们链接在一起。
以下是传入 HTTP 请求的传输方式:
此设置称为 FilterChain。
通过使用过滤器(或一系列过滤器),您可以有效地管理应用程序中的所有身份验证和授权挑战,而无需更改 @RestControllers 或 @Controllers 的核心实现。
假设您已经正确配置了 Spring Security 并启动了您的 Web 应用程序。您会注意到一条日志消息,如下所示:
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
扩展这一行表明 Spring Security 不仅仅添加一个过滤器,它还设置了一个包含 15 个(!)不同过滤器的完整过滤器链。
当 HTTP 请求到达时,它会依次通过这 15 个过滤器中的每一个,然后最终到达您的 @RestControllers。这些过滤器的顺序至关重要,因为请求是从链的顶部到底部处理的。
深入研究链中每个过滤器的细节会让我们走得太远,但这里是一些关键过滤器的解释。为了更深入地了解其他内容,您可以探索 Spring Security 的源代码。
问题 - 为什么 HTTP 请求会被 Spring Security 过滤器破坏?
回答 - 因为每次它试图靠近时,过滤器都会说:“等一下!让我先检查一下你!” ?是的,休息......哇,等等......这一次的安全讨论太多了!
开始使用 Spring 授权服务器的最简单方法是创建基于 Spring Boot 的应用程序。您可以使用start.spring.io生成一个基本项目。
唯一需要的依赖是实现(“org.springframework.boot:spring-boot-starter-oauth2-authorization-server”)
我们将添加另外两个来执行更多操作
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
如何配置 Spring Security
使用最新的 Spring Security 和/或 Spring Boot 版本,配置 Spring Security 的方法是使用一个类: 使用 @EnableWebSecurity 注解。
dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-validation") }
(1):协议端点的 Spring Security 过滤器链。
(2) :用于身份验证的 Spring Security 过滤器链。
(3) : com.nimbusds.jose.jwk.source.JWKSource 的实例,用于签署访问令牌。
(4) : 用于解码签名访问令牌的 JwtDecoder 实例。
(5) : AuthorizationServerSettings 实例,用于配置 Spring Authorization Server。
让我们配置 CORS 以允许某些 URL 访问我们的应用程序
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
Cors配置
该类用于定义CORS规则。在这种情况下:
UrlBasedCorsConfigurationSource
哇,配置太多了!但这就是 Spring 框架的魔力——它处理幕后的所有繁重工作。
dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-validation") }
上面我们做了一些事情
UserDetailsService 由 DaoAuthenticationProvider 用于检索 用户名、密码以及其他用于使用用户名和密码进行身份验证的属性。 Spring Security 提供了 UserDetailsService 的内存、JDBC 和缓存实现。
您可以通过将自定义 UserDetailsService 公开为 bean 来定义自定义身份验证。
@Configuration @EnableWebSecurity public class SecurityConfig { private static final String[] ALLOW_LIST = {"/oauth2/token", "/userinfo"}; //This is primarily configured to handle OAuth2 and OpenID Connect specific endpoints. It sets up the security for the authorization server, handling token endpoints, client authentication, etc. @Bean (1) @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); http .cors(Customizer.withDefaults()) .authorizeHttpRequests(authz -> authz .requestMatchers(ALLOW_LIST).permitAll() .requestMatchers("/**", "/oauth2/jwks/").hasAuthority("SCOPE_keys.write") .anyRequest() .authenticated()) .securityMatchers(matchers -> matchers.requestMatchers(antMatcher("/oauth2/**"), authorizationServerConfigurer.getEndpointsMatcher())) .with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer .oidc(Customizer.withDefaults())) // Enable OpenID Connect 1.0 // Redirect to the login page when not authenticated from the // authorization endpoint .exceptionHandling((exceptions) -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) )) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } // This configuration is set up for general application security, handling standard web security features like form login for paths not specifically managed by the OAuth2 configuration. @Bean (2) @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers("/login", "/error", "/main.css") .permitAll() .anyRequest() .authenticated() ) // Form login handles the redirect to the login page from the // authorization server filter chain .formLogin((login) -> login.loginPage("/login")); return http.build(); } @Bean (3) public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean (4) public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean (5) public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings .builder() .build(); } }
一旦我们启动应用程序,我们的 OIDC 和 OAuth2 设置与 Spring 授权服务器应该可以正常运行。但是,您会注意到我们使用了 InMemoryUserDetailsManager,它非常适合演示或原型设计。但对于生产环境,这是不可取的,因为应用程序重新启动后所有数据都会消失。
JdbcUserDetailsManager 是 Spring Security 中的一项功能,它使用 JDBC 通过连接到关系数据库来处理用户凭据和角色。当您的应用程序可以使用 Spring Security 期望的用户表的标准架构时,这是理想的选择。
可从 Spring security org/springframework/security/core/userdetails/jdbc/users.ddl
获得的架构
@Configuration public class CorsConfig { @Bean public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.addAllowedOrigin("http://localhost:3000/"); // Change to specific domains in production configuration.addAllowedMethod("*"); configuration.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
从 InMemoryUserDetailsManager 转换到 JdbcUserDetailsManager 所需的唯一调整
2020-02-25 10:24:27.875 INFO 11116 --- [ main] o.s.s.web.DefaultSecurityFilterChain : Creating filter chain: any request, [org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@46320c9a, org.springframework.security.web.context.SecurityContextPersistenceFilter@4d98e41b, org.springframework.security.web.header.HeaderWriterFilter@52bd9a27, org.springframework.security.web.csrf.CsrfFilter@51c65a43, org.springframework.security.web.authentication.logout.LogoutFilter@124d26ba, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@61e86192, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@10980560, org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter@32256e68, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@52d0f583, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@5696c927, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@5f025000, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@5e7abaf7, org.springframework.security.web.session.SessionManagementFilter@681c0ae6, org.springframework.security.web.access.ExceptionTranslationFilter@15639d09, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@4f7be6c8]|
此配置对于坚持 Spring Security 标准表模式的应用程序有效。但是,如果您需要自定义(例如使用电子邮件而不是用户名进行登录),则实现自定义 UserDetailsService 可提供必要的适应性。
让我们向提供程序添加自定义 CustomUserDetailsService。在 AuthenticationProvider 中使用 setUserDetailsService
设置自定义服务
dependencies { implementation("org.springframework.boot:spring-boot-starter-oauth2-authorization-server") implementation("org.springframework.boot:spring-boot-starter-webflux") implementation("org.springframework.boot:spring-boot-starter-validation") }
定制服务
@Configuration @EnableWebSecurity public class SecurityConfig { private static final String[] ALLOW_LIST = {"/oauth2/token", "/userinfo"}; //This is primarily configured to handle OAuth2 and OpenID Connect specific endpoints. It sets up the security for the authorization server, handling token endpoints, client authentication, etc. @Bean (1) @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = OAuth2AuthorizationServerConfigurer.authorizationServer(); http .cors(Customizer.withDefaults()) .authorizeHttpRequests(authz -> authz .requestMatchers(ALLOW_LIST).permitAll() .requestMatchers("/**", "/oauth2/jwks/").hasAuthority("SCOPE_keys.write") .anyRequest() .authenticated()) .securityMatchers(matchers -> matchers.requestMatchers(antMatcher("/oauth2/**"), authorizationServerConfigurer.getEndpointsMatcher())) .with(authorizationServerConfigurer, (authorizationServer) -> authorizationServer .oidc(Customizer.withDefaults())) // Enable OpenID Connect 1.0 // Redirect to the login page when not authenticated from the // authorization endpoint .exceptionHandling((exceptions) -> exceptions .defaultAuthenticationEntryPointFor( new LoginUrlAuthenticationEntryPoint("/login"), new MediaTypeRequestMatcher(MediaType.TEXT_HTML) )) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer((oauth2) -> oauth2.jwt(Customizer.withDefaults())); return http.build(); } // This configuration is set up for general application security, handling standard web security features like form login for paths not specifically managed by the OAuth2 configuration. @Bean (2) @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .requestMatchers("/login", "/error", "/main.css") .permitAll() .anyRequest() .authenticated() ) // Form login handles the redirect to the login page from the // authorization server filter chain .formLogin((login) -> login.loginPage("/login")); return http.build(); } @Bean (3) public JWKSource<SecurityContext> jwkSource() { KeyPair keyPair = generateRsaKey(); RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic(); RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate(); RSAKey rsaKey = new RSAKey.Builder(publicKey) .privateKey(privateKey) .keyID(UUID.randomUUID().toString()) .build(); JWKSet jwkSet = new JWKSet(rsaKey); return new ImmutableJWKSet<>(jwkSet); } private static KeyPair generateRsaKey() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } return keyPair; } @Bean (4) public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean (5) public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings .builder() .build(); } }
存储库
@Configuration public class CorsConfig { @Bean public UrlBasedCorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.addAllowedOrigin("http://localhost:3000/"); // Change to specific domains in production configuration.addAllowedMethod("*"); configuration.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", configuration); return source; } }
实体
@Configuration public class Clients { @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("stomble") .clientAuthenticationMethod(ClientAuthenticationMethod.NONE) .authorizationGrantTypes(types -> { types.add(AuthorizationGrantType.AUTHORIZATION_CODE); types.add(AuthorizationGrantType.REFRESH_TOKEN); }) .redirectUris(redirectUri -> { redirectUri.add("http://localhost:3000"); redirectUri.add("https://oauth.pstmn.io/v1/callback"); redirectUri.add("http://localhost:3000/signin-callback"); }) .postLogoutRedirectUri("http://localhost:3000") .scopes(score -> { score.add(OidcScopes.OPENID); score.add(OidcScopes.PROFILE); score.add(OidcScopes.EMAIL); }) .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(false) .requireProofKey(true) .build()) .build(); return new InMemoryRegisteredClientRepository(oidcClient); } }
在安全过滤器中,我们必须告诉 spring security 使用此服务
.clientAuthentication(clientAuth -> clientAuth.authenticationProvider(authenticationProvider))
@Configuration public class UserConfig { @Bean public UserDetailsService userDetailsService(PasswordEncoder passwordEncoder) { UserDetails userDetailFirst = User.builder() .username("user1") .password(passwordEncoder.encode("password")) .roles("USER") .build(); UserDetails userDetailSecond = User.builder() .username("user2") .password(passwordEncoder.encode("password")) .roles("USER") .build(); return new InMemoryUserDetailsManager(List.of(userDetailFirst, userDetailSecond)); } } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); }
在这里,您有两个强大的选择来处理身份验证:
无论您选择 JdbcUserDetailsManager 还是决定实现自定义 UserDetailsService,两者都将为您的应用程序配备可扩展的、数据库支持的身份验证系统。
以上是Spring 授权服务器 spring security 具有自定义用户详细信息服务,用于灵活的数据驱动身份验证的详细内容。更多信息请关注PHP中文网其他相关文章!