拥抱Spring全新OAuth解决方案(三)–登录认证

前言

token认证源码解析
总结



前言

上篇主要分析了Spring-Security-Oauth2-authorization-server 的filterChain及filter原理,这对于我们接下来分析token的认证和鉴权流程会很有帮助。

1.token认证

1.1 认证入口

某安某大型国企项目Oauth2实践(二)中介绍到老版的spring Security会暴露一个tokenEndpoint端口/oauth/token

拥抱Spring全新OAuth解决方案(三)--登录认证

新版的Spring-Security-Oauth2-authorization-server取而代之换成了配置类

拥抱Spring全新OAuth解决方案(三)--登录认证

用户可以自定义登录端口,默认是oauth2/token,同时新版的security不是直接通过controller来接收token请求,而是通过filterChain来依次处理。上篇有介绍认证的filterChain如下

拥抱Spring全新OAuth解决方案(三)--登录认证

对应的requestMatcher

拥抱Spring全新OAuth解决方案(三)--登录认证

其中可以看到OAuth2TokenEndpointFilter,显而易见这个就是取代了以前的TokenEndpoint。同时上图中比较重要的filter有:

OAuth2ClientAuthenticationFilter

OAuth2TokenEndpointFilter

接下来将重点分析这两个filter。

1.2 OAuth2ClientAuthenticationFilter

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        //请求匹配    
        if (!this.requestMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
           //组装认证对象
            Authentication authenticationRequest = this.authenticationConverter.convert(request);
            if (authenticationRequest instanceof AbstractAuthenticationToken) {
                ((AbstractAuthenticationToken) authenticationRequest).setDetails(
                        this.authenticationDetailsSource.buildDetails(request));
            }
            if (authenticationRequest != null) {
                validateClientIdentifier(authenticationRequest);
                //验证client
                Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
                //
验证成功后置处理
                this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
            }
            filterChain.doFilter(request, response);

        } catch (OAuth2AuthenticationException ex) {
            this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
        }
    }

❶this.requestMatcher.matches,基本上每个filterChain和filter都有自己的requestMatcher

拥抱Spring全新OAuth解决方案(三)--登录认证

❷authenticationConverter.convert

然后这里同样也用到了组合模式,通过DelegatingAuthenticationConverter来委托如下的converter

拥抱Spring全新OAuth解决方案(三)--登录认证

然后这里我们使用密码模式,客户端加密用的base64,所以converter就是ClientSecretBasicAuthenticationConverter
    public Authentication convert(HttpServletRequest request) {
        String header = request.getHeader(HttpHeaders.AUTHORIZATION);
        if (header == null) {
            return null;
        }
        String[] parts = header.split("\s");
        if (!parts[0].equalsIgnoreCase("Basic")) {
            return null;
        }
        byte[] decodedCredentials;
        try {
            decodedCredentials = Base64.getDecoder().decode(
                    parts[1].getBytes(StandardCharsets.UTF_8));
        } catch (IllegalArgumentException ex) {
            throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
        }

        String credentialsString = new String(decodedCredentials, StandardCharsets.UTF_8);
        String[] credentials = credentialsString.split(":", 2);
        
        String clientID;
        String clientSecret;
        try {
            clientID = URLDecoder.decode(credentials[0], StandardCharsets.UTF_8.name());
            clientSecret = URLDecoder.decode(credentials[1], StandardCharsets.UTF_8.name());
        } catch (Exception ex) {
            throw new OAuth2AuthenticationException(new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST), ex);
        }

        return new OAuth2ClientAuthenticationToken(clientID, ClientAuthenticationMethod.CLIENT_SECRET_BASIC, clientSecret,
                OAuth2EndpointUtils.getParametersIfMatchesAuthorizationCodeGrantRequest(request));

    }
这步主要就是从请求中获取header头为Authorization的header,然后通过base64解密出clientIDclientSecret,最后组装成client认证对象OAuth2ClientAuthenticationToken

❸authenticationManager.authenticate验证client

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        AuthenticationException parentException = null;
        Authentication result = null;
        Authentication parentResult = null;
        int currentPosition = 0;
        int size = this.providers.size();
        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }
            try {
                result = provider.authenticate(authentication);
            } 
        }
    return result;
}
这里的ProviderManager是spring Security核心包里的,和老版本的一样。
同样它也是组合模式,代理了很多AuthenticationProvider

拥抱Spring全新OAuth解决方案(三)--登录认证

跟requestMatch一样,通过supports方法找到匹配的认证处理器。
其中ClientSecretAuthenticationProvider即匹配上了第二步生成的认证对象OAuth2ClientAuthenticationToken
    @Override
    public boolean supports(Class<?> authentication) {
        return OAuth2ClientAuthenticationToken.class.isAssignableFrom(authentication);
    }

