0


Spring Security 自定义授权服务器实践

活动地址:CSDN21天学习挑战赛

相关文章:

  1. OAuth2的定义和运行流程
  2. Spring Security OAuth实现Gitee快捷登录
  3. Spring Security OAuth实现GitHub快捷登录
  4. Spring Security的过滤器链机制
  5. Spring Security OAuth Client配置加载源码分析
  6. Spring Security内置过滤器详解
  7. 为什么加载了两个OAuth2AuthorizationRequestRedirectFilter分析

前言

在之前我们已经对接过了GitHub、Gitee客户端,使用OAuth2 Client能够快速便捷的集成第三方登录,集成第三方登录一方面降低了企业的获客成本,同时为用户提供更为便捷的登录体验。
但是随着企业的发展壮大,越来越有必要搭建自己的OAuth2服务器。
OAuth2不仅包括前面的OAuth客户端,还包括了授权服务器,在这里我们要通过最小化配置搭建自己的授权服务器。
授权服务器主要提供OAuth Client注册、用户认证、token分发、token验证、token刷新等功能。实际应用中授权服务器与资源服务器可以在同一个应用中实现,也可以拆分成两个独立应用,在这里为了方便理解,我们拆分成两个应用。

授权服务器变迁

授权服务器(Authorization Server)目前并没有集成在Spring Security项目中,而是作为独立项目存在于Spring生态中,图1为Spring Authorization Server 在Spring 项目列表中的位置。
image.png

图1

Spring Authorization Server 为什么没被集成在Spring Security中呢?

起因是因为Spring 中的Spring Security OAuth、Spring Cloud Security都对OAuth有自己的实现,Spring团队开始是想把OAuth独立出来放到Spring Security中,但是后面Spring团队意识到OAuth授权服务并不适合包含在Spring Security框架中,于是在2019年11月Spring宣布不在Spring Security中支持授权服务器。原文如下:

原文:
Since the Spring Security OAuth project was created, the number of authorization server choices has grown significantly. Additionally, we did not feel like creating an authorization server was a common scenario. Nor did we feel like it was appropriate to provide authorization support within a framework with no library support. After careful consideration, the Spring Security team decided that we would not formally support creating authorization servers.

但是对于Spring Security不再支持授权服务器,社区反应强烈。于是在2020年4月,Spring推出了Spring Authorization Server项目。
目前项目最新GA版本为0.3 GA,预览版本1.0.0-M1。

最小化配置

安装授权服务器

1、新创建一个Spring Boot项目,命名为

  1. spring-security-authorization-server

2、引入pom依赖

  1. <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-security</artifactId></dependency><dependency><groupId>org.springframework.security</groupId><artifactId>spring-security-oauth2-authorization-server</artifactId><version>0.3.1</version></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency>

