谈谈Oauth2授权框架,token派发流程以及源码解析

一、本文解决的问题

在使用Oauth2,登陆授权框架使用的是Oauth2+Sping Security+jwt遇到了很多莫名奇妙的问题。因为套框架底层封装了很多东西,所以在操作的时候很多操作框架帮我们做了。我也莫名奇妙的根着框架做,最后连个认证流程都没搞清楚。

所以在这里主要为了总结下之前的疑惑做个小笔记。下面先把我遇到的问题列出来,然后再慢慢说解决,所有东西都是自己在项目打断点和自己的理解总结出来的,如果有偏差希望拍砖指出。

1. uri访问路径/oauth/token到底在哪里?
2. Oauth2的认证流程是怎么走的?
3. 授权框架client提交的client_id和client_secret怎样和数据库的client_id和client_secret对比?
4. 授权框架client提交的username和password怎样和数据库的username和password对比?
5. 怎么在Oauth2框架使用jwt作为认证协议?

框架各版本之间可能和我使用的版本有差异,所以列出出本文使用的框架版本

  • 本文Oauth2框架版本:spring-security-oauth2-2.0.14.RELEASE.jar
  • 本文Security框架版本:spring-security-web-4.2.3.RELEASE.jar

认识名词:
spring-Oauth2是一个Sping 提供的授权框架
spring-security是Spring提供的一个认证框架
jwt是一种认证协议

二、访问/oauth/token的入口

/oauth/token是Oauth2授权框架提供的一个站点,之前我一直以为是自定义的。后来发现原来这是授权框架提供的站点,使用Oauth2进行授权必然会对这个站点进行访问。

在框架下找到下面这个类(TokenEndpoint)

package org.springframework.security.oauth2.provider.endpoint;
//此次省略部分包....
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
    private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
    private Set<HttpMethod> allowedRequestMethods;

    public TokenEndpoint() {
        this.allowedRequestMethods = new HashSet(Arrays.asList(HttpMethod.POST));
    }
    
    //get请求入口
    @RequestMapping(value = {"/oauth/token"},method = {RequestMethod.GET})
    public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        if (!this.allowedRequestMethods.contains(HttpMethod.GET)) {
            throw new HttpRequestMethodNotSupportedException("GET");
        } else {
            return this.postAccessToken(principal, parameters);
        }
    }
    
    //post请求入口
    @RequestMapping(value = {"/oauth/token"},method = {RequestMethod.POST})
    public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        //判断请求是否经过了身份验证,如果没有需要写一个身份验证的过滤器进行身份验证
        if (!(principal instanceof Authentication)) {
            throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
        } else {
            //从principal中获取到clientId
            String clientId = this.getClientId(principal);
            //从数据库中加载ClientDetails
            ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
            //将ClientDetails参数和parameters封装到TokenRequest
            TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
            //鉴定从数据库查询出来的ClientDetails是否匹配表单提交过来的clientId
            if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
                throw new InvalidClientException("Given client ID does not match authenticated client");
            } else {
                if (authenticatedClient != null) {
                    //验证Scope(授权范围)是否有效
                    this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
                }
                
                //判断GrantType
                if (!StringUtils.hasText(tokenRequest.getGrantType())) {
                    throw new InvalidRequestException("Missing grant type");
                } else if (tokenRequest.getGrantType().equals("implicit")) {
                    throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
                } else {
                    if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
                        this.logger.debug("Clearing scope of incoming token request");
                        tokenRequest.setScope(Collections.emptySet());
                    }

                    if (this.isRefreshTokenRequest(parameters)) {
                        tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
                    }
                    //产生token,并且返回
                    OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
                    if (token == null) {
                        throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
                    } else {
                        return this.getResponse(token);
                    }
                }
            }
        }
    }
    
    //从Principal种获取到ClientId
    protected String getClientId(Principal principal) {
        //注意Authentication,是继承了Principal的
        Authentication client = (Authentication)principal;
        //判断第三方服务是否已经经过身份验证
        if (!client.isAuthenticated()) {
            throw new InsufficientAuthenticationException("The client is not authenticated.");
        } else {
            String clientId = client.getName();
            if (client instanceof OAuth2Authentication) {
                clientId = ((OAuth2Authentication)client).getOAuth2Request().getClientId();
            }

            return clientId;
        }
    }

    @ExceptionHandler({HttpRequestMethodNotSupportedException.class})
    public ResponseEntity<OAuth2Exception> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        return this.getExceptionTranslator().translate(e);
    }

    @ExceptionHandler({Exception.class})
    public ResponseEntity<OAuth2Exception> handleException(Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        return this.getExceptionTranslator().translate(e);
    }

    @ExceptionHandler({ClientRegistrationException.class})
    public ResponseEntity<OAuth2Exception> handleClientRegistrationException(Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        return this.getExceptionTranslator().translate(new BadClientCredentialsException());
    }

    @ExceptionHandler({OAuth2Exception.class})
    public ResponseEntity<OAuth2Exception> handleException(OAuth2Exception e) throws Exception {
        this.logger.info("Handling error: " + e.getClass().getSimpleName() + ", " + e.getMessage());
        return this.getExceptionTranslator().translate(e);
    }

    private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control", "no-store");
        headers.set("Pragma", "no-cache");
        return new ResponseEntity(accessToken, headers, HttpStatus.OK);
    }

    private boolean isRefreshTokenRequest(Map<String, String> parameters) {
        return "refresh_token".equals(parameters.get("grant_type")) && parameters.get("refresh_token") != null;
    }

    private boolean isAuthCodeRequest(Map<String, String> parameters) {
        return "authorization_code".equals(parameters.get("grant_type")) && parameters.get("code") != null;
    }

    public void setOAuth2RequestValidator(OAuth2RequestValidator oAuth2RequestValidator) {
        this.oAuth2RequestValidator = oAuth2RequestValidator;
    }

    public void setAllowedRequestMethods(Set<HttpMethod> allowedRequestMethods) {
        this.allowedRequestMethods = allowedRequestMethods;
    }
}

上面就是(TokenEndpoint)就是我们看到客户请求的/oauth/token的入口,但是在经过这个入口的时候,我们必须还会经过spring-security的一些过滤器和拦截器,同时还会经过我们自定义设置的安全策略设置等等。

所以下面我们在来看看这个认证流程和几个比较重要的拦截器。

三、Oauth2的认证流程

我们上面已经知道了/oauth/token的的入口,此时我们要就了解client第三方应用的请求连接怎么写了。
认证会用到的请求方式如下:

