①②③④⑤⑥⑦⑧⑨⑩⑪⑫⑬⑭⑮⑯⑰⑱⑲⑳✕✓✔✖
下面的例子取自Authorization Server Sample中,与 OAuth2 中的有重复,但是代码有不同
这是一个最简单的 authorization-server 程序。
有几个可以参考的代码
下面是具体的配置内容
@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("messaging-client").clientSecret("{noop}secret").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc").redirectUri("http://127.0.0.1:8080/authorized").scope(OidcScopes.OPENID).scope("message.read").scope("message.write").clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();// Save registered client in db as if in-memoryJdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);registeredClientRepository.save(registeredClient);return registeredClientRepository;}@BeanUserDetailsService users() {UserDetails user = User.withUsername("user1").password("{noop}password").roles("USER").build();return new InMemoryUserDetailsManager(user);}
单元测试 1
测试功能的描述
whenLoginSuccessfulThenDisplayNotFoundError
Bad credentials
:whenLoginFailsThenDisplayBadCredentials
/oauth2/authorize
,显示登陆页面:whenNotLoggedInAndRequestingTokenThenRedirectToLogin
code
,并且得到token
:whenLoggingInAndRequestingTokenThenRedirectToClientApplication
代码技巧:
使用@BeforeAll
,要添加@TestInstance(TestInstance.Lifecycle.PER_CLASS)
使用UriComponentsBuilder
用来拼写或者获取标准的 URL 信息。
使用@LocalServerPort
获得当前测试环境的端口号
webClient
进行 Post 的例子
URL url= new URL("http://localhost:"+randomServerPort+tokenRequestStr);WebRequest webRequest=new WebRequest(url, HttpMethod.POST);WebResponse response=this.webClient.getPage(webRequest).getWebResponse();
测试确认功能
openid message.read message.write
,点击确定后成功。whenUserConsentsToAllScopesThenReturnAuthorizationCode
功能描述:
mock
来OAuth2AuthorizationConsentService
返回空,是为了避免以前已经设置过了。http.formLogin(withDefaults());
是否是多余的内? 可以注释掉一个吗?不行
配置 application.yml
spring:security:oauth2:resourceserver:jwt:issuer-uri: http://localhost:9000
安全配置
@EnableWebSecuritypublic class ResourceServerConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{http.authorizeRequests(request->request.antMatchers(HttpMethod.GET,"/message/**").hasAuthority("SCOPE_message.read").anyRequest().authenticated());http.oauth2ResourceServer().jwt();return http.build();}}
这里使用了http.oauth2ResourceServer().jwt();
撰写测试的 rest 接口
@RestControllerpublic class MessagesController {@GetMapping("/messages")public String[] getMessages(String name) {if(StringUtils.hasText(name)){return new String[] {name,"Message 1", "Message 2", "Message 3"};}return new String[] {"Message 1", "Message 2", "Message 3"};}}
复习以前的例子
下面三种写法,功能都是一样的。
http.oauth2ResourceServer((oauth2) -> oauth2.jwt(withDefaults()));http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);http.oauth2ResourceServer().jwt();
获取等前的登陆的用户
@GetMapping("/")public String index(@AuthenticationPrincipal Jwt jwt){return String.format("Hello %s!",jwt.getSubject());}
// 主依赖implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'// html需要的依赖implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'implementation 'org.springframework.boot:spring-boot-starter-web'implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'// 要使用webclient的依赖,reactor-netty 是必须使用的implementation "org.springframework:spring-webflux"implementation "io.projectreactor.netty:reactor-netty"// 本地需要的html资源文件implementation "org.webjars:webjars-locator-core"implementation "org.webjars:bootstrap:3.4.1"implementation "org.webjars:jquery:3.4.1"
配置 yml
有几个重点:
spring:thymeleaf:cache: falsesecurity:oauth2:client:registration:messaging-client-oidc:provider: springclient-id: messaging-clientclient-secret: secretauthorization-grant-type: authorization_coderedirect-uri: 'http://127.0.0.1:8080/login/oauth2/code/{registrationId}'scope: openidclient-name: messaging-client-oidcmessaging-client-authorization-code:provider: springclient-id: messaging-clientclient-secret: secretauthorization-grant-type: authorization_coderedirect-uri: 'http://127.0.0.1:8080/authorized'scope: message.read,message.writeclient-name: messaging-client-authorization-codemessaging-client-client-credentials:provider: springclient-id: messaging-clientclient-secret: secretauthorization-grant-type: client_credentialsscope: message.read,message.writeclient-name: messaging-client-client-credentialsprovider:spring:issuer-uri: http://localhost:9000
安全配置
这个配置与以前的不一样,后续会说明。这里先说重点:
/oauth2/authorization/messaging-client-oidc
@EnableWebSecuritypublic class SecurityConfig {@BeanWebSecurityCustomizer webSecurityCustomizer() {return (web) -> web.ignoring().antMatchers("/webjars/**");}@BeanSecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {http.authorizeRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated()).oauth2Login(oauth2Login ->oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc")).oauth2Client(withDefaults());return http.build();}}
如果不指定登陆页面,那么会显示一个简单的登陆页面,进行登陆,下图是一个示例。
配置 webclient
这个 controller 会链接 resource server,所以需要先要配置 webclient,以便获取 token。
@Configurationpublic class WebClientConfig {@BeanWebClient webClient(OAuth2AuthorizedClientManager authorizedClientManager) {ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2Client =new ServletOAuth2AuthorizedClientExchangeFilterFunction(authorizedClientManager);return WebClient.builder().apply(oauth2Client.oauth2Configuration()).build();}@BeanOAuth2AuthorizedClientManager authorizedClientManager(ClientRegistrationRepository clientRegistrationRepository,OAuth2AuthorizedClientRepository authorizedClientRepository) {OAuth2AuthorizedClientProvider authorizedClientProvider =OAuth2AuthorizedClientProviderBuilder.builder().authorizationCode().refreshToken().clientCredentials().build();DefaultOAuth2AuthorizedClientManager authorizedClientManager = new DefaultOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientRepository);authorizedClientManager.setAuthorizedClientProvider(authorizedClientProvider);return authorizedClientManager;}}
编写 controller
ServletOAuth2AuthorizedClientExchangeFilterFunction.oauth2AuthorizedClient
方法来获取参数@GetMapping(value = "/authorize", params = "grant_type=authorization_code")public String authorizationCodeGrant(Model model, @RegisteredOAuth2AuthorizedClient("messaging-client-authorization-code")OAuth2AuthorizedClient authorizedClient){String[] messages=this.webClient.get().uri(this.messagesBaseUri+"?name=authorization_code").attributes(oauth2AuthorizedClient(authorizedClient)).retrieve().bodyToMono(String[].class).block();model.addAttribute("messages", messages);return "index";}
这里有一个疑问,会不会每次都调用 authorization server 来获得 token 呢?答案肯定是不会的。有两点可以证明
开发的时候,有些拼写错误要注意:
withId(UUID.randomUUID().toString()).clientId("messaging-client").clientSecret("{noop}secret")
RegisteredClient registeredClient=this.registeredClientRepository.findByClientId(clientId);OAuth2AuthorizationConsent currentAuthorizationConsent= this.oAuth2AuthorizationConsentService.findById(registeredClient.getId(), principal.getName());
model 中一些字符拼写错误与模板中的变量不一致
得到 code
在浏览器的地址栏中输入:
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openidmessage.read&state=state&redirect_uri=http://127.0.0.1:8080/authorized
注意事项
如果是第一次登陆,需要认证,请输入系统内置的用户名和密码:user password
如果要确认 Scope,那么会弹出范围确认页面。
使用 Post 认证模式得到 token
curl --location --request POST 'http://localhost:9000/oauth2/token?grant_type=authorization_code&client_id=messaging-client&client_secret=secret&redirect_uri=http://127.0.0.1:8080/authorized&code=Dyr6YdNElqwzSDQqtXQpsZYCP0AUR45FhnFTyC9MGNUlwV2Pu5RW3JE9WJlTLpQkXz0Ae6LHeDOS8GTPMFe7XMePN3p5LtEJcC2tlIPlLP87EW4wrqaE09y4OX0P84kf'
可以跟另外一个例子做对比。,这里使用了OAuth2AuthorizationServerConfigurer
可以做更复杂的配置。
@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception{OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =new OAuth2AuthorizationServerConfigurer<>();authorizationServerConfigurer.authorizationEndpoint(endPoint->endPoint.consentPage(CUSTOM_CONTENT_PAGE_URI));RequestMatcher endpointsMatchers=authorizationServerConfigurer.getEndpointsMatcher();http.requestMatcher(endpointsMatchers).authorizeRequests(request->request.anyRequest().authenticated()).csrf(csrf->csrf.ignoringRequestMatchers(endpointsMatchers)).apply(authorizationServerConfigurer);return http.formLogin(Customizer.withDefaults()).build();}
上一个例子的这部分配置比较简单,是个简化版。
@Bean@Order(1) //参数值越小优先级越高public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception{OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);return http.formLogin(Customizer.withDefaults()).build();}
OAuth2AuthorizationConsent
可以获取用户已经授权的范围
RegisteredClient registeredClient=this.registeredClientRepository.findByClientId(clientId);OAuth2AuthorizationConsent currentAuthorizationConsent= this.oAuth2AuthorizationConsentService.findById(registeredClient.getId(), principal.getName());Set<String> authorizedScopes;if(currentAuthorizationConsent!=null){authorizedScopes=currentAuthorizationConsent.getScopes();}else{authorizedScopes= Collections.emptySet();}
可以参考[security-samples/oauth2/login]中关于 github 的例子。
server:port: 9000spring:security:oauth2:client:registration:github-idp:provider: githubclient-id: ${GITHUB_CLIENT_ID:fef49e9c1a0122055c1779}client-secret: ${GITHUB_CLIENT_SECRET:9f3a9253a880sss871807ba4d21228939a15f2303ce140d}scope: user:email, read:userclient-name: Sign in with GitHubprovider:github:user-name-attribute: login
对比一下与[security-samples/oauth2/login]的不同,上面的例子中,使用了
spring:security:oauth2:client:registration:login-client:provider: springclient-id: login-clientclient-secret: openid-connectclient-authentication-method: client_secret_basicauthorization-grant-type: authorization_coderedirect-uri: http://127.0.0.1:8080/login/oauth2/code/login-clientscope: openid,profileclient-name: Springgithub:client-id: fef49e9c1a0122055c1779client-secret: 9f39253a18808222271807ba4d28939a15f2303ce140dprovider:spring:authorization-uri: http://localhost:9000/oauth2/authorizetoken-uri: http://localhost:9000/oauth2/tokenjwk-set-uri: http://localhost:9000/oauth2/jwks
添加基本的 Security,实现对要访问的 URL 进行保护,需要输入用户名与密码。
@EnableWebSecuritypublic class DefaultSecurityConfig {@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{http.authorizeRequests(request->request.mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults());return http.build();}@BeanUserDetailsService users() {UserDetails user = User.withUsername("user1").password("{noop}password").roles("USER").build();return new InMemoryUserDetailsManager(user);}}
这个章节有类似的例子,可以参考一下,上面的例子是保存在内容中,这个例子中使用了数据库。
需要制作一个@Bean JWKSource<SecurityContext>
@Beanpublic JWKSource<SecurityContext> jwkSource() {RSAKey rsaKey = Jwks.generateRsa();JWKSet jwkSet = new JWKSet(rsaKey);return (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);}
这里Jwks
是一个单独的类,在实际的过程中,可以通过 openSSL 来生成密钥来进行处理。
ProviderSettings
@Beanpublic ProviderSettings providerSettings() {return ProviderSettings.builder().issuer("http://localhost:9000").build();}
SecurityFilterChain
@Bean@Order(Ordered.HIGHEST_PRECEDENCE) //参数值越小优先级越高public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);return http.formLogin(Customizer.withDefaults()).build();}
今后读取注册的客户端信息,都从这个仓库中读取。有存在内存的,有保存到数据库中的。
下面是一个存放到内容的例子:
@Beanpublic RegisteredClientRepository registeredClientRepository(){......return new InMemoryRegisteredClientRepository(loginClient,registeredClient);}
那如何存放到数据库中呢?
将 jdbcTemplate 传入,并返回一个:JdbcRegisteredClientRepository
@Beanpublic RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()).clientId("messaging-client").clientSecret("{noop}secret").clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC).clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST).authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE).authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN).authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS).redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc").redirectUri("http://127.0.0.1:8080/authorized").scope(OidcScopes.OPENID).scope("message.read").scope("message.write").clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()).build();// Save registered client in db as if in-memoryJdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);registeredClientRepository.save(registeredClient);return registeredClientRepository;}
这里使用了一个嵌入式数据库,所以第一步要初始化这个数据库
@Beanpublic EmbeddedDatabase embeddedDatabase() {// @formatter:offreturn new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(EmbeddedDatabaseType.H2).setScriptEncoding("UTF-8").addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-schema.sql").addScript("org/springframework/security/oauth2/server/authorization/oauth2-authorization-consent-schema.sql").addScript("org/springframework/security/oauth2/server/authorization/client/oauth2-registered-client-schema.sql").build();// @formatter:on}
实际上就是定义了一个DataSource
public interface EmbeddedDatabase extends DataSource {/*** Shut down this embedded database.*/void shutdown();}
@Beanpublic OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);}@Beanpublic OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);}
http://localhost:9000/login/oauth2/code/github-idp# 下面是一个老的程序http://127.0.0.1:8080/login/oauth2/code/github
@Beanpublic SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception{// 添加联合身份配置类FederatedIdentityConfigurer federatedIdentityConfigurer = new FederatedIdentityConfigurer().oauth2UserHandler(new UserRepositoryOAuth2UserHandler());http.authorizeRequests(request->request.mvcMatchers("/assets/**", "/webjars/**", "/login").permitAll().anyRequest().authenticated()).formLogin(Customizer.withDefaults()).apply(federatedIdentityConfigurer) // 将联合身份配置类加载进去;return http.build();}
添加了两行
@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);// 添加联合身份配置类http.apply(new FederatedIdentityConfigurer());return http.formLogin(Customizer.withDefaults()).build();}// 添加联合身份Id Token的定制类@Beanpublic OAuth2TokenCustomizer<JwtEncodingContext> idTokenCustomizer() {return new FederatedIdentityIdTokenCustomizer();}
添加了:
http.apply(new FederatedIdentityConfigurer());
@Bean OAuth2TokenCustomizer
用来解析定义的 IDoauth2/authorization/messaging-client-oidc
/oauth2/authorize
,获取 code 页面。但是发现没有认证,就跳转到 9000 的登陆页/oauth2/authorization/github-idp
链接 | 说明 |
---|---|
http://localhost:8080/ | 手工输入,返回 302,要按照 Location 的内容进行跳转 |
http://localhost:8080/oauth2/authorization/messaging-client-oidc | 自动跳转,返回 302,要按照 Location 的内容进行跳转 |
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&state=state&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=cc | 自动跳转,返回 302,要按照 Location 的内容进行跳转 |
http://localhost:9000/login | 这是登陆页面,需要手工输入登陆口令 |
点击登陆 github 链接:http://localhost:9000/oauth2/authorization/github-idp |
接上回,点击 9000 的 github 登陆:/oauth2/authorization/github-idp
跳转到github.com/login/oauth/authorize
,来获得 code
把 github 得到 code,返回到 9000 的/oauth2/code/github-idp
,为了后来获得 token
本来认为要得到 github 的 token 的,谁知道回调到上会显示登陆前的页面:9000/oauth2/authorize?response_type=code
这次因为登陆成功了,所以获取了 code,并返回到8080/login/oauth2/code/messaging-client-oidc
但是返回了错误给这个请求:8080/oauth2/authorization/messaging-client-oidc
由于 8080 登陆错误,然后就重新访问9000/oauth2/authorize?response_type=code
这次 9000 已经登陆成功了,所以成功将 code 返回给8080/login/oauth2/code/messaging-client-oidc
8080 得到 code 后,就跳转到了首页。
8080/authorized?code
,然后返回到具体的页面http.formLogin(Customizer.withDefaults())
是否重复post /login
,可以修改from
例子http.apply(new FederatedIdentityConfigurer());
是否可以删除一个?authorizationRequestUri
从简到复杂,分为三步:
权限认证还是比较复杂的,所以要添加好单元测试:
/test
页面,跳转到登陆页面,然后输入用户名和密码,就可以得到/test
页面返回的内容。user1
用户登陆成功,然后获取 code,接着通过 code 获取 token。user1
用户登陆成功,跳转到获取 code 链接,显示Consent required
页面,在页面中选择范围,然后点击提交按钮后获取 code。user1
用户登陆成功,跳转到获取 code 链接,显示Consent required
页面,在页面中选择取消按钮,就显示error=access_denied
错误。以前 Spring 通过@EnableOAuth2Sso
来实现单点登陆,但是后来这个不推荐了,所以有人在外网问有没有替代的方法:Why is @EnableOAuth2Sso deprecated?。
网上有人回答:在 Spring Security 5.2.x
中,这些注释已被弃用,我们需要使用 DSL
方法。本指出是Spring OAuth2 迁移指南中规定的。
public class SecurityConf extends WebSecurityConfigurerAdapter {@Overrideprotected void configure(HttpSecurity http) throws Exception {http.oauth2Client(); //equivalent to @EnableOAuth2Clienthttp.oauth2Login(); //equivalent to @EnableOAuth2Sso}
所以本例做了演示。
在 html 文件中添加下面的代码,跳转到 9000 认证服务段,然后退出。
<div><a href="http://127.0.0.1:9000/logout">logout</a></div>
上述修改,会有一个严重的问题,就是如果是在浏览器中打开多个标签的情况下,只在其中一个标签点击 logout,并不会影响其他的标签退出。
如何解决这个问题?可能有两种思路:
1、使用 javaScript,来把当前页面全部重置了。
2、使用一个不透明的 athorizationServer
第一步: 输入http://127.0.0.1:8080
第二步:在登陆页面,输入 user1,password
第三步:跳回到 index 页面,可以看到一个 8081 的链接。
第四步:点击链接,跳转到 8081 这个服务,没有出现登陆窗口。
Spring-Authorization-Server 应该是有一个 Bug。
RememberMeAuthenticationFilter
放在OAuth2AuthorizationEndpointFilter
后面,这样就不能自动登陆了。普通 SpringSecurity 中的 Filter
0 = {DisableEncodeUrlFilter@8012}1 = {WebAsyncManagerIntegrationFilter@8011}2 = {SecurityContextPersistenceFilter@8010}3 = {HeaderWriterFilter@8009}4 = {CsrfFilter@8008}5 = {LogoutFilter@8006}6 = {UsernamePasswordAuthenticationFilter@8005}7 = {DefaultLoginPageGeneratingFilter@7993}8 = {DefaultLogoutPageGeneratingFilter@7992}9 = {RequestCacheAwareFilter@7991}10 = {SecurityContextHolderAwareRequestFilter@7990}11 = {RememberMeAuthenticationFilter@7989}12 = {AnonymousAuthenticationFilter@7988}13 = {SessionManagementFilter@7985}14 = {ExceptionTranslationFilter@7979}15 = {FilterSecurityInterceptor@9743}
OAuth2 中的 Filter
0 = {DisableEncodeUrlFilter@8210}1 = {WebAsyncManagerIntegrationFilter@8209}2 = {SecurityContextPersistenceFilter@8208}3 = {ProviderContextFilter@8207}4 = {HeaderWriterFilter@8197}5 = {CsrfFilter@8196}6 = {LogoutFilter@8195}7 = {OAuth2AuthorizationEndpointFilter@8194}8 = {OidcProviderConfigurationEndpointFilter@8193}9 = {NimbusJwkSetEndpointFilter@8192}10 = {OAuth2AuthorizationServerMetadataEndpointFilter@8191}11 = {OAuth2ClientAuthenticationFilter@8190}12 = {RequestCacheAwareFilter@8189}13 = {SecurityContextHolderAwareRequestFilter@8188}14 = {AnonymousAuthenticationFilter@8187}15 = {SessionManagementFilter@8186}16 = {ExceptionTranslationFilter@8181}17 = {FilterSecurityInterceptor@9627}18 = {OAuth2TokenEndpointFilter@10352}19 = {OAuth2TokenIntrospectionEndpointFilter@10353}20 = {OAuth2TokenRevocationEndpointFilter@10354}21 = {OidcUserInfoEndpointFilter@10355}
OAuth2 中添加 RememberMeFilter
这时候,会有一个 bug,因为OAuth2AuthorizationEndpointFilter
在RememberMeAuthenticationFilter
后面,也就是没有认证。
0 = {DisableEncodeUrlFilter@9264}1 = {WebAsyncManagerIntegrationFilter@9263}2 = {SecurityContextPersistenceFilter@8077}3 = {ProviderContextFilter@8076}4 = {HeaderWriterFilter@8075}5 = {CsrfFilter@8074}6 = {LogoutFilter@8073}7 = {OAuth2AuthorizationEndpointFilter@8071}8 = {OidcProviderConfigurationEndpointFilter@9312}9 = {NimbusJwkSetEndpointFilter@9311}10 = {OAuth2AuthorizationServerMetadataEndpointFilter@9310}11 = {OAuth2ClientAuthenticationFilter@9309}12 = {RequestCacheAwareFilter@9308}13 = {SecurityContextHolderAwareRequestFilter@9307}14 = {RememberMeAuthenticationFilter@9302}15 = {AnonymousAuthenticationFilter@9332}16 = {SessionManagementFilter@9331}17 = {ExceptionTranslationFilter@9329}18 = {FilterSecurityInterceptor@9702}19 = {OAuth2TokenEndpointFilter@9703}20 = {OAuth2TokenIntrospectionEndpointFilter@9704}21 = {OAuth2TokenRevocationEndpointFilter@9705}22 = {OidcUserInfoEndpointFilter@9706}
OAuth2AuthorizationEndpointConfigurer.class
Thank you for your reply.
The PR purpose is to modify OAuth2AuthorizationEndpointConfigurer.class, Because the wrong filter's order will cause bugs in the “remember-me” function.
在RememberMeAuthenticationFilter
代码中,会判断是否登陆过:
@Overrideprotected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)throws ServletException, IOException {if (!this.authorizationEndpointMatcher.matches(request)) {filterChain.doFilter(request, response);return;}try {......................................if (!authorizationCodeRequestAuthenticationResult.isAuthenticated()) {// If the Principal (Resource Owner) is not authenticated then// pass through the chain with the expectation that the authentication process// will commence via AuthenticationEntryPointfilterChain.doFilter(request, response);return;}......................................} catch (OAuth2AuthenticationException ex) {this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);}}
将 OAuth2AuthorizationEndpointFilter 放在后面的效果。
0 = {DisableEncodeUrlFilter@7168}1 = {WebAsyncManagerIntegrationFilter@7167}2 = {SecurityContextPersistenceFilter@7166}3 = {ProviderContextFilter@7165}4 = {HeaderWriterFilter@7164}5 = {CsrfFilter@7163}6 = {LogoutFilter@7162}7 = {OidcProviderConfigurationEndpointFilter@7154}8 = {NimbusJwkSetEndpointFilter@7153}9 = {OAuth2AuthorizationServerMetadataEndpointFilter@7152}10 = {OAuth2ClientAuthenticationFilter@7151}11 = {RequestCacheAwareFilter@7150}12 = {SecurityContextHolderAwareRequestFilter@7149}13 = {RememberMeAuthenticationFilter@7147}14 = {AnonymousAuthenticationFilter@8135}15 = {SessionManagementFilter@8134}16 = {OAuth2AuthorizationEndpointFilter@8128}17 = {ExceptionTranslationFilter@8662}18 = {FilterSecurityInterceptor@8663}19 = {OAuth2TokenEndpointFilter@8664}20 = {OAuth2TokenIntrospectionEndpointFilter@8665}21 = {OAuth2TokenRevocationEndpointFilter@8666}22 = {OidcUserInfoEndpointFilter@8667}
To implement the “remember-me” feature , need to modify oauth2authorizationendpointconfigurer.class
, put oauth2authorizationendpointfilter
after remembermeauthenticationfilter
.
Step1: modify OAuth2AuthorizationEndpointConfigurer.class
's code of spring-authorization-server
// builder.addFilterBefore(postProcess(authorizationEndpointFilter), AbstractPreAuthenticatedProcessingFilter.class);builder.addFilterBefore(postProcess(authorizationEndpointFilter), ExceptionTranslationFilter.class);
Step2: Modify the default-authorizationserver
's code in samples and test it
AuthorizationServerConfig.class
public class AuthorizationServerConfig {@Bean@Order(Ordered.HIGHEST_PRECEDENCE)public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http, UserDetailsService users) throws Exception {OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);// @formatter:offhttp.exceptionHandling(exceptions ->exceptions.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login")));http.rememberMe((rememberMe) -> {rememberMe.userDetailsService(users);rememberMe.key("123");});// @formatter:onreturn http.build();}
DefaultSecurityConfig.class
public class DefaultSecurityConfig {// @formatter:off@BeanSecurityFilterChain defaultSecurityFilterChain(HttpSecurity http, UserDetailsService users) throws Exception {http.authorizeRequests(authorizeRequests ->authorizeRequests.anyRequest().authenticated()).formLogin(withDefaults());http.rememberMe((rememberMe) -> {rememberMe.userDetailsService(users);rememberMe.key("123");});return http.build();}
Step3: Test
run application