配置授权服务器

  1. importcom.nimbusds.jose.jwk.JWKSet;importcom.nimbusds.jose.jwk.RSAKey;importcom.nimbusds.jose.jwk.source.ImmutableJWKSet;importcom.nimbusds.jose.jwk.source.JWKSource;importcom.nimbusds.jose.proc.SecurityContext;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importorg.springframework.core.Ordered;importorg.springframework.core.annotation.Order;importorg.springframework.security.config.Customizer;importorg.springframework.security.config.annotation.web.builders.HttpSecurity;importorg.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;importorg.springframework.security.core.userdetails.User;importorg.springframework.security.core.userdetails.UserDetails;importorg.springframework.security.core.userdetails.UserDetailsService;importorg.springframework.security.crypto.factory.PasswordEncoderFactories;importorg.springframework.security.oauth2.core.AuthorizationGrantType;importorg.springframework.security.oauth2.core.ClientAuthenticationMethod;importorg.springframework.security.oauth2.core.oidc.OidcScopes;importorg.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClient;importorg.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;importorg.springframework.security.oauth2.server.authorization.config.ClientSettings;importorg.springframework.security.oauth2.server.authorization.config.ProviderSettings;importorg.springframework.security.provisioning.InMemoryUserDetailsManager;importorg.springframework.security.web.SecurityFilterChain;importorg.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;importorg.springframework.security.web.util.matcher.RequestMatcher;importjava.security.KeyPair;importjava.security.KeyPairGenerator;importjava.security.interfaces.RSAPrivateKey;importjava.security.interfaces.RSAPublicKey;importjava.util.UUID;@Configuration(proxyBeanMethods =false)publicclassAuthorizationServerConfig{//授权端点过滤器链@Bean@Order(Ordered.HIGHEST_PRECEDENCE)publicSecurityFilterChainauthorizationServerSecurityFilterChain(HttpSecurity http)throwsException{OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =newOAuth2AuthorizationServerConfigurer<>();RequestMatcher endpointsMatcher = authorizationServerConfigurer
  2. .getEndpointsMatcher();
  3. http
  4. //没有认证会自动跳转到/login页面.exceptionHandling((exceptions)-> exceptions
  5. .authenticationEntryPoint(newLoginUrlAuthenticationEntryPoint("/login"))).requestMatcher(endpointsMatcher).authorizeRequests(authorizeRequests ->
  6. authorizeRequests.anyRequest().authenticated()).csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)).apply(authorizationServerConfigurer);return http.build();}//用于身份验证的过滤器链@Bean@Order(2)publicSecurityFilterChaindefaultSecurityFilterChain(HttpSecurity http)throwsException{
  7. http
  8. .authorizeHttpRequests((authorize)-> authorize
  9. .anyRequest().authenticated()).formLogin(Customizer.withDefaults());return http.build();}//配置主体用户@BeanpublicUserDetailsServiceuserDetailsService(){UserDetails userDetails =User.withDefaultPasswordEncoder().username("user").password("user").roles("USER").build();returnnewInMemoryUserDetailsManager(userDetails);}//注册客户端@BeanpublicRegisteredClientRepositoryregisteredClientRepository(){RegisteredClient registeredClient =RegisteredClient.withId(UUID.randomUUID().toString())//客户端id.clientId("testClientId")//客户端秘钥,授权服务器需要加密存储.clientSecret(PasswordEncoderFactories.createDelegatingPasswordEncoder().encode("testClientSecret"))//授权方法.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)//支持的授权类型.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)//回调地址,支持多个,本地测试不能使用localhost.redirectUri("http://127.0.0.1:8080/login/oauth2/code/customize").scope(OidcScopes.OPENID)//授权scope.scope("message.read").scope("userinfo").scope("message.write")//是否需要授权页面,开启跳转到授权页面,需要手动确认.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();returnnewInMemoryRegisteredClientRepository(registeredClient);}//token加密@BeanpublicJWKSource<SecurityContext>jwkSource(){KeyPair keyPair =generateRsaKey();RSAPublicKey publicKey =(RSAPublicKey) keyPair.getPublic();RSAPrivateKey privateKey =(RSAPrivateKey) keyPair.getPrivate();RSAKey rsaKey =newRSAKey.Builder(publicKey).privateKey(privateKey).keyID(UUID.randomUUID().toString()).build();JWKSet jwkSet =newJWKSet(rsaKey);returnnewImmutableJWKSet<>(jwkSet);}privatestaticKeyPairgenerateRsaKey(){KeyPair keyPair;try{KeyPairGenerator keyPairGenerator =KeyPairGenerator.getInstance("RSA");
  10. keyPairGenerator.initialize(2048);
  11. keyPair = keyPairGenerator.generateKeyPair();}catch(Exception ex){thrownewIllegalStateException(ex);}return keyPair;}//配置协议端点,比如/oauth2/authorize、/oauth2/token等@BeanpublicProviderSettingsproviderSettings(){returnProviderSettings.builder().build();}}