接下来就是最核心的认证逻辑了。

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        OAuth2ClientAuthenticationToken clientAuthentication =
                (OAuth2ClientAuthenticationToken) authentication;

        String clientId = clientAuthentication.getPrincipal().toString();
        //查询登记的client
        RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId);
        
        String clientSecret = clientAuthentication.getCredentials().toString();
        //比对clientSecret
        if (!this.passwordEncoder.matches(clientSecret, registeredClient.getClientSecret())) {
            throwInvalidClient(OAuth2ParameterNames.CLIENT_SECRET);
        }
        //校验client的有效期
        if (registeredClient.getClientSecretExpiresAt() != null &&
                Instant.now().isAfter(registeredClient.getClientSecretExpiresAt())) {
            throwInvalidClient("client_secret_expires_at");
        }
        //返回认证后的client认证对象
        return new OAuth2ClientAuthenticationToken(registeredClient,
                clientAuthentication.getClientAuthenticationMethod(), clientAuthentication.getCredentials())
;
    }

其中❶registeredClientRepository.findByClientId,查询数据库中登记的client信息

拥抱Spring全新OAuth解决方案(三)--登录认证

到这里OAuth2ClientAuthenticationFilter的职责基本上算完成了,但是已经认证的client信息如何保存呢?

❹this.authenticationSuccessHandler.onAuthenticationSuccess

成功后置处理

private void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) {

    SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
    securityContext.setAuthentication(authentication);
    SecurityContextHolder.setContext(securityContext);

    if (this.logger.isDebugEnabled()) {
        this.logger.debug(LogMessage.format("Set SecurityContextHolder authentication to %s",
                authentication.getClass().getSimpleName()));
    }
    }

将认证后的client信息放入了上下文中。

1.2 OAuth2TokenEndpointFilter

接下来开始正式处理请求。

public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {

    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        //请求匹配        
        if (!this.tokenEndpointMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }

        try {
            String[] grantTypes = request.getParameterValues(OAuth2ParameterNames.GRANT_TYPE);
            //组装认证对象
            Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);
            //认证处理
            OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
                    (OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);

            //认证成功后置处理        
            this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, accessTokenAuthentication);
        } catch (OAuth2AuthenticationException ex) {
        
            this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
        }
    }

大体是和上面讲的OAuth2ClientAuthenticationFilter流程是一样的,不过它没有继续往后请求了,少了filterChain.doFilter,所以请求到这里就结束了。

❶requestMatcher.matches

拥抱Spring全新OAuth解决方案(三)--登录认证

❷authenticationConverter.convert,组装认证对象

拥抱Spring全新OAuth解决方案(三)--登录认证

我们在第一篇有讲过Spring-Security-Oauth2-authorization-server是不支持密码模式的,如果我们要自己实现密码模式,应该如何扩展呢?接下来以开源项目Pig来的扩展作讲解。
首先从上面的截图开源看到两个自定义的converter,一个是password,一个是sms.那么在哪里注入OAuth2TokenEndpointFilter呢?

回头看第二篇介绍认证认证服务的FilterChain

拥抱Spring全新OAuth解决方案(三)--登录认证

然后accessTokenRequestConverter

拥抱Spring全新OAuth解决方案(三)--登录认证

这样基于密码和短信的认证转换器就接入进来了。基于密码的认证转换器:OAuth2ResourceOwnerPasswordAuthenticationConverter

1.2.1OAuth2ResourceOwnerPasswordAuthenticationConverter(自定义转换器)

public Authentication convert(HttpServletRequest request) {

        // grant_type (REQUIRED)
        String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
        //校验类型(密码,手机号等)
        if (!support(grantType)) {
            return null;
        }

        MultiValueMap<String, String> parameters = OAuth2EndpointUtils.getParameters(request);

        // 校验个性化参数
        checkParams(request);

        // 获取当前已经认证的客户端信息
        Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
        if (clientPrincipal == null) {
            OAuth2EndpointUtils.throwError(OAuth2ErrorCodes.INVALID_REQUEST, OAuth2ErrorCodes.INVALID_CLIENT,
                    OAuth2EndpointUtils.ACCESS_TOKEN_REQUEST_ERROR_URI);
        }

        // 扩展信息
        Map<String, Object> additionalParameters = parameters.entrySet().stream()
                .filter(e -> !e.getKey().equals(OAuth2ParameterNames.GRANT_TYPE)
                        && !e.getKey().equals(OAuth2ParameterNames.SCOPE))
                .collect(Collectors.toMap(Map.Entry::getKey, e -> e.getValue().get(0)));

        //创建token
        return buildToken(clientPrincipal, requestedScopes, additionalParameters);

    }