http://localhost/oauth/token?client_id=appClient&client_secret=123456&grant_type=password&username=admin&&password=123456

请求所需参数:
client_id(第三方应用id,可以理解为服务应用客户端名称)
client_secret(第三方应用密串,可以理解为服务应用客户端密码)
grant_type(oauth2授权模式,上面请求用的是密码模式,其他模式不细说)
username(申请授权的用户名)
password(申请授权的用户密码)。post请求参数一样

ClientCredentialsTokenEndpointFilter,第三方客户端申请token凭证过滤器。用户发起获取tokne请求,第三方服务客户端会走通一验证入口/oauth/token,同时携带参数,然后就会进入第一个过滤器ClientCredentialsTokenEndpointFilter过滤器,此过滤器主要是进行第三方客户端的申请token信息尝试验证。

package org.springframework.security.oauth2.provider.client;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.exceptions.BadClientCredentialsException;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.HttpRequestMethodNotSupportedException;

public class ClientCredentialsTokenEndpointFilter extends AbstractAuthenticationProcessingFilter {
    private AuthenticationEntryPoint authenticationEntryPoint;
    private boolean allowOnlyPost;
    
    //构造方法注入默认的访问uri
    public ClientCredentialsTokenEndpointFilter() {
        this("/oauth/token");
    }

    public ClientCredentialsTokenEndpointFilter(String path) {
        super(path);
        this.authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
        this.allowOnlyPost = false;
        //判断客户端请求访问的路径是否与构造器定义的路径匹配
        this.setRequiresAuthenticationRequestMatcher(new ClientCredentialsTokenEndpointFilter.ClientCredentialsRequestMatcher(path));
        ((OAuth2AuthenticationEntryPoint)this.authenticationEntryPoint).setTypeName("Form");
    }

    public void setAllowOnlyPost(boolean allowOnlyPost) {
        this.allowOnlyPost = allowOnlyPost;
    }

    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    public void afterPropertiesSet() {
        super.afterPropertiesSet();
        this.setAuthenticationFailureHandler(new AuthenticationFailureHandler() {
            public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                if (exception instanceof BadCredentialsException) {
                    exception = new BadCredentialsException(((AuthenticationException)exception).getMessage(), new BadClientCredentialsException());
                }

                ClientCredentialsTokenEndpointFilter.this.authenticationEntryPoint.commence(request, response, (AuthenticationException)exception);
            }
        });
        this.setAuthenticationSuccessHandler(new AuthenticationSuccessHandler() {
            public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            }
        });
    }

    //尝试身份证,获取Authentication
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (this.allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
            throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[]{"POST"});
        } else {
            //获取客户端信息
            String clientId = request.getParameter("client_id");
            String clientSecret = request.getParameter("client_secret");
            Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
            //判断是否已经通过身份验证,如果是已经通过身份证验证之间返回。SecurityContextHolder.getContext().setAuthentication()方法在AbstractAuthenticationProcessingFilter。this.successfulAuthentication(request, response, chain, authResult)
            if (authentication != null && authentication.isAuthenticated()) {
                return authentication;
            } else if (clientId == null) {
                throw new BadCredentialsException("No client credentials presented");
            } else {
                //进行身份证验证
                if (clientSecret == null) {
                    clientSecret = "";
                }

                clientId = clientId.trim();
                //参数封装
                UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId, clientSecret);
                //调用ProviderManager类的authenticate(authRequest)方法
                return this.getAuthenticationManager().authenticate(authRequest);
            }
        }
    }

    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    }
    //判断客户端请求访问的路径是否与构造器定义的路径匹配
    protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
        private String path;

        public ClientCredentialsRequestMatcher(String path) {
            this.path = path;
        }

        public boolean matches(HttpServletRequest request) {
            String uri = request.getRequestURI();
            int pathParamIndex = uri.indexOf(59);
            if (pathParamIndex > 0) {
                uri = uri.substring(0, pathParamIndex);
            }

            String clientId = request.getParameter("client_id");
            if (clientId == null) {
                return false;
            } else {
                return "".equals(request.getContextPath()) ? uri.endsWith(this.path) : uri.endsWith(request.getContextPath() + this.path);
            }
        }
    }
}

AbstractAuthenticationProcessingFilter过滤器

package org.springframework.security.web.authentication;

import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.event.InteractiveAuthenticationSuccessEvent;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.session.NullAuthenticatedSessionStrategy;
import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.Assert;
import org.springframework.web.filter.GenericFilterBean;

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {
    protected ApplicationEventPublisher eventPublisher;
    protected AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationManager authenticationManager;
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private RememberMeServices rememberMeServices = new NullRememberMeServices();
    private RequestMatcher requiresAuthenticationRequestMatcher;
    private boolean continueChainBeforeSuccessfulAuthentication = false;
    private SessionAuthenticationStrategy sessionStrategy = new NullAuthenticatedSessionStrategy();
    private boolean allowSessionCreation = true;
    private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
    private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();

    protected AbstractAuthenticationProcessingFilter(String defaultFilterProcessesUrl) {
        this.setFilterProcessesUrl(defaultFilterProcessesUrl);
    }

    protected AbstractAuthenticationProcessingFilter(RequestMatcher requiresAuthenticationRequestMatcher) {
        Assert.notNull(requiresAuthenticationRequestMatcher, "requiresAuthenticationRequestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requiresAuthenticationRequestMatcher;
    }

    public void afterPropertiesSet() {
        Assert.notNull(this.authenticationManager, "authenticationManager must be specified");
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        //判断是否需要进行拦截的path,如果不需要拦截直接放行
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                //尝试身份验证。因为有子类,所以会调用ClientCredentialsTokenEndpointFilter中的attemptAuthentication(request, response)方法,获取Authentication
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }
            //成功通过,保存authResult到SecurityContext并进行放行,进入下一个拦截器
            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

    protected boolean requiresAuthentication(HttpServletRequest request, HttpServletResponse response) {
        return this.requiresAuthenticationRequestMatcher.matches(request);
    }

    public abstract Authentication attemptAuthentication(HttpServletRequest var1, HttpServletResponse var2) throws AuthenticationException, IOException, ServletException;
    
     //保存Authentication到SecurityContext
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
        }

        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }

    protected AuthenticationManager getAuthenticationManager() {
        return this.authenticationManager;
    }

    public void setAuthenticationManager(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
    }

    public void setFilterProcessesUrl(String filterProcessesUrl) {
        this.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(filterProcessesUrl));
    }

    public final void setRequiresAuthenticationRequestMatcher(RequestMatcher requestMatcher) {
        Assert.notNull(requestMatcher, "requestMatcher cannot be null");
        this.requiresAuthenticationRequestMatcher = requestMatcher;
    }

    public RememberMeServices getRememberMeServices() {
        return this.rememberMeServices;
    }

    public void setRememberMeServices(RememberMeServices rememberMeServices) {
        Assert.notNull(rememberMeServices, "rememberMeServices cannot be null");
        this.rememberMeServices = rememberMeServices;
    }

    public void setContinueChainBeforeSuccessfulAuthentication(boolean continueChainBeforeSuccessfulAuthentication) {
        this.continueChainBeforeSuccessfulAuthentication = continueChainBeforeSuccessfulAuthentication;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        Assert.notNull(authenticationDetailsSource, "AuthenticationDetailsSource required");
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    protected boolean getAllowSessionCreation() {
        return this.allowSessionCreation;
    }

    public void setAllowSessionCreation(boolean allowSessionCreation) {
        this.allowSessionCreation = allowSessionCreation;
    }

    public void setSessionAuthenticationStrategy(SessionAuthenticationStrategy sessionStrategy) {
        this.sessionStrategy = sessionStrategy;
    }

    public void setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler) {
        Assert.notNull(successHandler, "successHandler cannot be null");
        this.successHandler = successHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler) {
        Assert.notNull(failureHandler, "failureHandler cannot be null");
        this.failureHandler = failureHandler;
    }

    protected AuthenticationSuccessHandler getSuccessHandler() {
        return this.successHandler;
    }

    protected AuthenticationFailureHandler getFailureHandler() {
        return this.failureHandler;
    }
}

