背景
搭建gateway + oauth-client 的网关服务 和 oauth-Authorisation-server授权服务
gateway的底层是netty,导致oauth-client需要使用响应式编程,但是授权服务使用的是oauth2.0的web方式编程,所以userInfo端点,jwk端点等都需要手动搭建。
前置概念
- nonce
- 安全工程中,Nonce是一个在加密通信只能使用一次的数字。在认证协议中,它往往是一个随机或伪随机数,以避免重放攻击。
- 在搭建oauth-client-server和Authorisation-server联调时,可以发现在访问oauth-client-server未授权资源时会重定向到/oauth/authorize端点而且携带了Nonce参数,并且oauth-client服务器在获取到/oauth/token端点的response后,需要对Nonce进行校验
jwkSetUri
是指授权服务器提供的获取JWK配置的well-known端点issuerUri
是指获取OAuth2.0 授权服务器用户元数据的端点- 现搭的授权服务器的userInfo端点是解析jwt中的payload,并且返回payload部分的json内容
OAuth-client开启客户端配置
@Configuration
public class SecurityConfig {
@Bean
SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http) {
http.authorizeExchange()
.anyExchange().authenticated();
// 开启 OAuth2 登录
http.oauth2Login();
return http.build();
}
}
oauth2Login拦截器配置概述
-
首先需要注意的是 所有的初始配置都是在ServerHttpSecurity类中配置,该小节的内容都在ServerHttpSecurity类中
public class ServerHttpSecurity { public SecurityWebFilterChain build() { if (this.built != null) { throw new IllegalStateException("This has already been built with the following stacktrace. " + this.buildToString()); } else { //........................ if (this.oauth2Login != null) { if (this.oauth2Login.securityContextRepository != null) { this.oauth2Login.securityContextRepository(this.oauth2Login.securityContextRepository); } else if (this.securityContextRepository != null) { this.oauth2Login.securityContextRepository(this.securityContextRepository); } else { this.oauth2Login.securityContextRepository(new WebSessionServerSecurityContextRepository()); } //oauth2login初始配置方法 this.oauth2Login.configure(this); } //........................ //每个过滤器都有一个order权重,过滤器添加完毕后,会进行一次排序 AnnotationAwareOrderComparator.sort(this.webFilters); List<WebFilter> sortedWebFilters = new ArrayList(); this.webFilters.forEach((f) -> { if (f instanceof OrderedWebFilter) { f = ((OrderedWebFilter)f).webFilter; } sortedWebFilters.add(f); }); sortedWebFilters.add(0, new ServerWebExchangeReactorContextWebFilter()); return new MatcherSecurityWebFilterChain(this.getSecurityMatcher(), sortedWebFilters); } } }
-
this.oauth2Login.configure(this); 初始配置方法
- 配置方法中主要是为过滤器链配置了2个过滤器,替代人工完成oauth2.0的认证
- OAuth2AuthorizationRequestRedirectWebFilter
- 拦截需授权资源,重定向授权服务器/oauth/authorize端点。
- AuthenticationWebFilter
- 拦截授权服器 携带code请求,重定向授权服务器/oauth/token端点,获取token。
- OAuth2AuthorizationRequestRedirectWebFilter
- 配置方法中主要是为过滤器链配置了2个过滤器,替代人工完成oauth2.0的认证
OAuth2AuthorizationRequestRedirectWebFilter
OAuth2AuthorizationRequestRedirectWebFilter 与授权服务器关联概述
OAuth2AuthorizationRequestRedirectWebFilter过滤器会将 请求uri 和 你所配置的放行uri进行匹配,如果匹配的就会放行,不放行的就会重定向到授权服务器。 重定向到授权服务器的地址就是你在yml中配置的authorization-uri指定的地址 但是在进入/oauth/authorize端点之前会有两种情况, 一种是通过身份认证直接重定向到授权服务器, 一种是未通过身份认证直接重定向到授权服务器。 如果通过了就正常走端点逻辑就行。 【题外话:代码中是如何判断是否通过了身份认证? 如果principal不为空且它的principal.Authenticated()为true,即为认证过。 】 但是如果未通过,进入/oauth/authorize端点, 则会重定向到/login进行登陆被UsernamePasswordAuthenticationFilter拦截器拦截。 登陆成功之后,会重新定向到/oauth/authorize端点 【题外话:UsernamePasswordAuthenticationFilter如何重定向到/oauth/authorize端点? 在身份认证成功之后,通过onAuthenticationSuccess方法(认证成功之后会调用认证成功方法) 进行直接重定向回到/oauth/authorize端点 】 在/oauth/authorize端点中进行正常的逻辑执行,如果传递的对应client以及相关信息无误, 则会根据将传递过来的redirectUri并添加上code以及state信息重定向回客户端
OAuth2LoginAuthenticationWebFilter
OAuth2LoginAuthenticationWebFilter与授权服务器关联概述
问题来了0.0,授权服务器重定向的地址是我设置,那我要设置什么样的redirectUri, 才能被客户端拦截住呢? 当然,肯定是会有一个默认配置地址的
可以看到OAuth2LoginAuthenticationWebFilter拦截器默认拦截的匹配地址就是: /login/oauth2/code/{registrationId}
进源码可知,OAuth2LoginAuthenticationWebFilter实际上只是重写了认证成功之后 的成功方法onAuthenticationSuccess()方法以及AuthenticatedPrincipal(身份信息) 的存储方式
public class OAuth2LoginAuthenticationWebFilter extends AuthenticationWebFilter { //身份信息存储方式实现类 private final ServerOAuth2AuthorizedClientRepository authorizedClientRepository; //认证成功的执行方法,对身份信息进行存储 protected Mono<Void> onAuthenticationSuccess(Authentication authentication, WebFilterExchange webFilterExchange) { OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken)authentication; OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(authenticationResult.getClientRegistration(), authenticationResult.getName(), authenticationResult.getAccessToken(), authenticationResult.getRefreshToken()); OAuth2AuthenticationToken result = new OAuth2AuthenticationToken(authenticationResult.getPrincipal(), authenticationResult.getAuthorities(), authenticationResult.getClientRegistration().getRegistrationId()); return this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, authenticationResult, webFilterExchange.getExchange()).then(super.onAuthenticationSuccess(result, webFilterExchange)); } }
实际上是AuthenticationWebFilter对授权服务器携带code请求 (/login/oauth2/code/{registrationId})进行拦截
public class AuthenticationWebFilter implements WebFilter { //拦截过/login/oauth2/code/{registrationId}请求 public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { //对请求的uri进行matches return this.requiresAuthenticationMatcher.matches(exchange).filter((matchResult) -> { return matchResult.isMatch(); }).flatMap((matchResult) -> { return this.authenticationConverter.convert(exchange); }).switchIfEmpty(chain.filter(exchange).then(Mono.empty())).flatMap((token) -> { //进行身份验证,根据code获取token同时,与对应jwk元信息,userInfo元信息对比 //就是调用下面这个方法 return this.authenticate(exchange, chain, token); }).onErrorResume(AuthenticationException.class, (e) -> { return this.authenticationFailureHandler.onAuthenticationFailure(new WebFilterExchange(exchange, chain), e); }); } private Mono<Void> authenticate(ServerWebExchange exchange, WebFilterChain chain, Authentication token) { return this.authenticationManagerResolver.resolve(exchange).flatMap((authenticationManager) -> { //进行身份信息,jwk元信息,userInfo原信息校验 return authenticationManager.authenticate(token); }).switchIfEmpty(Mono.defer(() -> { //身份信息,jwk元信息,userInfo原信息校验失败,报异常 return Mono.error(new IllegalStateException("No provider found for " + token.getClass())); })).flatMap((authentication) -> { //身份信息,jwk元信息,userInfo原信息校验成功,执行身份认证成功方法 return this.onAuthenticationSuccess(authentication, new WebFilterExchange(exchange, chain)); }); } }
首先说明一下OAuth2AuthorizationCodeAuthenticationToken变量, 这个变量存储了对客户端对授权服务器发送请求的相关信息,以及授权服务器 对客户端服务器请求的相关信息
//首先执行的是该类的authenticate方法再执行authenticationResult方法 //authenticate方法主要是 一些校验工作以及访问/oauth/token端点获取token //authenticationResult方法主要是身份信息,jwk元数据信息,userinfo信息的校验 public Mono<Authentication> authenticate(Authentication authentication) { //.... return this.authenticationResult(authorizationCodeAuthentication, accessTokenResponse); //....... }
OidcAuthorizationCodeReactiveAuthenticationManager#authenticate(处理授权服务器返回的code,并重定向获取token)
首先会判该断客户端在访问授权服务器/oauth/authorize端点时携带的scopes参数是否包含openid参数 该openId是在客户端服务器的yml中进行配置的
(1)判断客户端服务器接收授权服务器的response是否为error,实际上就是code验证是否通过 (2)判断state是否被篡改
访问授权服务器/oauth/token端点端点获取token
OidcAuthorizationCodeReactiveAuthenticationManager#authenticationResult(处理授权服务器返回的token,并进行一系列的有效性校验)
首先会判断授权服务器返回的OAuth2AccessTokenResponse中additionalParameters变量 中参数是否含有id_token(所以在授权服务器需要添加一个额外的参数,[id_token,jwt], 注意id_token一定要传jwt,这个参数主要是用在创建解码器的)
else中的执行顺序
可以直接看到return这, var1000,实际就是执行 this.createOidcToken(clientRegistration, accessTokenResponse) 方法 再执行 validateNonce(authorizationCodeAuthentication, idToken) 方法 再返回一个 new OidcUserRequest对象,执行 ReactiveOAuth2UserService 的 loadUser 方法 ReactiveOAuth2UserService 的默认配置实现类是 OidcReactiveOAuth2UserService类
this.createOidcToken(clientRegistration, accessTokenResponse) 方法源代码
private Mono<OidcIdToken> createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { //创建解码器 ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); //获取jwt String rawIdToken = (String)accessTokenResponse.getAdditionalParameters().get("id_token"); //进行解码 return jwtDecoder.decode(rawIdToken).map((jwt) -> { return new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(), jwt.getClaims()); }); }
jwtDecoder.decode(rawIdToken)源代码
public Mono<Jwt> decode(String token) throws JwtException { //分析jwt //分析jwt的主要内容就是对jwt的header,payload,sigure,进行拆分 JWT jwt = this.parse(token); if (jwt instanceof PlainJWT) { throw new BadJwtException("Unsupported algorithm of " + jwt.getHeader().getAlgorithm()); } else { //解码jwt return this.decode(jwt); } }
this.decode(jwt);源代码
private Mono<Jwt> decode(JWT parsedToken) { try { //jwt转换 return ((Mono)this.jwtProcessor.convert(parsedToken)).map((set) -> { return this.createJwt(parsedToken, set); //检测jwt是否有效 }).map(this::validateJwt).onErrorMap((e) -> { return !(e instanceof IllegalStateException) && !(e instanceof JwtException); }, (e) -> { return new JwtException("An error occurred while attempting to decode the Jwt: ", e); }); } catch (JwtException var3) { throw var3; } catch (RuntimeException var4) { throw new JwtException("An error occurred while attempting to decode the Jwt: " + var4.getMessage(), var4); } }
this.decode(jwt)方法中代码片段: #this.jwtProcessor.convert(parsedToken) 这个方法执行的是前面配置jwt解码器中的方法 默认配置的jwt解码器是NimbusReactiveJwtDecoder jwtProcessor(jwtProcessor的实现类NimbusReactiveJwtDecoder)解码器创建源代码
private Mono<OidcIdToken> createOidcToken(ClientRegistration clientRegistration, OAuth2AccessTokenResponse accessTokenResponse) { //创建解码器 ReactiveJwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration); //..... } | | | | | | | | | | | | | | | v v v public ReactiveJwtDecoder createDecoder(ClientRegistration clientRegistration) { Assert.notNull(clientRegistration, "clientRegistration cannot be null"); return (ReactiveJwtDecoder)this.jwtDecoders.computeIfAbsent(clientRegistration.getRegistrationId(), (key) -> { //....... //创建NimbusReactiveJwtDecoder NimbusReactiveJwtDecoder jwtDecoder = this.buildDecoder(clientRegistration); //...... return jwtDecoder; }); } | | | | | | | | | | | | | | | v v v private NimbusReactiveJwtDecoder buildDecoder(ClientRegistration clientRegistration) { //....... return NimbusReactiveJwtDecoder.withJwkSetUri(clientSecret).jwsAlgorithm((SignatureAlgorithm)jwsAlgorithm).build(); //....... } | | | | | | | | | | | | | | | v v v public NimbusReactiveJwtDecoder build() { return new NimbusReactiveJwtDecoder(this.processor()); } | | | | | | | | | | | | | | | v v v Converter<JWT, Mono<JWTClaimsSet>> processor() { JWKSecurityContextJWKSet jwkSource = new JWKSecurityContextJWKSet(); DefaultJWTProcessor<JWKSecurityContext> jwtProcessor = new DefaultJWTProcessor(); JWSKeySelector<JWKSecurityContext> jwsKeySelector = this.jwsKeySelector(jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); ReactiveRemoteJWKSource source = new ReactiveRemoteJWKSource(this.jwkSetUri); source.setWebClient(this.webClient); Set<JWSAlgorithm> expectedJwsAlgorithms = this.getExpectedJwsAlgorithms(jwsKeySelector); return (jwt) -> { JWKSelector selector = this.createSelector(expectedJwsAlgorithms, jwt.getHeader()); return source.get(selector).onErrorMap((e) -> { return new IllegalStateException("Could not obtain the keys", e); }).map((jwkList) -> { return NimbusReactiveJwtDecoder.createClaimsSet(jwtProcessor, jwt, new JWKSecurityContext(jwkList)); }); }; }
this.jwtProcessor.convert(parsedToken)就是执行了 processor()中return的方法 这里面最需要注意的就是createClaimsSet(jwtProcessor, jwt, new JWKSecurityContext(jwkList))方法,他会把从jwt中解析出来的payload转成ClaimsSet,对于解析出来的exp,nbf,iat的变量必须是Long类型
public static JWTClaimsSet parse(JSONObject json) throws ParseException { Builder builder = new Builder(); Iterator var2 = json.keySet().iterator(); while(var2.hasNext()) { //....... } else if (name.equals("exp")) { builder.expirationTime(new Date(JSONObjectUtils.getLong(json, "exp") * 1000L)); } else if (name.equals("nbf")) { builder.notBeforeTime(new Date(JSONObjectUtils.getLong(json, "nbf") * 1000L)); } else if (name.equals("iat")) { builder.issueTime(new Date(JSONObjectUtils.getLong(json, "iat") * 1000L)); } else if (name.equals("jti")) { builder.jwtID(JSONObjectUtils.getString(json, "jti")); } else { builder.claim(name, json.get(name)); } } return builder.build(); }
this.decode(jwt)方法的代码片段:map(this::validateJwt)
private Jwt validateJwt(Jwt jwt) { //检测jwt是否有效 //默认配置 this.jwtValidator = DelegatingOAuth2TokenValidator //DelegatingOAuth2TokenValidator是有效检验的管理器,里面存储了实际的校验实现类 //默认配置了JwtTimestampValidator校验类 和 OidcIdTokenValidator校验类 //JwtTimestampValidator校验类主要功能检测jwt //OidcIdTokenValidator校验类主要功能效验ClaimsSet中参数是否有误 OAuth2TokenValidatorResult result = this.jwtValidator.validate(jwt); //.... } }
//校验器管理类 //DelegatingOAuth2TokenValidatorr#validate public OAuth2TokenValidatorResult validate(T token) { Collection<OAuth2Error> errors = new ArrayList(); Iterator var3 = this.tokenValidators.iterator(); //遍历默认配置的校验器 while(var3.hasNext()) { OAuth2TokenValidator<T> validator = (OAuth2TokenValidator)var3.next(); errors.addAll(validator.validate(token).getErrors()); } return OAuth2TokenValidatorResult.failure(errors); }
//JwtTimestampValidator校验类 //JwtTimestampValidator#validate public OAuth2TokenValidatorResult validate(Jwt jwt) { Assert.notNull(jwt, "jwt cannot be null"); //获取jwt的过期时间 Instant expiry = jwt.getExpiresAt(); //进行过期时间校验 if (expiry != null && Instant.now(this.clock).minus(this.clockSkew).isAfter(expiry)) { OAuth2Error oAuth2Error = this.createOAuth2Error(String.format("Jwt expired at %s", jwt.getExpiresAt())); return OAuth2TokenValidatorResult.failure(new OAuth2Error[]{oAuth2Error}); } else { Instant notBefore = jwt.getNotBefore(); if (notBefore != null && Instant.now(this.clock).plus(this.clockSkew).isBefore(notBefore)) { OAuth2Error oAuth2Error = this.createOAuth2Error(String.format("Jwt used before %s", jwt.getNotBefore())); return OAuth2TokenValidatorResult.failure(new OAuth2Error[]{oAuth2Error}); } else { return OAuth2TokenValidatorResult.success(); } } }
//OidcIdTokenValidator校验类 //OidcIdTokenValidator#validate //"aud"(Audience):表示了该 ID Token 所面向的观众。它指明了可以使用该 ID Token 的目标受众。通常情况下,这个值是客户端(应用程序)的 client ID。 //"azp"(Authorized Party):表示了授权服务器颁发令牌的受众。这个声明通常用于指示授权服务器颁发令牌的受众。当存在多个受众时,"azp" 可以用于标识实际使用令牌的受众。 //"exp"(Expires At):表示了 ID Token 的过期时间。这个时间通常用于确定 ID Token 是否已经过期,如果当前时间晚于过期时间,则 ID Token 不再有效。 //"iat"(Issued At):表示了 ID Token 的签发时间。这个时间指明了 ID Token 什么时候被颁发的。 public OAuth2TokenValidatorResult validate(Jwt idToken) { Map<String, Object> invalidClaims = validateRequiredClaims(idToken); if (!invalidClaims.isEmpty()) { return OAuth2TokenValidatorResult.failure(new OAuth2Error[]{invalidIdToken(invalidClaims)}); } else { // 判断claim中的aud是否等于yml配置文件的clientId是否相同 if (!idToken.getAudience().contains(this.clientRegistration.getClientId())) { invalidClaims.put("aud", idToken.getAudience()); } //如果 ID Token 的受众(audience)有多个值,并且 authorizedParty 为 null,它会将 "azp" 和 authorizedParty 放入 invalidClaims 中。 String authorizedParty = idToken.getClaimAsString("azp"); if (idToken.getAudience().size() > 1 && authorizedParty == null) { invalidClaims.put("azp", authorizedParty); } //如果 authorizedParty 不为 null,并且它与 clientRegistration.getClientId() 不相等,同样将 "azp" 和 authorizedParty 放入 invalidClaims 中 if (authorizedParty != null && !authorizedParty.equals(this.clientRegistration.getClientId())) { invalidClaims.put("azp", authorizedParty); } //对于过期时间(exp):它检查当前时间减去时钟偏移是否晚于 ID Token 的过期时间,如果是,则将 "exp" 和过期时间放入 invalidClaims 中。 Instant now = Instant.now(this.clock); if (now.minus(this.clockSkew).isAfter(idToken.getExpiresAt())) { invalidClaims.put("exp", idToken.getExpiresAt()); } //对于签发时间(iat):它检查当前时间加上时钟偏移是否早于 ID Token 的签发时间,如果是,则将 "iat" 和签发时间放入 invalidClaims 中。 if (now.plus(this.clockSkew).isBefore(idToken.getIssuedAt())) { invalidClaims.put("iat", idToken.getIssuedAt()); } return !invalidClaims.isEmpty() ? OAuth2TokenValidatorResult.failure(new OAuth2Error[]{invalidIdToken(invalidClaims)}) : OAuth2TokenValidatorResult.success(); } }
回到else中,执行 validateNonce(authorizationCodeAuthentication, idToken) 方法
private static Mono<OidcIdToken> validateNonce(OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthentication, OidcIdToken idToken) { //取出客户端服务器请求授权服务器/oauth/authorize端点时携带nonce的参数 String requestNonce = (String)authorizationCodeAuthentication.getAuthorizationExchange().getAuthorizationRequest().getAttribute("nonce"); if (requestNonce != null) { String nonceHash; OAuth2Error oauth2Error; try { nonceHash = createHash(requestNonce); } catch (NoSuchAlgorithmException var6) { oauth2Error = new OAuth2Error("invalid_nonce"); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } //将授权服务器/oauth/token端点返回的jwt,解析jwt的payload获取出的nonce String nonceHashClaim = idToken.getNonce(); //比较nonce是否一致 if (nonceHashClaim == null || !nonceHashClaim.equals(nonceHash)) { oauth2Error = new OAuth2Error("invalid_nonce"); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } } return Mono.just(idToken); }
再回到else中,执行OidcReactiveOAuth2UserService的loadUser方法
//OidcReactiveOAuth2UserService#loadUser public Mono<OidcUser> loadUser(OidcUserRequest userRequest) throws OAuth2AuthenticationException { Assert.notNull(userRequest, "userRequest cannot be null"); //携带jwt重定向到userInfo端点,获取userInfo信息 return this.getUserInfo(userRequest).map((userInfo) -> { return new OidcUserAuthority(userRequest.getIdToken(), userInfo); }).defaultIfEmpty(new OidcUserAuthority(userRequest.getIdToken(), (OidcUserInfo)null)).map((authority) -> { //设置默认的用户权限 OidcUserInfo userInfo = authority.getUserInfo(); Set<GrantedAuthority> authorities = new HashSet(); authorities.add(authority); OAuth2AccessToken token = userRequest.getAccessToken(); Iterator var5 = token.getScopes().iterator(); while(var5.hasNext()) { String scope = (String)var5.next(); authorities.add(new SimpleGrantedAuthority("SCOPE_" + scope)); } String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); return StringUtils.hasText(userNameAttributeName) ? new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo, userNameAttributeName) : new DefaultOidcUser(authorities, userRequest.getIdToken(), userInfo); }); }
private Mono<OidcUserInfo> getUserInfo(OidcUserRequest userRequest) { //shouldRetrieveUserInfo判断yml中authorization-grant-type参数是否为授权码模式 //如果是则执行DefaultReactiveOAuth2UserService的loadUser方法重定向userInfo端点 return !OidcUserRequestUtils.shouldRetrieveUserInfo(userRequest) ? Mono.empty() : this.oauth2UserService.loadUser(userRequest).map(OAuth2AuthenticatedPrincipal::getAttributes).map((claims) -> { return this.convertClaims(claims, userRequest.getClientRegistration()); }).map(OidcUserInfo::new).doOnNext((userInfo) -> { String subject = userInfo.getSubject(); //授权服务器的userInfo端点解析客户端服务器的jwt的payload中的sub,与客户端服务器接收到授权服务器的jwt解析出来payload中的sub不相同则为 if (subject == null || !subject.equals(userRequest.getIdToken().getSubject())) { OAuth2Error oauth2Error = new OAuth2Error("invalid_user_info_response"); throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString()); } }); }
//DefaultReactiveOAuth2UserService#loadUser public Mono<OAuth2User> loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { return Mono.defer(() -> { Assert.notNull(userRequest, "userRequest cannot be null"); //获取授权服务器的userInfor端点 String userInfoUri = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUri(); //......... //获取yml配置中userNameAttributeName的值 String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName(); //......... //默认请求授权服务器的userInfor端点,jwt放在header上发送请求到授权服务器 requestHeadersSpec = this.webClient.get().uri(userInfoUri, new Object[0]).header("Accept", new String[]{"application/json"}).headers((headers) -> { headers.setBearerAuth(userRequest.getAccessToken().getTokenValue()); }); //......... //创建默认的oauth2用户,注意这里面还有一个判断,判断userInfo端点返回的元数据中,是否包含key为userNameAttributeName的元数据 return new DefaultOAuth2User(authorities, attrs, userNameAttributeName); //......... }
//DefaultOAuth2User#构造函数 public DefaultOAuth2User(Collection<? extends GrantedAuthority> authorities, Map<String, Object> attributes, String nameAttributeKey) { Assert.notEmpty(attributes, "attributes cannot be empty"); Assert.hasText(nameAttributeKey, "nameAttributeKey cannot be empty"); //判断yml中配置的userNameAttributeName在授权服务器的userInfo端点返回的元数据是否存在 if (!attributes.containsKey(nameAttributeKey)) { throw new IllegalArgumentException("Missing attribute '" + nameAttributeKey + "' in attributes"); } else { this.authorities = authorities != null ? Collections.unmodifiableSet(new LinkedHashSet(this.sortAuthorities(authorities))) : Collections.unmodifiableSet(new LinkedHashSet(AuthorityUtils.NO_AUTHORITIES)); this.attributes = Collections.unmodifiableMap(new LinkedHashMap(attributes)); this.nameAttributeKey = nameAttributeKey; } }
搭建GateWay以客户端身份访问授权服务器的必要配置
-
授权服务器需要配置userInfo端点以及JWK端点
-
如何配置userInfo端点
- userInfo必传的了两个参数,一个key为sub,value就是jwt解析出来的sub数据,另外一个key则是客户端服务中yml配置的user-name-attribute参数,Value也一样即可
- userInfo必传的了两个参数,一个key为sub,value就是jwt解析出来的sub数据,另外一个key则是客户端服务中yml配置的user-name-attribute参数,Value也一样即可
-
-
客户端服务器重新定向授权服务器的/oauth/authorize端点的 scopes参数是否有 openid范围
- 处理:网关的yml文件中配置scopes的范围中包含openId
- 处理:网关的yml文件中配置scopes的范围中包含openId
-
授权服务器返回的 jwt的payload必须添加参数 key为 id_token的,value 为 jwt,其
- 处理:更改授权服务器的jwt编码器,添加id_token参数
-
授权服务器生成的jwt的payload中key为 exp,nbf,iat的参数,value 值必须为 long
-
授权服务器生成的jwt的payload中必须存储客户端服务器访问/oauth/authorize端点携带的nonce参数,key值为nonce,value值为携带的nonce参数
-
授权服务器生成的jwt的payload中必须存储key为aud,value为clientId的参数。
-
授权服务器生成的jwt的payload中必须存储key为sub的参数
-
授权服务器生成jwt之后,必须将该jwt以key为token_id,value为jwt值的方式存储在jwt的payload中
//自定义oauth2.0
//以下一条配置都不能缺少
spring:
security:
oauth2:
client:
registration:
custom:
client-id: fooClientIdPassword
client-secret: secret
scope: openid
redirectUri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
provider:
custom:
jwk-set-uri: http://localhost:8080/.well-known/jwks.json
authorization-uri: http://localhost:8080/oauth/authorize
token-uri: http://localhost:8080/oauth/token
user-info-uri: http://localhost:8080/userInfo
user-name-attribute: admin
alue为jwt值的方式存储在jwt的payload中
//自定义oauth2.0
//以下一条配置都不能缺少
spring:
security:
oauth2:
client:
registration:
custom:
client-id: fooClientIdPassword
client-secret: secret
scope: openid
redirectUri: '{baseUrl}/{action}/oauth2/code/{registrationId}'
authorization-grant-type: authorization_code
provider:
custom:
jwk-set-uri: http://localhost:8080/.well-known/jwks.json
authorization-uri: http://localhost:8080/oauth/authorize
token-uri: http://localhost:8080/oauth/token
user-info-uri: http://localhost:8080/userInfo
user-name-attribute: admin
以上的每一条都是从oauth-client的源码中挖出来的默认必要配置条件,缺失一条都会导致默认配置失败。