❶support(grantType) 校验类型,如密码:password,手机号:app

拥抱Spring全新OAuth解决方案(三)--登录认证

❷checkParams校验个性化参数,如密码模式则校验用户名和密码不能为空

❸SecurityContextHolder.getContext().getAuthentication(),获取当前已经认证的client信息。在讲OAuth2ClientAuthenticationFilter时,最后一步则是将认证的client信息放入上下文。

❹获取扩展信息,如账号密码等。

❺buildToken,创建认证对象。

如密码模式则是(自定义)
    @Override
    public OAuth2ResourceOwnerPasswordAuthenticationToken buildToken(Authentication clientPrincipal,
            Set requestedScopes, Map additionalParameters) {
        return new OAuth2ResourceOwnerPasswordAuthenticationToken(AuthorizationGrantType.PASSWORD, clientPrincipal,
                requestedScopes, additionalParameters);
    }

到这里认证对象组装完毕,接下来就是认证处理,回到filter那里。第❸步

authenticationManager.authenticate

同样还是被ProviderManager代理,

拥抱Spring全新OAuth解决方案(三)--登录认证

这里也扩展了如下三个provider:

PigDaoAuthenticationProvider(密码provider)

OAuth2ResourceOwnerPasswordAuthenticationProvider(密码provider)

OAuth2ResourceOwnerSmsAuthenticationProvider(短信provider)

首先看自定义的PigDaoAuthenticationProvider

拥抱Spring全新OAuth解决方案(三)--登录认证

support的认证对象是spring security核心包提供的UsernamePasswordAuthenticationToken,暂时还不符合

接着自定的OAuth2ResourceOwnerPasswordAuthenticationProvider就是匹配自己定义的认证对象

拥抱Spring全新OAuth解决方案(三)--登录认证

这里大家可能会有个疑问,为什么要自定义两个密码认证provider,不直接定义一个,后面再解答。

1.2.2 OAuth2ResourceOwnerPasswordAuthenticationProvider(自定义认证器)

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        T resouceOwnerBaseAuthentication = (T) authentication;

        OAuth2ClientAuthenticationToken clientPrincipal = getAuthenticatedClientElseThrowInvalidClient(
                resouceOwnerBaseAuthentication);

        RegisteredClient registeredClient = clientPrincipal.getRegisteredClient();
        checkClient(registeredClient);

        Map<String, Object> reqParameters = resouceOwnerBaseAuthentication.getAdditionalParameters();
        try {

            //构建UsernamePasswordAuthenticationToken
            UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = buildToken(reqParameters);

            LOGGER.debug("got usernamePasswordAuthenticationToken=" + usernamePasswordAuthenticationToken);

            //认证usernamePasswordAuthenticationToken对象
            Authentication usernamePasswordAuthentication = authenticationManager
                    .authenticate(usernamePasswordAuthenticationToken);

            // @formatter:off
            DefaultOAuth2TokenContext.Builder tokenContextBuilder = DefaultOAuth2TokenContext.builder()
                    .registeredClient(registeredClient)
                    .principal(usernamePasswordAuthentication)
                    .authorizationServerContext(AuthorizationServerContextHolder.getContext())
                    .authorizedScopes(authorizedScopes)
                    .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                    .authorizationGrant(resouceOwnerBaseAuthentication);
            // @formatter:on

            OAuth2Authorization.Builder authorizationBuilder = OAuth2Authorization
                    .withRegisteredClient(registeredClient).principalName(usernamePasswordAuthentication.getName())
                    .authorizationGrantType(AuthorizationGrantType.PASSWORD)
                    // 0.4.0 新增的方法
                    .authorizedScopes(authorizedScopes);

            // ----- Access token -----
            OAuth2TokenContext tokenContext = tokenContextBuilder.tokenType(OAuth2TokenType.ACCESS_TOKEN).build();
            //生成accessToken
            OAuth2Token generatedAccessToken = this.tokenGenerator.generate(tokenContext);
            if (generatedAccessToken == null) {
                OAuth2Error error = new OAuth2Error(OAuth2ErrorCodes.SERVER_ERROR,
                        "The token generator failed to generate the access token.", ERROR_URI);
                throw new OAuth2AuthenticationException(error);
            }
            OAuth2AccessToken accessToken = new OAuth2AccessToken(OAuth2AccessToken.TokenType.BEARER,
                    generatedAccessToken.getTokenValue(), generatedAccessToken.getIssuedAt(),
                    generatedAccessToken.getExpiresAt(), tokenContext.getAuthorizedScopes());
            if (generatedAccessToken instanceof ClaimAccessor) {
                authorizationBuilder.id(accessToken.getTokenValue())
                        .token(accessToken,
                                (metadata) -> metadata.put(OAuth2Authorization.Token.CLAIMS_METADATA_NAME,
                                        ((ClaimAccessor) generatedAccessToken).getClaims()))
                        // 0.4.0 新增的方法
                        .authorizedScopes(authorizedScopes)
                        .attribute(Principal.class.getName(), usernamePasswordAuthentication);
            }
            else {
                authorizationBuilder.id(accessToken.getTokenValue()).accessToken(accessToken);
            }
//----- Refresh token -----
            ...
            //持久化token
this.authorizationService.save(authorization);
        }
        catch (Exception ex) {
            throw oAuth2AuthenticationException(authentication, (AuthenticationException) ex);
        }

    }