ClientCredentialsTokenEndpointFilter过滤器继承了AbstractAuthenticationProcessingFilter的方法,所以在经过ClientCredentialsTokenEndpointFilter过滤器的时候首先会对请求做判断,验证path是否是认证的请求/oauth/token,如果为false,则直接返回没有后续操作

ProviderManager类,服务授权的一个操作类,需要注意的是这是一个通用的提供服务授权的类。(第三方客户端client_id和client_secret认证会用这个类,用户username和password认证也是用这个类),他们都是先转为UsernamePasswordAuthenticationToken,再获取到Authentication,然后进行后续的认证操作。这点很重要。

为什么说第三方客户端和用户身份都是这个类呢?

UserDetailsManager和ClientDetailsUserDetailsService都继承了UserDetailsService,所以都会有UserDetails loadUserByUsername(String var1),但是不同的是ClientDetailsUserDetailsService重写UserDetails loadUserByUsername(String var1)后,底层查询数据库使用的是ClientDetailsService的clientDetailsService.clientDetailsService.loadClientByClientId(username)的方法,先获取客户端的信息,然后封装为UserDetails返回。所以我们感觉到都是使用UserDetails。

我会在后面的代码里指出来。

package org.springframework.security.authentication;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.CredentialsContainer;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.util.Assert;

public class ProviderManager implements AuthenticationManager, MessageSourceAware, InitializingBean {
    private static final Log logger = LogFactory.getLog(ProviderManager.class);
    private AuthenticationEventPublisher eventPublisher;
    private List<AuthenticationProvider> providers;
    protected MessageSourceAccessor messages;
    private AuthenticationManager parent;
    private boolean eraseCredentialsAfterAuthentication;

    public ProviderManager(List<AuthenticationProvider> providers) {
        this(providers, (AuthenticationManager)null);
    }

    public ProviderManager(List<AuthenticationProvider> providers, AuthenticationManager parent) {
        this.eventPublisher = new ProviderManager.NullEventPublisher();
        this.providers = Collections.emptyList();
        this.messages = SpringSecurityMessageSource.getAccessor();
        this.eraseCredentialsAfterAuthentication = true;
        Assert.notNull(providers, "providers list cannot be null");
        this.providers = providers;
        this.parent = parent;
        this.checkState();
    }

    public void afterPropertiesSet() throws Exception {
        this.checkState();
    }

    private void checkState() {
        if (this.parent == null && this.providers.isEmpty()) {
            throw new IllegalArgumentException("A parent AuthenticationManager or a list of AuthenticationProviders is required");
        }
    }
    //此方法在过滤器ClientCredentialsTokenEndpointFilter会被调用,获取Authentication
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Class<? extends Authentication> toTest = authentication.getClass();
        AuthenticationException lastException = null;
        Authentication result = null;
        boolean debug = logger.isDebugEnabled();
        //判断所有的服务提供者
        Iterator var6 = this.getProviders().iterator();
        while(var6.hasNext()) {
            AuthenticationProvider provider = (AuthenticationProvider)var6.next();
            if (provider.supports(toTest)) {
                if (debug) {
                    logger.debug("Authentication attempt using " + provider.getClass().getName());
                }

                try {
                    //方法由org.springframework.security.authentication.dao.AbstractUserDetailsAuthenticationProvider提供,只要匹配到一个结果就返回
                    result = provider.authenticate(authentication);
                    if (result != null) {
                        this.copyDetails(authentication, result);
                        break;
                    }
                } catch (AccountStatusException var11) {
                    this.prepareException(var11, authentication);
                    throw var11;
                } catch (InternalAuthenticationServiceException var12) {
                    this.prepareException(var12, authentication);
                    throw var12;
                } catch (AuthenticationException var13) {
                    lastException = var13;
                }
            }
        }

        if (result == null && this.parent != null) {
            try {
                result = this.parent.authenticate(authentication);
            } catch (ProviderNotFoundException var9) {
                ;
            } catch (AuthenticationException var10) {
                lastException = var10;
            }
        }

