OAuth2-client配置oauth2Login源码解读

背景

搭建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

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也一样即可
        在这里插入图片描述
  • 客户端服务器重新定向授权服务器的/oauth/authorize端点的 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的源码中挖出来的默认必要配置条件,缺失一条都会导致默认配置失败。

  • 22
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
在Spring Boot中,使用`spring-boot-starter-oauth2-client`配置`SecurityFilterChain`可以实现OAuth2客户端的认证和授权功能。下面是配置`SecurityFilterChain`的步骤: 1. 首先,在`application.properties`或`application.yml`文件中配置OAuth2客户端的相关属性,例如: ``` spring.security.oauth2.client.registration.<client-id>.client-id=<client-id> spring.security.oauth2.client.registration.<client-id>.client-secret=<client-secret> spring.security.oauth2.client.registration.<client-id>.redirect-uri=<redirect-uri> spring.security.oauth2.client.registration.<client-id>.authorization-grant-type=authorization_code spring.security.oauth2.client.registration.<client-id>.scope=<scope> spring.security.oauth2.client.provider.<provider-id>.authorization-uri=<authorization-uri> spring.security.oauth2.client.provider.<provider-id>.token-uri=<token-uri> spring.security.oauth2.client.provider.<provider-id>.user-info-uri=<user-info-uri> ``` 2. 创建一个`@Configuration`类,并使用`@EnableWebSecurity`注解启用Web安全功能。 3. 在该配置类中,创建一个实现`SecurityFilterChain`接口的`@Bean`方法,并使用`HttpSecurity`对象进行配置,例如: ```java @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeRequests(authorizeRequests -> authorizeRequests .antMatchers("/login").permitAll() .anyRequest().authenticated() ) .oauth2Login(); return http.build(); } } ``` 4. 在上述配置中,使用`authorizeRequests`方法配置请求的授权规则,例如使用`antMatchers`方法指定某些URL的访问权限,使用`anyRequest`方法指定其他请求的访问权限。 5. 使用`oauth2Login`方法启用OAuth2登录功能,该方法会自动配置OAuth2客户端的登录流程。 6. 最后,将上述配置类添加到Spring Boot应用程序的启动类上,例如: ```java @SpringBootApplication public class MyApp { public static void main(String[] args) { SpringApplication.run(MyApp.class, args); } } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值