❶buildToken构建UsernamePasswordAuthenticationToken,这个就是SpringSecurity提供的默认密码认证对象,新版的认证服务虽然去掉了旧的密码模式provider,但是还是保留了密码认证对象。

密码登录:

拥抱Spring全新OAuth解决方案(三)--登录认证

短信登录:

拥抱Spring全新OAuth解决方案(三)--登录认证

❷authenticationManager.authenticate认证usernamePasswordAuthenticationToken对象,这里就该我们上面提到的自定义PigDaoAuthenticationProvider上场了。这个实现和旧版的基本上一致,需要实现认证信息的获取方式(如数据库)

拥抱Spring全新OAuth解决方案(三)--登录认证

以及认证信息的校验。(如密码,手机验证码的匹配)

拥抱Spring全新OAuth解决方案(三)--登录认证

❸tokenGenerator.generate 生成accessToken

旧版的SpringSecurity在验证完认证对象后,会通过tokenservice来生成token
public abstract class AbstractTokenGranter implements TokenGranter {
    
    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

新版版认证服务就删掉了tokenService,提供了OAuth2TokenGenerator,默认的有

拥抱Spring全新OAuth解决方案(三)--登录认证

Pig项目就自定以了TokenGenerator:CustomeOAuth2AccessTokenGenerator

public OAuth2AccessToken generate(OAuth2TokenContext context) {
        
    String issuer = null;
    if (context.getAuthorizationServerContext() != null) {
        issuer = context.getAuthorizationServerContext().getIssuer();
    }
    //从上下文获取已经认证的client信息
    RegisteredClient registeredClient = context.getRegisteredClient();

    Instant issuedAt = Instant.now();
    //过期时间配置在登记的client配置中
    Instant expiresAt = issuedAt.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());

    // @formatter:off
    OAuth2TokenClaimsSet.Builder claimsBuilder = OAuth2TokenClaimsSet.builder();
    if (StringUtils.hasText(issuer)) {
        claimsBuilder.issuer(issuer);
    }
    claimsBuilder
            .subject(context.getPrincipal().getName())
            .audience(Collections.singletonList(registeredClient.getClientId()))
            .issuedAt(issuedAt)
            .expiresAt(expiresAt)
            .notBefore(issuedAt)
            .id(UUID.randomUUID().toString());
    if (!CollectionUtils.isEmpty(context.getAuthorizedScopes())) {
        claimsBuilder.claim(OAuth2ParameterNames.SCOPE, context.getAuthorizedScopes());
    }
    // @formatter:on

    if (this.accessTokenCustomizer != null) {
        // @formatter:off
        OAuth2TokenClaimsContext.Builder accessTokenContextBuilder = OAuth2TokenClaimsContext.with(claimsBuilder)
                .registeredClient(context.getRegisteredClient())
                .principal(context.getPrincipal())
                .authorizationServerContext(context.getAuthorizationServerContext())
                .authorizedScopes(context.getAuthorizedScopes())
                .tokenType(context.getTokenType())
                .authorizationGrantType(context.getAuthorizationGrantType());
        if (context.getAuthorization() != null) {
            accessTokenContextBuilder.authorization(context.getAuthorization());
        }
        if (context.getAuthorizationGrant() != null) {
            accessTokenContextBuilder.authorizationGrant(context.getAuthorizationGrant());
        }
        // @formatter:on

        OAuth2TokenClaimsContext accessTokenContext = accessTokenContextBuilder.build();
        this.accessTokenCustomizer.customize(accessTokenContext);
    }