        if (result != null) {
            if (this.eraseCredentialsAfterAuthentication && result instanceof CredentialsContainer) {
                ((CredentialsContainer)result).eraseCredentials();
            }

            this.eventPublisher.publishAuthenticationSuccess(result);
            return result;
        } else {
            if (lastException == null) {
                lastException = new ProviderNotFoundException(this.messages.getMessage("ProviderManager.providerNotFound", new Object[]{toTest.getName()}, "No AuthenticationProvider found for {0}"));
            }

            this.prepareException((AuthenticationException)lastException, authentication);
            throw lastException;
        }
    }

    private void prepareException(AuthenticationException ex, Authentication auth) {
        this.eventPublisher.publishAuthenticationFailure(ex, auth);
    }

    private void copyDetails(Authentication source, Authentication dest) {
        if (dest instanceof AbstractAuthenticationToken && dest.getDetails() == null) {
            AbstractAuthenticationToken token = (AbstractAuthenticationToken)dest;
            token.setDetails(source.getDetails());
        }

    }

    public List<AuthenticationProvider> getProviders() {
        return this.providers;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setAuthenticationEventPublisher(AuthenticationEventPublisher eventPublisher) {
        Assert.notNull(eventPublisher, "AuthenticationEventPublisher cannot be null");
        this.eventPublisher = eventPublisher;
    }

    public void setEraseCredentialsAfterAuthentication(boolean eraseSecretData) {
        this.eraseCredentialsAfterAuthentication = eraseSecretData;
    }

    public boolean isEraseCredentialsAfterAuthentication() {
        return this.eraseCredentialsAfterAuthentication;
    }

    private static final class NullEventPublisher implements AuthenticationEventPublisher {
        private NullEventPublisher() {
        }

        public void publishAuthenticationFailure(AuthenticationException exception, Authentication authentication) {
        }

        public void publishAuthenticationSuccess(Authentication authentication) {
        }
    }
}

AbstractUserDetailsAuthenticationProvider,UserDetails授权服务提供者

package org.springframework.security.authentication.dao;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AccountExpiredException;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsChecker;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.core.userdetails.cache.NullUserCache;
import org.springframework.util.Assert;

public abstract class AbstractUserDetailsAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
    protected final Log logger = LogFactory.getLog(this.getClass());
    protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();
    private UserCache userCache = new NullUserCache();
    private boolean forcePrincipalAsString = false;
    protected boolean hideUserNotFoundExceptions = true;
    private UserDetailsChecker preAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPreAuthenticationChecks();
    private UserDetailsChecker postAuthenticationChecks = new AbstractUserDetailsAuthenticationProvider.DefaultPostAuthenticationChecks();
    private GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();

    public AbstractUserDetailsAuthenticationProvider() {
    }

    protected abstract void additionalAuthenticationChecks(UserDetails var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public final void afterPropertiesSet() throws Exception {
        Assert.notNull(this.userCache, "A user cache must be set");
        Assert.notNull(this.messages, "A message source must be set");
        this.doAfterPropertiesSet();
    }

    //该方法会被ProviderManager调用
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        //从缓存中获取UserDetails信息,缓存是user为空的状态下,查询数据的时候封装进去的
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                //请求请求的用户进行检索,此时调用的是DaoAuthenticationProvider,此时会到数据库查询用户,查询数据库的方法loadUserByUsername(username),同时会封装进行userCache缓存。
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            //检查用户信息
            this.preAuthenticationChecks.check(user);
            //校验密码,UsernamePasswordAuthenticationToken是用户提交的信息。additionalAuthenticationChecks是org.springframework.security.authentication.dao.DaoAuthenticationProvider提供的行
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);
    }

    protected Authentication createSuccessAuthentication(Object principal, Authentication authentication, UserDetails user) {
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(principal, authentication.getCredentials(), this.authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());
        return result;
    }

    protected void doAfterPropertiesSet() throws Exception {
    }

    public UserCache getUserCache() {
        return this.userCache;
    }

    public boolean isForcePrincipalAsString() {
        return this.forcePrincipalAsString;
    }

    public boolean isHideUserNotFoundExceptions() {
        return this.hideUserNotFoundExceptions;
    }

    protected abstract UserDetails retrieveUser(String var1, UsernamePasswordAuthenticationToken var2) throws AuthenticationException;

    public void setForcePrincipalAsString(boolean forcePrincipalAsString) {
        this.forcePrincipalAsString = forcePrincipalAsString;
    }

    public void setHideUserNotFoundExceptions(boolean hideUserNotFoundExceptions) {
        this.hideUserNotFoundExceptions = hideUserNotFoundExceptions;
    }

    public void setMessageSource(MessageSource messageSource) {
        this.messages = new MessageSourceAccessor(messageSource);
    }

    public void setUserCache(UserCache userCache) {
        this.userCache = userCache;
    }

    public boolean supports(Class<?> authentication) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication);
    }

    protected UserDetailsChecker getPreAuthenticationChecks() {
        return this.preAuthenticationChecks;
    }

    public void setPreAuthenticationChecks(UserDetailsChecker preAuthenticationChecks) {
        this.preAuthenticationChecks = preAuthenticationChecks;
    }

    protected UserDetailsChecker getPostAuthenticationChecks() {
        return this.postAuthenticationChecks;
    }

    public void setPostAuthenticationChecks(UserDetailsChecker postAuthenticationChecks) {
        this.postAuthenticationChecks = postAuthenticationChecks;
    }

    public void setAuthoritiesMapper(GrantedAuthoritiesMapper authoritiesMapper) {
        this.authoritiesMapper = authoritiesMapper;
    }

    private class DefaultPostAuthenticationChecks implements UserDetailsChecker {
        private DefaultPostAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isCredentialsNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account credentials have expired");
                throw new CredentialsExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.credentialsExpired", "User credentials have expired"));
            }
        }
    }
    
//验证UserDetails的有效性
    private class DefaultPreAuthenticationChecks implements UserDetailsChecker {
        private DefaultPreAuthenticationChecks() {
        }

        public void check(UserDetails user) {
            if (!user.isAccountNonLocked()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is locked");
                throw new LockedException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.locked", "User account is locked"));
            } else if (!user.isEnabled()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is disabled");
                throw new DisabledException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.disabled", "User is disabled"));
            } else if (!user.isAccountNonExpired()) {
                AbstractUserDetailsAuthenticationProvider.this.logger.debug("User account is expired");
                throw new AccountExpiredException(AbstractUserDetailsAuthenticationProvider.this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.expired", "User account has expired"));
            }
        }
    }
}

DaoAuthenticationProvider,身份证验证服务类

package org.springframework.security.authentication.dao;

import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.InternalAuthenticationServiceException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.authentication.encoding.PasswordEncoder;
import org.springframework.security.authentication.encoding.PlaintextPasswordEncoder;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.util.Assert;

public class DaoAuthenticationProvider extends AbstractUserDetailsAuthenticationProvider {
    private static final String USER_NOT_FOUND_PASSWORD = "userNotFoundPassword";
    private PasswordEncoder passwordEncoder;
    private String userNotFoundEncodedPassword;
    private SaltSource saltSource;
    private UserDetailsService userDetailsService;