如上是最小化授权服务器的配置,这里我们将授权主体和客户端都存储在内存中,当然也可以持久化到数据库中,分别使用

  1. JdbcUserDetailsManager

  1. JdbcRegisteredClientRepository

  1. ProviderSettings.builder().build()

使用了默认的配置,这几个地址我们后面就会用到:

  1. publicstaticBuilderbuilder(){returnnewBuilder().authorizationEndpoint("/oauth2/authorize").tokenEndpoint("/oauth2/token").jwkSetEndpoint("/oauth2/jwks").tokenRevocationEndpoint("/oauth2/revoke").tokenIntrospectionEndpoint("/oauth2/introspect").oidcClientRegistrationEndpoint("/connect/register").oidcUserInfoEndpoint("/userinfo");}

❗ 官方指出@Import(OAuth2AuthorizationServerConfiguration.class)也可以用来最小化配置,但我亲测这种方式没多大用处,并且还有问题。

配置客户端

这里我们要使用自己的搭建授权服务器,需要自定义一个客户端,还是使用前面集成GitHub的示例,只要在配置文件中扩展就可以。
完整配置如下:

  1. spring:security:oauth2:client:registration:gitee:client-id: gitee_clientId
  2. client-secret: gitee_secret
  3. authorization-grant-type: authorization_code
  4. redirect-uri:'{baseUrl}/login/oauth2/code/{registrationId}'client-name: Gitee
  5. github:client-id: github_clientId
  6. client-secret: github_secret
  7. # 自定义customize:client-id: testClientId
  8. client-secret: testClientSecret
  9. authorization-grant-type: authorization_code
  10. redirect-uri:'{baseUrl}/login/oauth2/code/{registrationId}'client-name: Customize
  11. scope:- userinfo
  12. provider:gitee:authorization-uri: https://gitee.com/oauth/authorize
  13. token-uri: https://gitee.com/oauth/token
  14. user-info-uri: https://gitee.com/api/v5/user
  15. user-name-attribute: name
  16. # 自定义customize:authorization-uri: http://localhost:9000/oauth2/authorize
  17. token-uri: http://localhost:9000/oauth2/token
  18. user-info-uri: http://localhost:9000/userinfo
  19. user-name-attribute: username

❗ 在配置授权服务器uri的时候,请勿依旧使用127.0.0.1,由于是在本地测试,授权服务器的session和客户端的session会互相覆盖,导致莫名其妙的问题。
请区分回调地址,和授权服务器端点uri的地址。
211e791bab68cfa344eb5baef4f9e57.jpg

客户端的session

3cf1ff7e476779505f0ba034160ee73.jpg

授权服务器的session

体验

另外为了能够更好的调式,可以在两个应用增加

  1. @EnableWebSecurity(debug = true)

和 log日志,日志如下,打开

  1. TRACE

级别日志:

  1. logging:level:root: INFO
  2. org.springframework.web: INFO
  3. org.springframework.security: TRACE
  4. org.springframework.security.oauth2: TRACE

现在启动两个应用,访问

  1. http://127.0.0.1:8080/hello

,自动跳转到登录页面。
image.png
点击

  1. Customize

,将跳转至授权服务器,注意看地址栏地址为localhost:9000/login,输入

  1. 用户名/密码登录,user/user


image.png
登录后,将跳转至授权页面,由于我们没有定制,使用的是默认页面,可以看到该页面的地址为

  1. http://localhost:9000/oauth2/authorize?response_type=code&client_id=testClientId&scope=userinfo&state=yV1ElAN2855yq3bY5kgj_rmilnCclyvZHkxVB7a1d84%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/customize