    OAuth2TokenClaimsSet accessTokenClaimsSet = claimsBuilder.build();
    //生成token对象
    return new CustomeOAuth2AccessTokenGenerator.OAuth2AccessTokenClaims(OAuth2AccessToken.TokenType.BEARER,
            UUID.randomUUID().toString(), accessTokenClaimsSet.getIssuedAt(), accessTokenClaimsSet.getExpiresAt(),
            context.getAuthorizedScopes(), accessTokenClaimsSet.getClaims());

}

这里生成的token的value是一个随机的uuid,其他认证信息也包含在token对象中,token的有效期是通过登记的client信息配置的。

❹authorizationService.save(authorization)持久化token.

在老版本中,我们通过扩展tokenGranter来扩展token的状态维护。新版本没有了tokenGranter,提供了一个更加友好的扩展类OAuth2AuthorizationService

这里Pig项目也是实现了Redis的token持久化方式。

    @Override
    public void save(OAuth2Authorization authorization) {
        Assert.notNull(authorization, "authorization cannot be null");

        if (isAccessToken(authorization)) {
            OAuth2AccessToken accessToken = authorization.getAccessToken().getToken();
            long between = ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt());
            redisTemplate.setValueSerializer(RedisSerializer.Java());
            redisTemplate.opsForValue().set(buildKey(OAuth2ParameterNames.ACCESS_TOKEN, accessToken.getTokenValue()),
                    authorization, between, TimeUnit.SECONDS);
        }
    }

redis中token的有效期就是前面从登记的client信息中获取的。

到这里整个OAuth2TokenEndpointFilter差不多就结束了,但是这里都是filter,如何将token信息返回回去了?

拥抱Spring全新OAuth解决方案(三)--登录认证

我们看最后一步❹authenticationSuccessHandler.onAuthenticationSuccess

private void sendAccessTokenResponse(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException {

        OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
                (OAuth2AccessTokenAuthenticationToken) authentication;

        OAuth2AccessToken accessToken = accessTokenAuthentication.getAccessToken();
        OAuth2RefreshToken refreshToken = accessTokenAuthentication.getRefreshToken();
        Map<String, Object> additionalParameters = accessTokenAuthentication.getAdditionalParameters();

        OAuth2AccessTokenResponse.Builder builder =
                OAuth2AccessTokenResponse.withToken(accessToken.getTokenValue())
                        .tokenType(accessToken.getTokenType())
                        .scopes(accessToken.getScopes());
        if (accessToken.getIssuedAt() != null && accessToken.getExpiresAt() != null) {
            builder.expiresIn(ChronoUnit.SECONDS.between(accessToken.getIssuedAt(), accessToken.getExpiresAt()));
        }
        if (refreshToken != null) {
            builder.refreshToken(refreshToken.getTokenValue());
        }
        if (!CollectionUtils.isEmpty(additionalParameters)) {
            builder.additionalParameters(additionalParameters);
        }
        OAuth2AccessTokenResponse accessTokenResponse = builder.build();
        ServletServerHttpResponse httpResponse = new ServletServerHttpResponse(response);
        this.accessTokenHttpResponseConverter.write(accessTokenResponse, null, httpResponse);
    }
前面都是组装最终的token数据,最后两步就是通过repsonse流将数据写出去。

最终的响应如下:

拥抱Spring全新OAuth解决方案(三)--登录认证

2. 总结

整体上看,虽然新版的认证服务抹掉了密码认证功能,但是还是保留了核心的密码认证对象,留了很多扩展点。
       然后整个认证通过filter传递下去,每个filter职责都大致一样(如下),这样对于源码也更有可读性。
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        //请求匹配    
        if (!this.requestMatcher.matches(request)) {
            filterChain.doFilter(request, response);
            return;
        }
        try {
          //组装认证对象
          Authentication authenticationRequest = this.authenticationConverter.convert(request);
           //验证认证对象
           Authentication authenticationResult = this.authenticationManager.authenticate(authenticationRequest);
            //
验证成功后置处理
            this.authenticationSuccessHandler.onAuthenticationSuccess(request, response, authenticationResult);
            }
            filterChain.doFilter(request, response);

        } catch (OAuth2AuthenticationException ex) {
            this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
        }
    }


客户端认证过程:

拥抱Spring全新OAuth解决方案(三)--登录认证

token生成过程

拥抱Spring全新OAuth解决方案(三)--登录认证(ps,公众号推文图片分片率有限,大家如果想要原文件,可以私信我,我看见消息会回复的哈)


拥抱Spring全新OAuth解决方案(三)--登录认证
关注我的你,是最香哒!


原文始发于微信公众号(小李的源码图):拥抱Spring全新OAuth解决方案(三)–登录认证

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/145240.html

(0)
青莲明月的头像青莲明月

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!