    public DaoAuthenticationProvider() {
        this.setPasswordEncoder((PasswordEncoder)(new PlaintextPasswordEncoder()));
    }

    
    /**
    * 校验密码,UsernamePasswordAuthenticationToken是用户提交的信息,passwordEncoder默认是使用框架提供的,我们可以自定义自己的自己的密码器
    *
    * @param UserDetails 用户详情对象,从数据查询出来的
    * @param UsernamePasswordAuthenticationToken 用户表单提交的用户名和密码
    * 
    */
    protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;
        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            //密码比较,会使用passwordEncoder会把用户提交的密码进行编码然后与userDetails的密码进行比较,具体查看 org.springframework.security.authentication.encoding.LdapShaPasswordEncoder。没有异常过滤器就通过,进入后续的流程。将会进入到下一个过滤器:org.springframework.security.oauth2.provider.endpoint.TokenEndpointAuthenticationFilter
            if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }

    protected void doAfterPropertiesSet() throws Exception {
        Assert.notNull(this.userDetailsService, "A UserDetailsService must be set");
    }
    
    //检索用户
    protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
            //加载UserDetails,调用的是UserDetailsService子类里面的方法。提前已经有提到过了,这个一个通用的授权认证,所以第三方客户和用户授权验证也都是调用这个方法,然后会进行转换。详情可以查询下面的子类,我们这里重点关注的它的2个子类:org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService和org.springframework.security.provisioning.UserDetailsManager,如果使用的ClientDetailsUserDetailsService,那么查询的是第三方客户端的数据库信息,然后会将信息转为UserDetails进行验证。
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
            }

            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }

    //设置密码加密方式
    public void setPasswordEncoder(Object passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        if (passwordEncoder instanceof PasswordEncoder) {
            this.setPasswordEncoder((PasswordEncoder)passwordEncoder);
        } else if (passwordEncoder instanceof org.springframework.security.crypto.password.PasswordEncoder) {
            final org.springframework.security.crypto.password.PasswordEncoder delegate = (org.springframework.security.crypto.password.PasswordEncoder)passwordEncoder;
            this.setPasswordEncoder(new PasswordEncoder() {
                public String encodePassword(String rawPass, Object salt) {
                    this.checkSalt(salt);
                    return delegate.encode(rawPass);
                }

                public boolean isPasswordValid(String encPass, String rawPass, Object salt) {
                    this.checkSalt(salt);
                    return delegate.matches(rawPass, encPass);
                }

                private void checkSalt(Object salt) {
                    Assert.isNull(salt, "Salt value must be null when used with crypto module PasswordEncoder");
                }
            });
        } else {
            throw new IllegalArgumentException("passwordEncoder must be a PasswordEncoder instance");
        }
    }
    
   //设置密码加密方式
    private void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        Assert.notNull(passwordEncoder, "passwordEncoder cannot be null");
        this.userNotFoundEncodedPassword = passwordEncoder.encodePassword("userNotFoundPassword", (Object)null);
        this.passwordEncoder = passwordEncoder;
    }

    protected PasswordEncoder getPasswordEncoder() {
        return this.passwordEncoder;
    }

    public void setSaltSource(SaltSource saltSource) {
        this.saltSource = saltSource;
    }

    protected SaltSource getSaltSource() {
        return this.saltSource;
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    protected UserDetailsService getUserDetailsService() {
        return this.userDetailsService;
    }
}

这里把ClientDetailsUserDetailsService放出来,方便看下内部转换部分。

package org.springframework.security.oauth2.provider.client;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.NoSuchClientException;

public class ClientDetailsUserDetailsService implements UserDetailsService {
    private final ClientDetailsService clientDetailsService;
    private String emptyPassword = "";

    public ClientDetailsUserDetailsService(ClientDetailsService clientDetailsService) {
        this.clientDetailsService = clientDetailsService;
    }

    public void setPasswordEncoder(PasswordEncoder passwordEncoder) {
        this.emptyPassword = passwordEncoder.encode("");
    }
    
    //loadUserByUsername方法的username是客户端的clientId
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        ClientDetails clientDetails;
        try {
            //里面底层肯定是调用数据库的,默认它有自己的JDBC实现(有默认的sql,可以到org.springframework.security.oauth2.provider.client.JdbcClientDetailsService查看),我们一般重写它的方法做自己的实现。
            clientDetails = this.clientDetailsService.loadClientByClientId(username);
        } catch (NoSuchClientException var4) {
            throw new UsernameNotFoundException(var4.getMessage(), var4);
        }

        String clientSecret = clientDetails.getClientSecret();
        if (clientSecret == null || clientSecret.trim().length() == 0) {
            clientSecret = this.emptyPassword;
        }
        //对客户进行转换,最后返回的是User
        return new User(username, clientSecret, clientDetails.getAuthorities());
    }
}

第三方客户端验证完毕,就会进入TokenEndpointAuthenticationFilter过滤器,进行用户身份信息验证

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.security.oauth2.provider.endpoint;

import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.security.authentication.AuthenticationDetailsSource;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.common.util.OAuth2Utils;
import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.OAuth2Request;
import org.springframework.security.oauth2.provider.OAuth2RequestFactory;
import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;

public class TokenEndpointAuthenticationFilter implements Filter {
    private static final Log logger = LogFactory.getLog(TokenEndpointAuthenticationFilter.class);
    private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new WebAuthenticationDetailsSource();
    private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
    private final AuthenticationManager authenticationManager;
    private final OAuth2RequestFactory oAuth2RequestFactory;

    public TokenEndpointAuthenticationFilter(AuthenticationManager authenticationManager, OAuth2RequestFactory oAuth2RequestFactory) {
        this.authenticationManager = authenticationManager;
        this.oAuth2RequestFactory = oAuth2RequestFactory;
    }

    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    public void setAuthenticationDetailsSource(AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource) {
        this.authenticationDetailsSource = authenticationDetailsSource;
    }

    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        boolean debug = logger.isDebugEnabled();
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;