image.png
我们勾选userinfo,提交后即跳转回客户端。
我们看下客户收到的日志,授权服务器带着code回调了我们填写的回调地址。

  1. Request received for GET '/login/oauth2/code/customize?code=DPAlx5uyrUpfrZIlBKrpIy_mmcgiyC2qCxPFtUeLA0fBrZd238XM2vN8M1jv9XAgl0KA-D54P_KzVH7RbUw7ApBUc2pbnuSVRZUyHazozmNM4YgQ06CZryfr20qLRhW4&state=_Sgak7GLILLKbwr9JVuwA2xVp95CWPgUMByQcvePkgM%3D'
  1. ************************************************************Request received for GET '/login/oauth2/code/customize?code=DPAlx5uyrUpfrZIlBKrpIy_mmcgiyC2qCxPFtUeLA0fBrZd238XM2vN8M1jv9XAgl0KA-D54P_KzVH7RbUw7ApBUc2pbnuSVRZUyHazozmNM4YgQ06CZryfr20qLRhW4&state=_Sgak7GLILLKbwr9JVuwA2xVp95CWPgUMByQcvePkgM%3D':
  2. org.apache.catalina.connector.RequestFacade@1a8761d0
  3. servletPath:/login/oauth2/code/customize
  4. pathInfo:nullheaders:host: 127.0.0.1:8080connection: keep-alive
  5. upgrade-insecure-requests:1user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.0.0 Safari/537.36
  6. accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
  7. sec-fetch-site: cross-site
  8. sec-fetch-mode: navigate
  9. sec-fetch-user:?1
  10. sec-fetch-dest: document
  11. sec-ch-ua: "Chromium";v="104", " Not A;Brand";v="99", "Google Chrome";v="104"
  12. sec-ch-ua-mobile:?0
  13. sec-ch-ua-platform:"Windows"referer: http://127.0.0.1:8080/
  14. accept-encoding: gzip, deflate, br
  15. accept-language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
  16. cookie: JSESSIONID=2527F412F53FA27A30BFBC39161ABB63
  17. Security filter chain:[
  18. DisableEncodeUrlFilter
  19. WebAsyncManagerIntegrationFilter
  20. SecurityContextPersistenceFilter
  21. HeaderWriterFilter
  22. CsrfFilter
  23. LogoutFilter
  24. OAuth2AuthorizationRequestRedirectFilter
  25. OAuth2AuthorizationRequestRedirectFilter
  26. OAuth2LoginAuthenticationFilter
  27. DefaultLoginPageGeneratingFilter
  28. DefaultLogoutPageGeneratingFilter
  29. RequestCacheAwareFilter
  30. SecurityContextHolderAwareRequestFilter
  31. AnonymousAuthenticationFilter
  32. OAuth2AuthorizationCodeGrantFilter
  33. SessionManagementFilter
  34. ExceptionTranslationFilter
  35. FilterSecurityInterceptor
  36. ]************************************************************

总结

Spring Security 的最小化授权服务器的配置,到这里结束了,该demo虽然代码量非常少,但涉及的知识非常多,并且坑也多。

Spring Security文档中的代码说明更新不及时,比如

  1. @Import(OAuth2AuthorizationServerConfiguration.class)

文档中说明是最小化配置,但文档的快速开始又提供了另外一种的最小化配置方式。

另外授权服务器如果发生异常,是不会打印堆栈的,而是把错误信息放入到response中,是打算要在页面上显示,然而demo的默认错误页并不会显示错误详情,只有错误编号400,如图。

8504aba9d06b7e267befd0a244ddb07.jpg
Spring Authorization Server 还需要多多完善,Spring Security也不例外,不久前我还提了一个PR,把一个持续数个版本的bug给修复了🤣(过了,只是文档中的错误罢了,被标记为文档中的bug😅),看多了外国人的产品,其实也没有太比国内的开源项目好,坑也很多,而我们某些大厂的开源项目其实很好,却被网友门各种喷。

image.png


本文转载自: https://blog.csdn.net/weixin_40972073/article/details/126397563
版权归原作者 阿提说说 所有, 如有侵权,请联系我们删除。

“Spring Security 自定义授权服务器实践”的评论:

还没有评论