HI
调试网关和认证服务之间的单点登录流程并且简单记录了一下,主要是留着自己备用,开发过程中其实有挺多问题,简要记录下,如果后面有空的话再好好整理吧。
已经是开发了一段时间的项目,所以可能个别有自定义的类,但是自定义的类基本也是从原来的类中cp出来的,源码名字会很类似
过滤器链
-> DefaultWebFilterChain // 遍历调用器链入口
WebFilterChainProxy
WebFluxHttpSecurity
HttpHeaderWriterWebFilter
ReactorContextWebFilter
OAuth2AuthorizationRequestRedirectWebFilter // 匹配/oauth2/authorization/{registrationId},识别客户端信息
AuthenticationWebFilter // 匹配/login/oauth2/code/{registrationId}
AuthenticationWebFilter
LoginPageGeneratingWebFilter // GET /login
LogoutPageGeneratingWebFilter // GET /logout
SecurityContextServerWebExchangeWebFilter
ServerRequestCacheWebFilter
LogoutWebFilter // POST /logout
ExceptionTranslationWebFilter
AuthorizationWebFilter
主要流程调用链
请求认证
- 发起认证 http://10.14.3.244:9007/oauth2/authorization/venus-test
OAuth2AuthorizationRequestRedirectWebFilter.filter()
-> DefaultServerOAuth2AuthorizationRequestResolver.resolve()
-> authorizationRequest(exchange, clientRegistration) // 拼接redirectUriStr:http://10.14.3.244:9007/login/oauth2/code/venus-test
-> sendRedirectForAuthorization(exchange, clientRegistration)
-> WebSessionOAuth2ServerAuthorizationRequestRepository.saveAuthorizationRequest() // 存储<state, authorizationRequest> 到 exchange.getSession()
-> DefaultServerRedirectStrategy.sendRedirect() // 重定向到
- 请求授权 http://10.14.3.244:9007/oauth/authorize?response_type=code&client_id=venus-test&scope=admin&state=nzDaWairJyBbopcR7_XYYrSL-0s9hqesFKYJfUOhf0Y%3D&redirect_uri=http://10.14.3.244:9007/login/oauth2/code/venus-test
- code 模式请求 token: http://10.14.3.244:9007/login/oauth2/code/venus-test?code=skrqTM&state=nKOkQvzZf_sml-qTwrr_uXrkVvjkmAoMBw35xvyoqc0%3D
OAuth2LoginAuthenticationWebFilter.authenticate()
-> (super)AuthenticationWebFilter.authenticate() // 开始认证
-> this.authenticationManager.authenticate(token) // token <- OAuth2AuthorizationCodeAuthenticationToken
// DelegatingReactiveAuthenticationManager类
-> m.authenticate(authentication) // 第二次遍历
-> OAuth2LoginReactiveAuthenticationManager.authenticate(Authentication authentication)
-> OAuth2AuthorizationCodeReactiveAuthenticationManager.authenticate(token)
-> this.accessTokenResponseClient.getTokenResponse(authzRequest) // 实际发送请求,获取 token 数据
-> OAuth2AuthorizationCodeReactiveAuthenticationManager.onSuccess // refreshToken 和 accessToken 构造到 OAuth2AuthorizationCodeAuthenticationToken 中
-> OAuth2LoginReactiveAuthenticationManager.onSuccess
-> DefaultReactiveOAuth2UserService.loadUser // 查询用户信息、构造OAuth2LoginAuthenticationToken
-> OAuth2LoginAuthenticationWebFilter.onAuthenticationSuccess
-> AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository(默认,该bean后修改为WebSessionServerOAuth2AuthorizedClientRepository).saveAuthorizedClient
-> InMemoryReactiveOAuth2AuthorizedClientService.saveAuthorizedClient // 存储到map,<base64([venus-test][管理员]),认证信息OAuth2AuthorizedClient>
-> (super)AuthenticationWebFilter.onAuthenticationSuccess
-> securityContext.setAuthentication(authentication) // 上下文存储认证信息
// context当前格式:org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken@fb50e915: Principal: Name: [管理员], Granted Authorities: [ROLE_USER], User Attributes: [sub=管理员, org=预留科室, roles=[auditor, admin, guard], scope=[admin], oid=25, exp=2023-03-24T20:41:07Z, iat=2023-01-14T10:01:07Z, jti=1cd7ab5a-5a3f-4cf5-b93c-03d4203fecbe, client_id=venus-test, sid=0]; Credentials: [PROTECTED]; Authenticated: true; Details: null; Granted Authorities: ROLE_USER
-> session.getAttributes().put("SPRING_SECURITY_CONTEXT", context)
登陆后,各个请求网关将携带headers
-> TokenRelayGatewayFilterFactory.apply(Object config)
-> authorizedClient(exchange, authentication)
-> WebSessionServerOAuth2AuthorizedClientRepository.loadAuthorizedClient // 根据登录时存储的 map,将人员认证信息取出来
-> withBearerAuth(exchange, token) // 添加 token 到 headers 中
Token 信息存储
DefaultWebSessionManager.save
需要定义 ServerOAuth2AuthorizedClientRepository
作用:
维护一个map:<base64([client_id] [username]), OAuth2AuthorizedClient>,登录时存入认证信息,调用接口时在此处取出,并将 token 添加到 headers 中
实现:
默认使用类 AuthenticatedPrincipalServerOAuth2AuthorizedClientRepository -> InMemoryReactiveOAuth2AuthorizedClientService,map存储在内存中,重启后必须重新登录
若修改到 redis 存储方式,使用 WebSessionServerOAuth2AuthorizedClientRepository,存储到 session 中,该信息会一并存入 redis
@Bean
public ServerOAuth2AuthorizedClientRepository authorizedClientRepository() {
return new WebSessionServerOAuth2AuthorizedClientRepository();
}
已遇到的问题
重复登录 404 问题
首次登录,发送单点请求,未认证状态,请求到 sso
登陆后发送单点登录请求,已认证状态,不再请求 sso,直接报404
ServerHttpSecurity 的 createAttemptAuthenticationRequestMatcher 方法指定了请求路径/login/oauth2/code/{registrationId}并且未认证状态才能进入登录页面,我们项目中重写了 ServerHttpSecurity 屏蔽了这段代码:
private ServerWebExchangeMatcher createAttemptAuthenticationRequestMatcher() {
PathPatternParserServerWebExchangeMatcher loginPathMatcher =
new PathPatternParserServerWebExchangeMatcher("/login/oauth2/code/{registrationId}");
/// 此处限制重复发起认证 2 次请求时会导致404,暂时取消 notAuthenticatedMatcher
// ServerWebExchangeMatcher notAuthenticatedMatcher = e -> ReactiveSecurityContextHolder.getContext()
// .flatMap(p -> ServerWebExchangeMatcher.MatchResult.notMatch())
// .switchIfEmpty(ServerWebExchangeMatcher.MatchResult.match());
// return new AndServerWebExchangeMatcher(loginPathMatcher, notAuthenticatedMatcher);
return new AndServerWebExchangeMatcher(loginPathMatcher);
}
怎么指定接口校验权限?
ServerHttpSecurity 类中有 hasAuthority 和 hasRole 方法,其中 hasRole 会自动指定 ROLE_前缀来拼接一下,实际调用的还是hasAuthority。
role 和 scope 的来源正常是根据 token 属性获取的,token 中原本是不带 SCOPE_、ROLE_前缀的,通过 JwtAuthenticationConverter 的 extractAuthorities 方法,将 token 中的 scope 属性增加前缀后,再去与要 check 的属性作比较。我截图的 JwtAuthenticationConverter 这里是只有 SCOPE_的,但是这个类我们项目自己重写了,处理了 SCOPE_和 ROLE_,默认流程的话是否直接包含 ROLE_的处理要自己调试看看。
// 校验 /api/test/ 开头的 url,认证的 scope 属性包含 test
http.authorizeExchange().pathMatchers("/api/test/**").hasAuthority("SCOPE_test");
// 校验 /api/test/ 开头的 url,认证的 role 属性包含 admin
http.authorizeExchange().pathMatchers("/api/test/**").hasRole("admin");
// 效果与上一行一致
http.authorizeExchange().pathMatchers("/api/test/**").hasAuthority("ROLE_admin");
// 指定匹配的 Matchers:
http.authorizeExchange().matchers( 一个ServerWebExchangeMatcher 的子类) .hasRole("admin");
// 如指定携带头信息 X-Type=test的 才校验
http.authorizeExchange().matchers(new HeaderServerWebExchangeMatcher("X-Type", "test")) .hasRole("admin");
// 指定 /api/test/ 开头并且头信息存在才校验
AndServerWebExchangeMatcher requestMatcher = new AndServerWebExchangeMatcher(
new PathPatternParserServerWebExchangeMatcher("/api/test/**"), new HeaderServerWebExchangeMatcher("X-Type", "test"));
http.authorizeExchange().matchers(requestMatcher ) .hasRole("admin");