        try {
            //提取认证信息
            Authentication credentials = this.extractCredentials(request);
            if (credentials != null) {
                if (debug) {
                    logger.debug("Authentication credentials found for '" + credentials.getName() + "'");
                }
                //进入验证流程,返回user的Authentication。验证流程和第三方客户验证流程一样。就是使用ProviderManager类,请查看前面介绍的内容。
                Authentication authResult = this.authenticationManager.authenticate(credentials);
                if (debug) {
                    logger.debug("Authentication success: " + authResult.getName());
                }
                //在SecurityContext获取第三方客户端的Authentication,在ClientCredentialsTokenEndpointFilter过滤器执行完毕后保存有的。
                Authentication clientAuth = SecurityContextHolder.getContext().getAuthentication();
                if (clientAuth == null) {
                    throw new BadCredentialsException("No client authentication found. Remember to put a filter upstream of the TokenEndpointAuthenticationFilter.");
                }
                //封装授权请求,底层应该是所有请求参数遍历出来然后封装
                Map<String, String> map = this.getSingleValueMap(request);
                map.put("client_id", clientAuth.getName());
                AuthorizationRequest authorizationRequest = this.oAuth2RequestFactory.createAuthorizationRequest(map);
                authorizationRequest.setScope(this.getScope(request));
                //判断客户端是否已经做验证
                if (clientAuth.isAuthenticated()) {
                    //设置为被认可的凭证
                    authorizationRequest.setApproved(true);
                }
                //将授权请求封装成OAuth2Request,底层调用的还是AuthorizationRequest的方法public OAuth2Request createOAuth2Request(){}方法,这里不把类写出来了。
                OAuth2Request storedOAuth2Request = this.oAuth2RequestFactory.createOAuth2Request(authorizationRequest);
                //将来user的Authentication保存到SecurityContext中
                SecurityContextHolder.getContext().setAuthentication(new OAuth2Authentication(storedOAuth2Request, authResult));
                //放行请求。到这里说明客户端和用户都做了验证,这时候就会进入到前面提到的/oauth/token站点里面去。最后生成token返回。
                this.onSuccessfulAuthentication(request, response, authResult);
            }
        } catch (AuthenticationException var13) {
            SecurityContextHolder.clearContext();
            if (debug) {
                logger.debug("Authentication request for failed: " + var13);
            }

            this.onUnsuccessfulAuthentication(request, response, var13);
            this.authenticationEntryPoint.commence(request, response, var13);
            return;
        }

        chain.doFilter(request, response);
    }
    
    //封装授权请求
    private Map<String, String> getSingleValueMap(HttpServletRequest request) {
        Map<String, String> map = new HashMap();
        Map<String, String[]> parameters = request.getParameterMap();
        Iterator var4 = parameters.keySet().iterator();

        while(var4.hasNext()) {
            String key = (String)var4.next();
            String[] values = (String[])parameters.get(key);
            map.put(key, values != null && values.length > 0 ? values[0] : null);
        }

        return map;
    }

    protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) throws IOException {
    }

    protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException {
    }
    
    //提取认证信息
    protected Authentication extractCredentials(HttpServletRequest request) {
        String grantType = request.getParameter("grant_type");
        //判断授权码是否用密码的方式授权
        if (grantType != null && grantType.equals("password")) {
            //提取用户信息封装成UsernamePasswordAuthenticationToken,获取到Authentication
            UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(request.getParameter("username"), request.getParameter("password"));
            result.setDetails(this.authenticationDetailsSource.buildDetails(request));
            return result;
        } else {
            return null;
        }
    }

    private Set<String> getScope(HttpServletRequest request) {
        return OAuth2Utils.parseParameterList(request.getParameter("scope"));
    }

    public void init(FilterConfig filterConfig) throws ServletException {
    }

    public void destroy() {
    }
}

生成token的代码可以查看org.springframework.security.oauth2.provider.token.TokenEnhancer下面的实现类
其中默认的应该是使用:org.springframework.security.oauth2.provider.token.TokenEnhancerChain,但是因为我项目使用的是jwt,所以用了org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter

TokenEnhancer ,具体实现可以看它的实现方法

package org.springframework.security.oauth2.provider.token;

import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;

public interface TokenEnhancer {
    OAuth2AccessToken enhance(OAuth2AccessToken var1, OAuth2Authentication var2);
}

下面再把这个2个实习类写写出来给大家参考

JwtAccessTokenConverter类

package org.springframework.security.oauth2.provider.token.store;

import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.Map;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.crypto.codec.Base64;
import org.springframework.security.jwt.Jwt;
import org.springframework.security.jwt.JwtHelper;
import org.springframework.security.jwt.crypto.sign.InvalidSignatureException;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.jwt.crypto.sign.RsaSigner;
import org.springframework.security.jwt.crypto.sign.RsaVerifier;
import org.springframework.security.jwt.crypto.sign.SignatureVerifier;
import org.springframework.security.jwt.crypto.sign.Signer;
import org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken;
import org.springframework.security.oauth2.common.ExpiringOAuth2RefreshToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import org.springframework.security.oauth2.common.util.JsonParser;
import org.springframework.security.oauth2.common.util.JsonParserFactory;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.util.Assert;

public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {
    public static final String TOKEN_ID = "jti";
    public static final String ACCESS_TOKEN_ID = "ati";
    private static final Log logger = LogFactory.getLog(JwtAccessTokenConverter.class);
    private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
    private JsonParser objectMapper = JsonParserFactory.create();
    private String verifierKey = (new RandomValueStringGenerator()).generate();
    private Signer signer;
    private String signingKey;
    private SignatureVerifier verifier;

    public JwtAccessTokenConverter() {
        this.signer = new MacSigner(this.verifierKey);
        this.signingKey = this.verifierKey;
    }

    public void setAccessTokenConverter(AccessTokenConverter tokenConverter) {
        this.tokenConverter = tokenConverter;
    }

    public AccessTokenConverter getAccessTokenConverter() {
        return this.tokenConverter;
    }

    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        return this.tokenConverter.convertAccessToken(token, authentication);
    }

    public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
        return this.tokenConverter.extractAccessToken(value, map);
    }

    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        return this.tokenConverter.extractAuthentication(map);
    }

    public void setVerifier(SignatureVerifier verifier) {
        this.verifier = verifier;
    }

    public void setSigner(Signer signer) {
        this.signer = signer;
    }

    public Map<String, String> getKey() {
        Map<String, String> result = new LinkedHashMap();
        result.put("alg", this.signer.algorithm());
        result.put("value", this.verifierKey);
        return result;
    }

    public void setKeyPair(KeyPair keyPair) {
        PrivateKey privateKey = keyPair.getPrivate();
        Assert.state(privateKey instanceof RSAPrivateKey, "KeyPair must be an RSA ");
        this.signer = new RsaSigner((RSAPrivateKey)privateKey);
        RSAPublicKey publicKey = (RSAPublicKey)keyPair.getPublic();
        this.verifier = new RsaVerifier(publicKey);
        this.verifierKey = "-----BEGIN PUBLIC KEY-----\n" + new String(Base64.encode(publicKey.getEncoded())) + "\n-----END PUBLIC KEY-----";
    }

    public void setSigningKey(String key) {
        Assert.hasText(key);
        key = key.trim();
        this.signingKey = key;
        if (this.isPublic(key)) {
            this.signer = new RsaSigner(key);
            logger.info("Configured with RSA signing key");
        } else {
            this.verifierKey = key;
            this.signer = new MacSigner(key);
        }

    }

    private boolean isPublic(String key) {
        return key.startsWith("-----BEGIN");
    }

    public boolean isPublic() {
        return this.signer instanceof RsaSigner;
    }

    public void setVerifierKey(String key) {
        this.verifierKey = key;
    }

    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> info = new LinkedHashMap(accessToken.getAdditionalInformation());
        String tokenId = result.getValue();
        if (!info.containsKey("jti")) {
            info.put("jti", tokenId);
        } else {
            tokenId = (String)info.get("jti");
        }

        result.setAdditionalInformation(info);
        result.setValue(this.encode(result, authentication));
        OAuth2RefreshToken refreshToken = result.getRefreshToken();
        if (refreshToken != null) {
            DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
            encodedRefreshToken.setValue(refreshToken.getValue());
            encodedRefreshToken.setExpiration((Date)null);

            try {
                Map<String, Object> claims = this.objectMapper.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                if (claims.containsKey("jti")) {
                    encodedRefreshToken.setValue(claims.get("jti").toString());
                }
            } catch (IllegalArgumentException var11) {
                ;
            }

            Map<String, Object> refreshTokenInfo = new LinkedHashMap(accessToken.getAdditionalInformation());
            refreshTokenInfo.put("jti", encodedRefreshToken.getValue());
            refreshTokenInfo.put("ati", tokenId);
            encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
            DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication));
            if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                Date expiration = ((ExpiringOAuth2RefreshToken)refreshToken).getExpiration();
                encodedRefreshToken.setExpiration(expiration);
                token = new DefaultExpiringOAuth2RefreshToken(this.encode(encodedRefreshToken, authentication), expiration);
            }

            result.setRefreshToken((OAuth2RefreshToken)token);
        }

        return result;
    }

    public boolean isRefreshToken(OAuth2AccessToken token) {
        return token.getAdditionalInformation().containsKey("ati");
    }

    protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        String content;
        try {
            content = this.objectMapper.formatMap(this.tokenConverter.convertAccessToken(accessToken, authentication));
        } catch (Exception var5) {
            throw new IllegalStateException("Cannot convert access token to JSON", var5);
        }

        String token = JwtHelper.encode(content, this.signer).getEncoded();
        return token;
    }

    protected Map<String, Object> decode(String token) {
        try {
            Jwt jwt = JwtHelper.decodeAndVerify(token, this.verifier);
            String content = jwt.getClaims();
            Map<String, Object> map = this.objectMapper.parseMap(content);
            if (map.containsKey("exp") && map.get("exp") instanceof Integer) {
                Integer intValue = (Integer)map.get("exp");
                map.put("exp", new Long((long)intValue));
            }

            return map;
        } catch (Exception var6) {
            throw new InvalidTokenException("Cannot convert access token to JSON", var6);
        }
    }

    public void afterPropertiesSet() throws Exception {
        if (this.verifier == null) {
            Object verifier = new MacSigner(this.verifierKey);

            try {
                verifier = new RsaVerifier(this.verifierKey);
            } catch (Exception var5) {
                logger.warn("Unable to create an RSA verifier from verifierKey (ignoreable if using MAC)");
            }

            if (this.signer instanceof RsaSigner) {
                byte[] test = "test".getBytes();

                try {
                    ((SignatureVerifier)verifier).verify(test, this.signer.sign(test));
                    logger.info("Signing and verification RSA keys match");
                } catch (InvalidSignatureException var4) {
                    logger.error("Signing and verification RSA keys do not match");
                }
            } else if (verifier instanceof MacSigner) {
                Assert.state(this.signingKey == this.verifierKey, "For MAC signing you do not need to specify the verifier key separately, and if you do it must match the signing key");
            }

            this.verifier = (SignatureVerifier)verifier;
        }
    }
}

TokenEnhancerChain类

package org.springframework.security.oauth2.provider.token;

import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;

public class TokenEnhancerChain implements TokenEnhancer {
    private List<TokenEnhancer> delegates = Collections.emptyList();

    public TokenEnhancerChain() {
    }

    public void setTokenEnhancers(List<TokenEnhancer> delegates) {
        this.delegates = delegates;
    }

    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        OAuth2AccessToken result = accessToken;

        TokenEnhancer enhancer;
        for(Iterator var4 = this.delegates.iterator(); var4.hasNext(); result = enhancer.enhance(result, authentication)) {
            enhancer = (TokenEnhancer)var4.next();
        }

        return result;
    }
}

OAuthSecurityConfig,这是自定义的一认证授权服务配置

/**
* 认证授权服务器。提供认证(Authentication)、授权服务(Authorization)
*
*/
@Configuration
@Slf4j
public class OAuthSecurityConfig extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private AuthenticationManager auth;
    @Autowired
    @Qualifier("dataSource")
    private DataSource dataSource;
    @Autowired
    private RedisTemplate<String, String> redisTemplate;
    @Autowired
    private RsaKeyHelper rsaKeyHelper;
    @Autowired
    private JwtTokenUtil jwtTokenUtil;
    @Autowired
    private KeyConfiguration keyConfiguration;
    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    /**
     * 自定义TokenStore
     *
     * @return .
     */
    @Bean
    public RedisTokenStore redisTokenStore() {
        RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
        redisTokenStore.setPrefix("AG:OAUTH:");
        return redisTokenStore;
    }

    /**
     * 配置令牌端点(Token Endpoint)的安全约束
     *
     * @param security .
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        //SpEL 表达式配置安全约束
        security.tokenKeyAccess("permitAll()");
        security.checkTokenAccess("isAuthenticated()");
        //密码编码器,需要更换成加密模式
        security.passwordEncoder(NoOpPasswordEncoder.getInstance());
    }


    /**
     * 授权服务配置
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     * 关键拦截器:
     * org.springframework.security.oauth2.provider.endpoint.TokenEndpointAuthenticationFilter
     * org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter
     * @param endpoints .
     * @throws Exception .
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                //顶级身份管理器AuthenticationManager,会对用户提交的密码进行编码后与数据的密码进行比较
                .authenticationManager(auth)
                //token存储方式,此处会将token保持到redis数据库
                .tokenStore(redisTokenStore())
                //token转换器,覆盖默认的token生成方式
                .accessTokenConverter(accessTokenConverter());
    }

    /**
     * 定义服务验证器
     * 配置客户端详情服务(ClientDetailsService),客户端详情信息在这里进行初始化,你能够把客户端详情信息写死在这里或者是通过数据库来存储调取详情信息
     * <br/>
     * 底层会调用ClientDetailsService的loadClientByClientId(),从数据库oauth_client_details表查询第三方服务信息,但查询出来的信息最终会转为UserDetails进行验证
     * 默认的sql见:org.springframework.security.oauth2.provider.client.JdbcClientDetailsService
     * <br/>
     * NoOpPasswordEncoder.getInstance()表示当前数据库的密码是没有加密的
     *
     * @param clients .
     * @throws Exception .
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                //该类会使用jdbc将服务信息从数据库读取出来,然后使用PasswordEncoder密码编码器将请求提交的密码密码编码,最后与数据库获取到的密码做对比。数据库获取到的密码会被封装在ClientDetails中,密码对比是AuthenticationManager去进行认证
                .jdbc(dataSource)
                //密码编码器,需要更换成加密模式
                .passwordEncoder(NoOpPasswordEncoder.getInstance());
    }

    /**
     * 定义用户验证器(重新写AuthenticationManager,使用自定义的属性)
     * <br/>
     * 使用AuthenticationManagerBuilder构造AuthenticationManager,覆盖了系统默认的AuthenticationManager
     * 底层通过UserDetailsManager的loadUserByUsername(String var1)方法获取到UserDetails
     */
    @Configuration
    @Order(100)
    protected static class AuthenticationManagerConfiguration extends GlobalAuthenticationConfigurerAdapter {
        /**
         * OauthUserDetailsService 没有继承JdbcDaoImpl,而且实现UserDetailsService,然后自定义访问数据库用户
         */
        @Autowired
        private OauthUserDetailsService oauthUserDetailsService;

        /**
         * 定义用户验证器(属于安全框架,不属于Oauth2授权框架)
         * AuthenticationManagerBuilder为AuthenticationManager生成器,配置完成后会根据我们定义的属性生成AuthenticationManager
         *
         * @param auth 。
         * @throws Exception 。
         */
        @Override
        public void init(AuthenticationManagerBuilder auth) throws Exception {
            auth
                    //该类会从数据库中通过用户名将用户数据查询出来,然后使用PasswordEncoder密码编码器将请求提交的密码密码编码,最后与数据库获取到的密码做对比。数据库获取到的密码会被封装在UserDetails中,密码对比是AuthenticationManager去进行认证
                    .userDetailsService(oauthUserDetailsService)
                    //密码编码器
                    .passwordEncoder(new Sha256PasswordEncoder());
        }
    }

    /**
     * 定义token的生成方式,AccessToken转换器,这里使用对称加密JWT生成token,覆盖默认DefaultOAuth2AccessToken的生成方式
     *
     * @return JwtAccessTokenConverter
     * @throws InvalidKeyException      .
     * @throws NoSuchAlgorithmException .
     */
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() throws InvalidKeyException, NoSuchAlgorithmException {
        byte[] pri, pub;
        try {
            pri = rsaKeyHelper.toBytes(redisTemplate.opsForValue().get(RedisKeyConstants.REDIS_USER_PRI_KEY));
            pub = rsaKeyHelper.toBytes(redisTemplate.opsForValue().get(RedisKeyConstants.REDIS_USER_PUB_KEY));
        } catch (Exception e) {
            Map<String, byte[]> keyMap = rsaKeyHelper.generateKey(keyConfiguration.getUserSecret());
            redisTemplate.opsForValue().set(RedisKeyConstants.REDIS_USER_PRI_KEY, rsaKeyHelper.toHexString(keyMap.get("pri")));
            redisTemplate.opsForValue().set(RedisKeyConstants.REDIS_USER_PUB_KEY, rsaKeyHelper.toHexString(keyMap.get("pub")));
            pri = keyMap.get("pri");
            pub = keyMap.get("pub");
        }
        JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
            /***
             * 重写增强token方法,用于自定义一些token返回的信息
             */
            @Override
            public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
                // 从Authentication中获取OauthUser,Authentication在经过过滤器的时候会通过用户请求参数生成
                Authentication userAuthentication = authentication.getUserAuthentication();
                OauthUser user = (OauthUser) userAuthentication.getPrincipal();
                // 增加一些自定义信息返回
                final Map<String, Object> additionalInformation = Maps.newHashMap();
                Date expireTime = DateTime.now().plusSeconds(jwtTokenUtil.getExpire()).toDate();
                additionalInformation.put(CommonConstants.JWT_KEY_EXPIRE, expireTime);
                additionalInformation.put(CommonConstants.JWT_KEY_USER_ID, user.getId());
                additionalInformation.put(CommonConstants.JWT_KEY_TENANT_ID, user.getTenantId());
                additionalInformation.put(CommonConstants.JWT_KEY_DEPART_ID, user.getDepartId());
                additionalInformation.put(CommonConstants.JWT_KEY_NAME, user.getName());
                additionalInformation.put("sub", user.getUsername());
                ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
                return super.enhance(accessToken, authentication);
            }

        };
        accessTokenConverter.setKeyPair(new KeyPair(new RSAPublicKeyImpl(pub), RSAPrivateCrtKeyImpl.newKey(pri)));
        return accessTokenConverter;
    }
}

我们下面对派发token流程做一个简单的总结。

  1. 用户发起获取token的请求。
  2. 通过web拦截器和我们自定义的过滤,看到是申请派发token的请求,一律放行。
  3. 当经过ClientCredentialsTokenEndpointFilter过滤器的时候,进行第三方客户端凭证验证。
  4. 执行第三方客户端凭证验证流程,验证通过继续放行,验证不通过响应客户端。
  5. 当经过TokenEndpointAuthenticationFilter过滤器的时候,进行用户凭证验证。
  6. 执行用户身份凭证验证流程,验证通过继续放行,验证不通过响应客户端。
  7. 进入到TokenEndpoint站点,执行postAccessToken方法
  8. 生成token方法返回客户端。
  9. token派发完成
  10. 用户携带token访问资源(本文没有讲,携带token访问的时候也有一套验证token的流程,有时间再总结出来)

好了,Oauth2授权框架派发token的流程的大概说到这里,希望对大家有帮助,如有误解理解不对的地方望大家帮我修正。

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值