Oauth2源碼分析(上)

前言

攔截器順序:

    FilterComparator() {
        int order = 100;
        put(ChannelProcessingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(WebAsyncManagerIntegrationFilter.class, order);
        order += STEP;
        put(SecurityContextPersistenceFilter.class, order);
        order += STEP;
        put(HeaderWriterFilter.class, order);
        order += STEP;
        put(CorsFilter.class, order);
        order += STEP;
        put(CsrfFilter.class, order);
        order += STEP;
        put(LogoutFilter.class, order);
        order += STEP;
        filterToOrder.put(
            "org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter",
            order);
        order += STEP;
        put(X509AuthenticationFilter.class, order);
        order += STEP;
        put(AbstractPreAuthenticatedProcessingFilter.class, order);
        order += STEP;
        filterToOrder.put("org.springframework.security.cas.web.CasAuthenticationFilter",
                order);
        order += STEP;
        filterToOrder.put(
            "org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter",
            order);
        order += STEP;
        put(UsernamePasswordAuthenticationFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        filterToOrder.put(
                "org.springframework.security.openid.OpenIDAuthenticationFilter", order);
        order += STEP;
        put(DefaultLoginPageGeneratingFilter.class, order);
        order += STEP;
        put(ConcurrentSessionFilter.class, order);
        order += STEP;
        put(DigestAuthenticationFilter.class, order);
        order += STEP;
        put(BasicAuthenticationFilter.class, order);
        order += STEP;
        put(RequestCacheAwareFilter.class, order);
        order += STEP;
        put(SecurityContextHolderAwareRequestFilter.class, order);
        order += STEP;
        put(JaasApiIntegrationFilter.class, order);
        order += STEP;
        put(RememberMeAuthenticationFilter.class, order);
        order += STEP;
        put(AnonymousAuthenticationFilter.class, order);
        order += STEP;
        put(SessionManagementFilter.class, order);
        order += STEP;
        put(ExceptionTranslationFilter.class, order);
        order += STEP;
        put(FilterSecurityInterceptor.class, order);
        order += STEP;
        put(SwitchUserFilter.class, order);
    }

認證流程:Filter->構造Token->AuthenticationManager->轉給Provider處理->認證處理成功后續(xù)操作或者不通過拋異常

Security中的關鍵類:

  • ①UsernamePasswordAuthenticationFilter:如果是賬號密碼認證谷异,從請求參數中獲取賬號密碼模狭,封裝成為未認證過的UsernamePasswordAuthenticationToken對象绞蹦,調用attemptAuthentication方法進行認證耀销,在attemptAuthentication方法中會調用AuthenticationManager的authenticate方法對未認證的Authenticate對象token進行認證矫夯;
  • ②UsernamePasswordAuthenticationToken:Authentication的子類背稼,是驗證方式的一種周循,有待驗證和已驗證兩個構造方法宫纬。調用authenticate方法對其進行驗證焚挠。principal參數的類型一般為UserDetails、String漓骚、AuthenticatedPrincipal蝌衔、Principal;
  • ③ProviderManager:在AuthenticationProvider的authenticate方法中會遍歷AuthenticationProvider接口實現類的集合蝌蹂,遍歷時會調用AuthenticationProvider實現類AbstractUserDetailsAuthenticationProvider的support方法判斷需要驗證的Authentication對象是否符合AuthenticationProvider的類型噩斟。直到support方法判斷為true;
  • ④AbstractUserDetailsAuthenticationProvider(AuthenticationProvider的實現類):support方法為true孤个,匹配上合適的AuthenticationProvider實現類后(UsernamePasswordAuthenticationToken匹配的是AbstractUserDetailsAuthenticationProvider抽象類)剃允,調用AuthenticationProvider的authenticate方法進行驗證(所以真正進行驗證的是AuthenticationProvider實現類的authenticate方法);
  • ⑤DaoAuthenticationProvider(AuthenticationProvider和AbstractUserDetailsAuthenticationProvider的子類):在authenticate方法中對Authentication對象token進行認證硼身,取出對象中的username硅急,在retrieveUser方法中調用UserDetailsService對象的loadUserByUsername(username)方法得到UserDetails對象,如果UserDetails對象不是null佳遂,則認證通過营袜;最后調用繼承自父類AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication的方法,構建已認證的UsernamePasswordAuthenticationToken對象并返回丑罪。

構建已認證的UsernamePasswordAuthenticationToken對象并設置到上下文中SecurityContextHolder.getContext().setAuthentication(authenticationToken); 表示該請求已認證完成荚板,后續(xù)安全攔截器放行。
構建已認證的UsernamePasswordAuthenticationToken的第三個參數是該用戶所擁有的權限吩屹,后續(xù)的鑒權攔截器會根據傳入的權限信息對請求進行鑒權跪另。

21580557-a1b4bb0cec787209.png

繼承關系圖

AuthenticationManager

21580557-069e9c62f9d5f45b.png


一、授權碼模式源碼

1.1 概述

流程圖:

1622700752041.png

框架默認提供的接口:

  • AuthorizationEndpoint /oauth/authorize
  • WhitelabelApprovalEndpoint /oauth/confirm_access
  • TokenEndpoint /oauth/token
  • CheckTokenEndpoint /oauth/check_token
  • WhitelabelErrorEndpoint /oauth/error

授權碼模式和密碼模式的部分區(qū)別:

  • 授權碼模式輸入賬號密碼通過UsernamePasswordAuthenticationFilter類的attemptAuthentication方法進行驗證煤搜。
    授權碼模式攜帶code請求令牌通過ClientCredentialsTokenEndpointFilter類的attemptAuthentication方法進行驗證免绿。
  • 密碼模式調用ResourceOwnerPasswordTokenGranter類的getOAuth2Authentication方法獲取OAuth2Authentication;
    授權碼模式調用AuthorizationCodeTokenGranter類的getOAuth2Authentication方法獲取OAuth2Authentication;

流程分析:

訪問認證服務器和資源服務器,需要第三方服務器已經注冊到了認證服務器并生成了client_id和client_secret擦盾。

  1. 以csdn為例嘲驾,打開csdn登錄頁面淌哟,選擇QQ登錄。此時client為csdn辽故,qq為authroization server和resouce server徒仓。qq授權服務器里存儲了很多client信息,csdn只是眾多client中的一個誊垢。
  2. 攜帶response_type,client_id以及redirect_uri參數訪問認證服務器掉弛,跳轉到認證服務器的登錄頁面。用戶填寫用戶名喂走、密碼后殃饿,點擊授權并登錄,首先訪問qq授權服務器的/login路徑芋肠,spring security驗證username和password后給用戶發(fā)放JSessionId的cookie壁晒,session中存儲了Authentication。
  3. 再訪問qq授權服務器/oauth/authorize业栅,請求參數有client_id、client_secret谬晕、grant_type碘裕、code、redirect_uri攒钳,驗證通過后請求重定向到redirect_uri帮孔,且傳遞Authorization code。
  4. redirect_uri路徑指向的是client中的一個endpoint不撑,client接收到了code文兢,表明client信息已經在QQ授權服務器驗證成功。再憑借這個code值外加client_id,client_secret,grant_type=authorization_code,code,redirect_uri等參數焕檬,去訪問QQ的/oauth/token姆坚,返回access_token。
  5. 獲得access_token后实愚,client再去找qq的資源服務器要資源兼呵。

授權碼模式流程的理解:

授權碼模式獲取授權碼過程,其實就是訪問接口/oauth/authorize來重定向到指定網址并攜帶授權碼腊敲,但是該接口需要進行認證和鑒權(一般只要認證即可击喂,如果只有部分用戶可以使用授權碼模式,可以給/oauth/authorize接口和用戶設置權限)碰辅,所以拋出異常重定向到用戶登錄頁面進行用戶登錄和鑒權懂昂。如果客戶端autoprove是false,會重定向到/oauth/confirm_access接口返回確認登錄頁面没宾,點擊確認攜帶user_oauth_approval(true)參數請求/oauth/authorize接口凌彬,最終攜帶授權碼code重定向到指定頁面沸柔。

各過濾器作用:

  • UsernamePasswordAuthenticationFilter => This filter by default responds to the URL {@code /login}.
  • OAuth2AuthenticationProcessingFilter => 添加@EnableResourceServer注解就會自動添加該過濾器,用于解析和校驗請求攜帶的token饿序。

Oauth框架源碼比較奇怪的地方:

  1. 生成freshToken:先創(chuàng)建一個沒有exp的勉失,然后查詢exp再創(chuàng)建有exp的freshToken覆蓋最先生成的freshToken艾栋。
  2. 驗證token:loadAuthentication方法調用readAccessToken生成OAuth2AccessToken渗常,readAuthentication方法調用readAuthentication生成OAuth2Authentication兽叮。連續(xù)調用了兩次JwtHelper類的decodeAndVerify方法對token進行驗證茫虽。

關鍵類(校驗令牌硕糊,生成令牌蛔垢,令牌加強)

Token令牌時遍歷List<AuthenticationProvider>尋找合適的AuthenticationProvider狐树;生成Token令牌時遍歷List<AuthorizationCodeTokenGranter>尋找合適的AuthorizationCodeTokenGranter沟启;加強Token令牌時遍歷List<TokenEnhancer>尋找合適的 TokenEnhancer 型型,并調用enhance方法對OAuth2AccessToken 進行加強段审。

總結:

不管是請求/oauth/authorize還是/oauth/token,不管是授權碼模式還是密碼模式闹蒜,都需要經過過濾器對用戶賬號密碼或是客戶端賬號密碼進行認證寺枉,然后到達接口進行業(yè)務處理(如獲取授權碼、獲取令牌)绷落。然后經過ExceptionTranslationFilter過濾器到達FilterSecurityInterceptor姥闪,對請求的認證信息和權限信息進行校驗,如果校驗不通過拋出異常到FilterSecurityInterceptor進行相應的處理(如重定向到/login頁面)砌烁。

異常處理

如果授權未通過拋出異常筐喳,會在ExceptionTrancationalFilter類中處理,如果是AccessDeniedException異常且是匿名用戶函喉,會調用AuthenticationEntryPoint接口的commence方法進行后續(xù)處理避归。默認的是重定向到請求地址的/login頁面進行登錄,可以進行重寫管呵,實現重定向地址的改寫或直接拋異常等功能梳毙。

1.2 源碼

1.2.1 獲取授權碼流程

流程:

  1. 請求http://localhost:9500/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.taobao.com
    對client信息進行校驗撇寞。校驗通過后進行接口權限校驗顿天。
  2. 因為/oauth/authorize未開放權限,所以用戶需要鑒權蔑担。分兩種情況牌废,cookie中是否有用戶信息:
    2.1 如果請求的cookie中有用戶的賬號密碼,直接進行登錄啤握,并完成登錄用戶的鑒權鸟缕,直接跳轉到/oauth/confirm_access接口(WhitelabelApprovalEndpoint類),返回該類中拼接html代碼,即確認授權頁面懂从。
    2.2 如果cookie中沒有用戶的賬號密碼授段,重定向到登錄頁面(缺省為/login)。點擊login in后進行表單提交(默認提交地址為/login)番甩,通過UsernamePasswordAuthenticationFilter進行驗證侵贵,驗證通過后,繼續(xù)請求資源地址http://localhost:9500/oauth/authorize?client_id=c1&response_type=code&scope=ROLE_ADMIN&redirect_uri=http://www.taobao.com缘薛。對客戶端的賬號密碼等信息進行校驗窍育。然后跳轉到/oauth/confirm_access接口(WhitelabelApprovalEndpoint類),返回該類中拼接html代碼宴胧,即確認授權頁面漱抓。
  3. 同意授權后攜帶參數user_oauth_approval(true)請求http://localhost:9500/oauth/authorize,最終重定向到帶有code的指定路徑恕齐。

請求中會攜帶已認證的請求參數乞娄,在服務中的session中也存儲請求信息。

①首次請求/oauth/authorize

因為請求未經認證显歧,直接跳過上述攔截器進入ExceptionTranslationFilter類中的仪或,因為用戶未進行認證是匿名用戶,且未輸入用戶密碼士骤,拋出AccessDeniedException異常溶其,進入handleSpringSecurityException方法,調用sendStartAuthentication方法敦间,其中會將該次請求信息存儲到session中requestCache.saveRequest(request, response),然后
調用LoginUrlAuthenticationEntryPoint的commence方法束铭,其中會調用redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);得到redirectUrl="http://localhost:9500/login"廓块,
并將重定向地址寫入到response中 ,并將現請求路徑存儲到session的saverequest中契沫。然后等到過濾器鏈走完后就會重定向到指定地址带猴。

    public void commence(HttpServletRequest request, HttpServletResponse response,
            AuthenticationException authException) throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {

            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be
                // used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response,
                        authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        }
        else {
            // 得到重定向的地址redirectUrl="http://localhost:9500/login"
            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }
        //跳轉到重定向的地址
        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }

②用戶密碼登錄

輸入賬號密碼Sign in,且已輸入用戶密碼懈万,則將該次請求信息存儲到session中拴清,進入到UsernamePasswordAuthenticationFilter攔截器調用attemptAuthentication方法對賬號密碼進行驗證。授權碼模式最終會調用AbstractUserDetailsAuthenticationProvider類的authenticate方法對創(chuàng)建的未進行認證的UsernamePasswordAuthenticationToken進行認證会通。

//UsernamePasswordAuthenticationFilter的attemptAuthentication方法
    public Authentication attemptAuthentication(HttpServletRequest request,
            HttpServletResponse response) throws AuthenticationException {
        if (postOnly && !request.getMethod().equals("POST")) {
            //不是POST方法口予,拋異常
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }
        //從請求中獲取參數
        String username = obtainUsername(request);
        String password = obtainPassword(request);

        if (username == null) {
            username = "";
        }

        if (password == null) {
            password = "";
        }

        username = username.trim();
        // 我不知道用戶名密碼是不是對的,所以構造一個未認證的Token先
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
                username, password);

        // 順便把請求和Token存起來
        setDetails(request, authRequest);
        // Token給當前的AuthenticationManager處理
        return this.getAuthenticationManager().authenticate(authRequest);
    }

從請求參數中獲取username和password涕侈,通過username和password構建一個未認證的UsernamePasswordAuthenticationToken沪停,然后調用AuthenticationManager的authenticate方法進行認證。

//AbstractUserDetailsAuthenticationProvider類的authenticate方法
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,() -> messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports","Only UsernamePasswordAuthenticationToken is supported"));

        // 此處getPrincipal()為null,所以username為用戶輸入的用戶名admin
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED" : authentication.getName();

        boolean cacheWasUsed = true;
        //緩存中沒有UserDetails木张,所以user為null
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                //通過我們重寫的loadUserByUsername方法獲取UserDetails
                user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
            } catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

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

        try {
            //檢查isAccountNonLocked众辨、isEnabled、isAccountNonExpired信息舷礼,如果為false則拋異常
            preAuthenticationChecks.check(user);
            //檢查UserDetails中的密碼和UsernamePasswordAuthenticationToken中的密碼是否一致
            additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }
        //檢查isCredentialsNonExpired信息鹃彻,如果為false則拋異常
        postAuthenticationChecks.check(user);

        //將UserDetails放到緩存中
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        //因為forcePrincipalAsString是false,所以principalToReturn 是UserDetails
        if (forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

調用了上述類的子類DaoAuthenticationProvider的重寫方法retrieveUser妻献,在該方法中會調用我們重寫的loadUserByUsername方法獲取用戶的UserDetails(包含password)蛛株,如果返回的UserDetails為null,則拋異常旋奢。

//DaoAuthenticationProvider的retrieveUser方法
    protected final UserDetails retrieveUser(String username,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        prepareTimingAttackProtection();
        try {
            UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
            if (loadedUser == null) {
                throw new InternalAuthenticationServiceException(
                        "UserDetailsService returned null, which is an interface contract violation");
            }
            return loadedUser;
        }
        catch (UsernameNotFoundException ex) {
            mitigateAgainstTimingAttack(authentication);
            throw ex;
        }
        catch (InternalAuthenticationServiceException ex) {
            throw ex;
        }
        catch (Exception ex) {
            throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
        }
    }

驗證密碼會調用子類DaoAuthenticationProvider重寫的方法additionalAuthenticationChecks泳挥,如果UserDetails中的password解碼后與未認證的UsernamePasswordAuthenticationToken中的password不一致,拋BadCredentialsException異常至朗。

//DaoAuthenticationProvider類的additionalAuthenticationChecks
    protected void additionalAuthenticationChecks(UserDetails userDetails,
            UsernamePasswordAuthenticationToken authentication)
            throws AuthenticationException {
        if (authentication.getCredentials() == null) {
            logger.debug("Authentication failed: no credentials provided");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }

        String presentedPassword = authentication.getCredentials().toString();

        if (!passwordEncoder.matches(presentedPassword, userDetails.getPassword())) {
            logger.debug("Authentication failed: password does not match stored value");

            throw new BadCredentialsException(messages.getMessage(
                    "AbstractUserDetailsAuthenticationProvider.badCredentials",
                    "Bad credentials"));
        }
    }

最后將UserDetails(principal)屉符、未認證的UsernamePasswordAuthenticationToken和UserDetails作為參數調用父類AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication方法,最終調用得到锹引,得到已認證的UsernamePasswordAuthenticationToken矗钟。

//AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication方法
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        // Ensure we return the original credentials the user supplied,
        // so subsequent attempts are successful even with encoded passwords.
        // Also ensure we return the original getDetails(), so that future
        // authentication events after cache expiry contain the details
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

最后得到已認證的UsernamePasswordAuthenticationToken,將其添加到請求的參數中(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal)

21580557-2af07faffc4711cb.png

驗證通過后執(zhí)行successHandler.onAuthenticationSuccess(request, response, authResult)嫌变,獲取session中的savedrequest吨艇,重定向到原先的地址/oauth/authorize,并附帶完整請求參數腾啥。

public class SavedRequestAwareAuthenticationSuccessHandler extends
        SimpleUrlAuthenticationSuccessHandler {
    protected final Log logger = LogFactory.getLog(this.getClass());
 
    private RequestCache requestCache = new HttpSessionRequestCache();
 
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
            HttpServletResponse response, Authentication authentication)
            throws ServletException, IOException {
             // HttpSessionRequestCache.getRequest ,找名為SPRING_SECURITY_SAVED_REQUEST的session
        SavedRequest savedRequest = requestCache.getRequest(request, response);
 
        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);
 
            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl()
                || (targetUrlParameter != null && StringUtils.hasText(request
                        .getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);
 
            return;
        }
 
        clearAuthenticationAttributes(request);
 
        // Use the DefaultSavedRequest URL
           // 獲得原先存儲在SavedRequest中的redirectUrl,即/oauth/authorize
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }
 
    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

在ProviderManager中通過eraseCredentials方法將UsernamePasswordAuthenticationToken中的所有密碼刪除()东涡,然后通過publishAuthenticationSuccess方法將發(fā)布認證成功事件。

如果驗證過程沒有拋出異常倘待,最后會再次進入ExceptionTranslationFilter類中疮跑,用于接收FilterSecurityInterceptor攔截器拋出的異常。如果沒有拋出異常凸舵,那么正常訪問/oauth/authorize接口祖娘。
進入AuthorizationEndpoint類(接口類)的authorize方法中(/oauth/authorize接口),對client信息進行驗證(包括有效性啊奄、scope渐苏、重定向地址等)。如果客戶端已經預授權菇夸,直接生成code(將code和序列化后的OAuth2Authentication存儲到數據庫)并重定向到指定地址琼富;如果客戶端未預授權,則重定向到確認授權頁面庄新。

    @RequestMapping(value = "/oauth/authorize")
    public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
            SessionStatus sessionStatus, Principal principal) {

        // 通過Oauth2RequestFactory構建AuthorizationRequest
        AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);

        Set<String> responseTypes = authorizationRequest.getResponseTypes();

        //oauth/authorize這個請求只支持授權碼code模式和Implicit隱式模式
        if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
            throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
        }

        if (authorizationRequest.getClientId() == null) {
            throw new InvalidClientException("A client id must be provided");
        }

        try {
            //驗證請求中的攜帶的身份信息principal 是否已經驗證
            if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
                throw new InsufficientAuthenticationException(
                        "User must be authenticated with Spring Security before authorization can be completed.");
            }
            //通過ClientDetailsService檢索ClientDetails
            ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
            //獲取重定向的地址
            String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
            String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
            //確保requst中有重定向redirect_uri
            if (!StringUtils.hasText(resolvedRedirect)) {
                throw new RedirectMismatchException(
                        "A redirectUri must be either supplied or preconfigured in the ClientDetails");
            }
            //設置重定向地址
            authorizationRequest.setRedirectUri(resolvedRedirect);

            // 校驗client請求的是一組有效的scope,通過比對表oauth_client_details
            oauth2RequestValidator.validateScope(authorizationRequest, client);

            //預同意處理(ApprovalStoreUserApprovalHandler)
            //1. 校驗所有的scope是否已經全部是自動同意授權公黑,如果全部自動授權同意,則設置authorizationRequest
            //中屬性approved為true,否則走2
            //2. 查詢client_id下所有oauth_approvals,校驗在有效時間內Scope授權的情況凡蚜,如果在有效時間內Scope授權全部同意人断,
            //則設置authorizationRequest中屬性approved為true,否則為false
            authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
                    (Authentication) principal);
            // TODO: is this call necessary?
            // 這個步驟是不是多余的?朝蜘?
            boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
            authorizationRequest.setApproved(approved);

            // 如果預授權參數是true恶迈,直接將code重定向到redirect_uri
            if (authorizationRequest.isApproved()) {
                if (responseTypes.contains("token")) {
                    return getImplicitGrantResponse(authorizationRequest);
                }
                if (responseTypes.contains("code")) {
                    return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
                            (Authentication) principal));
                }
            }

            //如果預授權參數是false,跳轉到授權頁面
            //授權頁面是由WhitelabelApprovalEndpoint類生成的
            return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);

        }
        catch (RuntimeException e) {
            sessionStatus.setComplete();
            throw e;
        }

    }

如果未進行預授權谱醇,則將認證信息添加到response中并會重定向到確認授權頁面暇仲,代碼如下。

    private String userApprovalPage = "forward:/oauth/confirm_access";
    private ModelAndView getUserApprovalPageResponse(Map<String, Object> model,
            AuthorizationRequest authorizationRequest, Authentication principal) {
        if (logger.isDebugEnabled()) {
            logger.debug("Loading user approval page: " + userApprovalPage);
        }
        model.putAll(userApprovalHandler.getUserApprovalRequest(authorizationRequest, principal));
                //userApprovalPage為重定向地址:/oauth/confirm_access
        return new ModelAndView(userApprovalPage, model);
    }

同意授權后會攜帶授權信息并再次進入過濾器鏈副渴,并攜帶code重定向到指定的地址奈附。

1.2.2 請求token流程

①驗證客戶端信息

概述:此處的源碼流程與請求相同,只是授權碼模式已經完成了賬號密碼的驗證煮剧,只需要將其換成client的賬號密碼即可斥滤。

訪問localhost:9500/oauth/token?client_id=c1&client_secret=123456&grant_type=authorization_code&code=8bhrYC&redirect_uri=http://www.taobao.com
先進入AbstractAuthenticationProcessingFilter的doFilter方法中勉盅,調用其實現類ClientCredentialsTokenEndpointFilter的attemptAuthentication方法進行驗證佑颇。主要是對client的信息進行驗證,通過clientId和clientSecret構建未認證的UsernamePasswordAuthenticationToken對象草娜,調用authenticate方法對其進行認證挑胸。

//ClientCredentialsTokenEndpointFilter類的attemptAuthentication方法
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
            throws AuthenticationException, IOException, ServletException {

        if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
            throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
        }

        String clientId = request.getParameter("client_id");
        String clientSecret = request.getParameter("client_secret");

        // If the request is already authenticated we can assume that this
        // filter is not needed
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication != null && authentication.isAuthenticated()) {
            return authentication;
        }

        if (clientId == null) {
            throw new BadCredentialsException("No client credentials presented");
        }

        if (clientSecret == null) {
            clientSecret = "";
        }

        clientId = clientId.trim();
        UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
                clientSecret);

        return this.getAuthenticationManager().authenticate(authRequest);

    }

在ProviderManager類的authentication方法中對AuthenticationProvider列表進行遍歷,直到獲得可以匹配傳入的UsernamePasswordAuthenticationToken類的AuthenticationProvider類宰闰,即DaoAuthenticationProvider茬贵。

//ProviderManager類的authentication方法
    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;
        boolean debug = logger.isDebugEnabled();

        for (AuthenticationProvider provider : getProviders()) {
            if (!provider.supports(toTest)) {
                continue;
            }

            if (debug) {
                logger.debug("Authentication attempt using "
                        + provider.getClass().getName());
            }

            try {
                result = provider.authenticate(authentication);

                if (result != null) {
                    copyDetails(authentication, result);
                    break;
                }
            }
            catch (AccountStatusException | InternalAuthenticationServiceException e) {
                prepareException(e, authentication);
                throw e;
            } catch (AuthenticationException e) {
                lastException = e;
            }
        }

        if (result != null) {
            if (eraseCredentialsAfterAuthentication
                    && (result instanceof CredentialsContainer)) {
                // Authentication is complete. Remove credentials and other secret data
                // from authentication
                ((CredentialsContainer) result).eraseCredentials();
            }

            // If the parent AuthenticationManager was attempted and successful than it will publish an AuthenticationSuccessEvent
            // This check prevents a duplicate AuthenticationSuccessEvent if the parent AuthenticationManager already published it
            if (parentResult == null) {
                eventPublisher.publishAuthenticationSuccess(result);
            }
            return result;
        }

        ......

    }

同樣調用AbstractUserDetailsAuthenticationProvider的authenticate方法。

    public Authentication authenticate(Authentication authentication)
            throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
                () -> messages.getMessage(
                        "AbstractUserDetailsAuthenticationProvider.onlySupports",
                        "Only UsernamePasswordAuthenticationToken is supported"));

        // Determine username
        String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
                : authentication.getName();

        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);

        if (user == null) {
            cacheWasUsed = false;

            try {
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            catch (UsernameNotFoundException notFound) {
                logger.debug("User '" + username + "' not found");

                if (hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(messages.getMessage(
                            "AbstractUserDetailsAuthenticationProvider.badCredentials",
                            "Bad credentials"));
                }
                else {
                    throw notFound;
                }
            }

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

        try {
            preAuthenticationChecks.check(user);
            additionalAuthenticationChecks(user,
                    (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (AuthenticationException exception) {
            if (cacheWasUsed) {
                // There was a problem, so try again after checking
                // we're using latest data (i.e. not from the cache)
                cacheWasUsed = false;
                user = retrieveUser(username,
                        (UsernamePasswordAuthenticationToken) authentication);
                preAuthenticationChecks.check(user);
                additionalAuthenticationChecks(user,
                        (UsernamePasswordAuthenticationToken) authentication);
            }
            else {
                throw exception;
            }
        }

        postAuthenticationChecks.check(user);

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

        Object principalToReturn = user;

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

        return createSuccessAuthentication(principalToReturn, authentication, user);
    }

區(qū)別在于retrieveUser時注入的是ClientDetailsUserDetailsService對象移袍,調用loadUserByUsername方法闷沥,查詢的是client表,獲取到client的信息咐容。返回的是User對象,該對象自動將check方法檢查的屬性全部置為true蚂维。

//User類繼承自UserDetails
public class User implements UserDetails, CredentialsContainer {
    public User(String username, String password,
            Collection<? extends GrantedAuthority> authorities) {
        this(username, password, true, true, true, true, authorities);
    }
}

同樣的通過additionalAuthenticationChecks方法檢查client的密碼是否正確戳粒。并進行緩存。
所有校驗都通過后虫啥,調用 createSuccessAuthentication() 返回認證信息蔚约。

//AbstractUserDetailsAuthenticationProvider的createSuccessAuthentication方法
    protected Authentication createSuccessAuthentication(Object principal,
            Authentication authentication, UserDetails user) {
        //創(chuàng)建已認證的UsernamePasswordAuthenticationToken
        UsernamePasswordAuthenticationToken result = new UsernamePasswordAuthenticationToken(
                principal, authentication.getCredentials(),
                authoritiesMapper.mapAuthorities(user.getAuthorities()));
        result.setDetails(authentication.getDetails());

        return result;
    }

在該方法中創(chuàng)建已認證的UsernamePasswordAuthenticationToken(將authenticated屬性設置為true),并設置UserDetails后返回涂籽。
在ProviderManager類的eraseCredentials方法中將credentials置為null后返回到AbstractAuthenticationProcessingFilter類的dofilter方法中苹祟。
最后調用AbstractAuthenticationProcessingFilter的successfulAuthentication方法。

//ClientCredentialsTokenEndpointFilter的successfulAuthentication方法
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response,
            FilterChain chain, Authentication authResult) throws IOException, ServletException {
        super.successfulAuthentication(request, response, chain, authResult);
        chain.doFilter(request, response);
    }

在其父類的successfulAuthentication方法中將已認證的UsernamePasswordAuthenticationToken放置到安全上下文中

//ClientCredentialsTokenEndpointFilter的父類AbstractAuthenticationProcessingFilter的successfulAuthentication方法
    protected void successfulAuthentication(HttpServletRequest request,
            HttpServletResponse response, FilterChain chain, Authentication authResult)
            throws IOException, ServletException {

        if (logger.isDebugEnabled()) {
            logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
                    + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);

        rememberMeServices.loginSuccess(request, response, authResult);

        // Fire event
        if (this.eventPublisher != null) {
            eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
                    authResult, this.getClass()));
        }

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

最后調用下一級過濾器树枫。因為已經設置到安全上下文中直焙,所以過濾器放行,請求最終到達TokenEndpoint類的/oauth/token接口中砂轻。

②生成token流程

21580557-a5e7032d24362bbc.png

在TokenPoint類的postAccessToken方法(/oauth/token接口)中進行client校驗和令牌獲取奔誓。
大致流程如下:
從 principal 中獲取 clientId, 進而裝載 ClientDetails 。
從 parameters 中獲取 clientId搔涝、scope厨喂、grantType 以組裝 TokenRequest。
校驗 Client 信息庄呈。
根據 grantType 設置 TokenRequest 的 scope蜕煌。
通過令牌授予者獲取 Token。

@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
    // 以下是核心部分代碼...
    @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.");
        }

        // 1. 從 principal 中獲取 clientId, 進而 load client 信息
        String clientId = getClientId(principal);
        ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);

        // 2. 從 parameters 中拿 clientId诬留、scope斜纪、grantType 組裝 TokenRequest
        TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);

        // 3. 校驗 client 信息
        if (clientId != null && !clientId.equals("")) {
            if (!clientId.equals(tokenRequest.getClientId())) {
                // 雙重校驗: 確保從 principal 拿到的 client 信息與根據 parameters 得到的 client 信息一致
                throw new InvalidClientException("Given client ID does not match authenticated client");
            }
        }
        if (authenticatedClient != null) {
            oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
        }

        // 4. 根據 grantType 設置 TokenRequest 的 scope。
        // 授權類型有: password 模式故响、authorization_code 模式傀广、refresh_token 模式、client_credentials 模式彩届、implicit 模式
        if (!StringUtils.hasText(tokenRequest.getGrantType())) {
            throw new InvalidRequestException("Missing grant type");
        }
        if (tokenRequest.getGrantType().equals("implicit")) {
            throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
        }

        // 如果是授權碼模式, 則清空從數據庫查詢到的 scope伪冰。 因為授權請求過程會確定 scope, 所以沒必要傳。
        if (isAuthCodeRequest(parameters)) {
            if (!tokenRequest.getScope().isEmpty()) {
                logger.debug("Clearing scope of incoming token request");
                tokenRequest.setScope(Collections.<String> emptySet());
            }
        }

        // 如果是刷新 Token 模式, 解析并設置 scope
        if (isRefreshTokenRequest(parameters)) {
            // A refresh token has its own default scopes, so we should ignore any added by the factory here.
            tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
        }

        // 5. 通過令牌授予者獲取 token
        OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
        if (token == null) {
            throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
        }

        return getResponse(token);
    }
    // ...
}

通過getTokenGranter方法獲取AuthorizationServerEndpointsConfigurer樟蠕,以tokenRequest作為參數贮聂,調用grant方法獲取token。
21580557-6d771ccaf22d5b64.png

以下是各授權模式對應的 TokenGranter:

實現類 對應的授權模式
AuthorizationCodeTokenGranter 授權碼模式
ClientCredentialsTokenGranter 客戶端模式
ImplicitTokenGranter implicit 模式
RefreshTokenGranter 刷新 token 模式
ResourceOwnerPasswordTokenGranter 密碼模式
//AuthorizationServerEndpointsConfigurer中的getTokenGranter和grant方法
    public TokenGranter getTokenGranter() {
        return tokenGranter();
    }

    private TokenGranter tokenGranter() {
        if (tokenGranter == null) {
            tokenGranter = new TokenGranter() {
                private CompositeTokenGranter delegate;

                @Override
                public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
                    if (delegate == null) {
                        delegate = new CompositeTokenGranter(getDefaultTokenGranters());
                    }
                    return delegate.grant(grantType, tokenRequest);
                }
            };
        }
        return tokenGranter;
    }

疑問:為什么TokenEndpoint中的getTokenGranter方法會調用AuthorizationServerEndpointsConfigurer中的getTokenGranter方法寨辩。

最終調用了AuthorizationServerEndpointsConfigurer中的TokenGranter的grant方法吓懈。在該方法中調用了CompositeTokenGranter類的grant方法,CompositeTokenGranter的屬性List<TokenGranter>中包含了如下5種授權模式靡狞。


21580557-b82d8d36e87a0293.png
//CompositeTokenGranter類的grant方法
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
        for (TokenGranter granter : tokenGranters) {
            OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
            if (grant!=null) {
                return grant;
            }
        }
        return null;
    }

同驗證Token令牌時遍歷List<AuthenticationProvider>尋找合適的AuthenticationProvider一樣耻警,此處也會尋找合適的TokenGranter,調用grant方法返回生成的OAuth2AccessToken甸怕。其子類繼承了grant方法甘穿,判斷每個子類的grantType屬性是否和請求的grantType一致,最終匹配到AuthorizationCodeTokenGranter梢杭。

AuthorizationServerConfigurerAdapter類的三個重載方法的配置參數

  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務温兼,客戶端詳情信息在這里進行初始化,可以把客戶端詳情信息寫死在這里或者通過數據庫來存儲調取詳情信息武契。
  • AuthorizationServerEndpointsConfigurer:用來配置令牌(token) 的訪問端點和令牌服務(token services)募判。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束(權限)荡含。
//AbstractTokenGranter類的grant,getAccessToken和方法
    public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {

        if (!this.grantType.equals(grantType)) {
            return null;
        }
        
        String clientId = tokenRequest.getClientId();
        ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
        validateGrantType(grantType, client);

        if (logger.isDebugEnabled()) {
            logger.debug("Getting access token for: " + clientId);
        }

        return getAccessToken(client, tokenRequest);

    }

    protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
        return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
    }

createAccessToken方法的參數是OAuth2Authentication届垫,通過getOAuth2Authentication方法獲得释液,AuthorizationCodeTokenGranter重寫了父類的該方法。該方法中獲取參數中的授權碼和生成授權碼時的客戶端信息敦腔,然后刪除數據庫中的授權碼均澳,返回生成授權碼時的客戶端信息(即authentication)構成的OAuth2Authentication。對請求的客戶端信息和生成授權碼的客戶端信息進行校驗符衔。

21580557-dc4f99bbd915c5e7.png
//AuthorizationCodeTokenGranter類的getOAuth2Authentication方法找前。
    @Override
    protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {

        Map<String, String> parameters = tokenRequest.getRequestParameters();
        String authorizationCode = parameters.get("code");
        String redirectUri = parameters.get(OAuth2Utils.REDIRECT_URI);

        if (authorizationCode == null) {
            throw new InvalidRequestException("An authorization code must be supplied.");
        }

        OAuth2Authentication storedAuth = authorizationCodeServices.consumeAuthorizationCode(authorizationCode);
        if (storedAuth == null) {
            throw new InvalidGrantException("Invalid authorization code: " + authorizationCode);
        }

        OAuth2Request pendingOAuth2Request = storedAuth.getOAuth2Request();
        // https://jira.springsource.org/browse/SECOAUTH-333
        // This might be null, if the authorization was done without the redirect_uri parameter
        String redirectUriApprovalParameter = pendingOAuth2Request.getRequestParameters().get(
                OAuth2Utils.REDIRECT_URI);

        if ((redirectUri != null || redirectUriApprovalParameter != null)
                && !pendingOAuth2Request.getRedirectUri().equals(redirectUri)) {
            throw new RedirectMismatchException("Redirect URI mismatch.");
        }

        String pendingClientId = pendingOAuth2Request.getClientId();
        String clientId = tokenRequest.getClientId();
        if (clientId != null && !clientId.equals(pendingClientId)) {
            // just a sanity check.
            throw new InvalidClientException("Client ID mismatch");
        }

        // Secret is not required in the authorization request, so it won't be available
        // in the pendingAuthorizationRequest. We do want to check that a secret is provided
        // in the token request, but that happens elsewhere.

        Map<String, String> combinedParameters = new HashMap<String, String>(pendingOAuth2Request
                .getRequestParameters());
        // Combine the parameters adding the new ones last so they override if there are any clashes
        combinedParameters.putAll(parameters);
        
        // Make a new stored request with the combined parameters
        OAuth2Request finalStoredOAuth2Request = pendingOAuth2Request.createOAuth2Request(combinedParameters);
        
        Authentication userAuth = storedAuth.getUserAuthentication();
        
        return new OAuth2Authentication(finalStoredOAuth2Request, userAuth);

    }

返回的OAuth2Authentication作為參數,調用AuthorizationServerTokenServices的子類DefaultTokenServices的createAccessToken方法生成OAuth2AccessToken判族。該方法使用了tokenStore.getAccessToken(authentication)來獲取的token躺盛。如果getAccessToken返回的token是null,則直接創(chuàng)建新的token形帮;如果getAccessToken返回了持久化的token槽惫,則判斷token是否過期,如果未過期則根據OAuth2Authentication 信息重新存儲token以防信息變更辩撑,如果已過期則

    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setClientDetailsService(clientDetailsService); //客戶端詳情服務
        services.setSupportRefreshToken(true); //支持刷新令牌
        services.setTokenStore(tokenStore); //令牌的存儲策略
        //令牌增強,設置JWT令牌
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter));
        services.setTokenEnhancer(tokenEnhancerChain);

        services.setAccessTokenValiditySeconds(7200); //令牌默認有效時間2小時
        services.setRefreshTokenValiditySeconds(259200); //刷新令牌默認有效期3天
        return services;
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .authenticationManager(authenticationManager)//認證管理器
                .authorizationCodeServices(authorizationCodeServices)//授權碼服務
                .tokenServices(tokenServices()) //令牌管理服務(設置令牌存儲方式和令牌類型JWT)
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
//DefaultTokenServices的createAccessToken方法界斜。其中TokenStore為配置的JwtTokenStore。
    @Transactional
    public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {

        OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
        OAuth2RefreshToken refreshToken = null;
        if (existingAccessToken != null) {
            if (existingAccessToken.isExpired()) {
                if (existingAccessToken.getRefreshToken() != null) {
                    refreshToken = existingAccessToken.getRefreshToken();
                    // The token store could remove the refresh token when the
                    // access token is removed, but we want to
                    // be sure...
                    tokenStore.removeRefreshToken(refreshToken);
                }
                tokenStore.removeAccessToken(existingAccessToken);
            }
            else {
                // Re-store the access token in case the authentication has changed
                tokenStore.storeAccessToken(existingAccessToken, authentication);
                return existingAccessToken;
            }
        }

        // Only create a new refresh token if there wasn't an existing one
        // associated with an expired access token.
        // Clients might be holding existing refresh tokens, so we re-use it in
        // the case that the old access token
        // expired.
        if (refreshToken == null) {
            refreshToken = createRefreshToken(authentication);
        }
        // But the refresh token itself might need to be re-issued if it has
        // expired.
        else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
            ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
            if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
                refreshToken = createRefreshToken(authentication);
            }
        }

        OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
        tokenStore.storeAccessToken(accessToken, authentication);
        // In case it was modified
        refreshToken = accessToken.getRefreshToken();
        if (refreshToken != null) {
            tokenStore.storeRefreshToken(refreshToken, authentication);
        }
        return accessToken;

    }

    //創(chuàng)建accessToken
    private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
        int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
        if (validitySeconds > 0) {
            token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
        }
        token.setRefreshToken(refreshToken);
        token.setScope(authentication.getOAuth2Request().getScope());

        return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
    }

    //創(chuàng)建refreshToken
    private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) {
        if (!isSupportRefreshToken(authentication.getOAuth2Request())) {
            return null;
        }
        int validitySeconds = getRefreshTokenValiditySeconds(authentication.getOAuth2Request());
        String value = UUID.randomUUID().toString();
        if (validitySeconds > 0) {
            return new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis()
                    + (validitySeconds * 1000L)));
        }
        return new DefaultOAuth2RefreshToken(value);
    }

這個tokenStore具體是哪個實現類的對象合冀,還要看我們在認證服務器(即繼承了AuthorizationServerConfigurerAdapter類)各薇,如果是Jwt,則直接返回null君躺,重新創(chuàng)建token峭判;如果是其他,則會獲取該用戶緩存的token并返回棕叫,不會創(chuàng)建新的token林螃。

//JwtTokenStore的getAccessToken方法
    @Override
    public OAuth2AccessToken getAccessToken(OAuth2Authentication authentication) {
        // We don't want to accidentally issue a token, and we have no way to reconstruct the refresh token
        return null;
    }

在DefaultTokenServices的createAccessToken方法中創(chuàng)建DefaultOAuth2AccessToken 并將expiration、refreshToken俺泣、scopes等信息存儲到其中疗认,得到如下token:

21580557-9cd5c457adbf8c4e.png

在最后會通過accessTokenEnhancer的enhance方法對該token進行強化

public class TokenEnhancerChain implements TokenEnhancer {

    private List<TokenEnhancer> delegates = Collections.emptyList();

    /**
     * @param delegates the delegates to set
     */
    public void setTokenEnhancers(List<TokenEnhancer> delegates) {
        this.delegates = delegates;
    }

    /**
     * Loop over the {@link #setTokenEnhancers(List) delegates} passing the result into the next member of the chain.
     */
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        OAuth2AccessToken result = accessToken;
        for (TokenEnhancer enhancer : delegates) {
            result = enhancer.enhance(result, authentication);
        }
        return result;
    }
}

此處同驗證Token令牌時遍歷List<AuthenticationProvider>尋找合適的AuthenticationProvider和生成Token令牌時遍歷List<AuthorizationCodeTokenGranter>尋找合適的AuthorizationCodeTokenGranter眠蚂。此處也會遍歷List<TokenEnhancer>尋找合適的 TokenEnhancer 赤兴,并調用enhance方法對OAuth2AccessToken 進行加強。此處List<TokenEnhancer>只有一個元素盼忌,即JwtAccessTokenConverter贝润。enhance方法添加jti(將value作為jti)、更改了value和refreshToken铝宵。

//JwtAccessTokenConverter類的enhance方法
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
        Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
        String tokenId = result.getValue();
        if (!info.containsKey(TOKEN_ID)) {
            info.put(TOKEN_ID, tokenId);
        }
        else {
            tokenId = (String) info.get(TOKEN_ID);
        }
        result.setAdditionalInformation(info);
        result.setValue(encode(result, authentication));
        OAuth2RefreshToken refreshToken = result.getRefreshToken();
        if (refreshToken != null) {
            DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
            encodedRefreshToken.setValue(refreshToken.getValue());
            // Refresh tokens do not expire unless explicitly of the right type
            encodedRefreshToken.setExpiration(null);
            try {
                Map<String, Object> claims = objectMapper
                        .parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
                if (claims.containsKey(TOKEN_ID)) {
                    encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
                }
            }
            catch (IllegalArgumentException e) {
            }
            Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
                    accessToken.getAdditionalInformation());
            refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
            refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
            encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
            DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
                    encode(encodedRefreshToken, authentication));
            if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
                Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
                encodedRefreshToken.setExpiration(expiration);
                token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
            }
            result.setRefreshToken(token);
        }
        return result;
    }

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

通過DefaultAccessTokenConverter的convertAccessToken將token的value轉換為jwt格式的token打掘。將USERNAME华畏、AUTHORITIES、SCOPE尊蚁、JTI亡笑、EXP、CLIENT_IDGRANT_TYPE横朋、AUD(resourceId)放入Map中仑乌。

//DefaultAccessTokenConverter的convertAccessToken方法
    public Map<String, ?> convertAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) {
        Map<String, Object> response = new HashMap<String, Object>();
        OAuth2Request clientToken = authentication.getOAuth2Request();

        if (!authentication.isClientOnly()) {
            response.putAll(userTokenConverter.convertUserAuthentication(authentication.getUserAuthentication()));
        } else {
            if (clientToken.getAuthorities()!=null && !clientToken.getAuthorities().isEmpty()) {
                response.put(UserAuthenticationConverter.AUTHORITIES,
                             AuthorityUtils.authorityListToSet(clientToken.getAuthorities()));
            }
        }

        if (token.getScope()!=null) {
            response.put(scopeAttribute, token.getScope());
        }
        if (token.getAdditionalInformation().containsKey(JTI)) {
            response.put(JTI, token.getAdditionalInformation().get(JTI));
        }

        if (token.getExpiration() != null) {
            response.put(EXP, token.getExpiration().getTime() / 1000);
        }
        
        if (includeGrantType && authentication.getOAuth2Request().getGrantType()!=null) {
            response.put(GRANT_TYPE, authentication.getOAuth2Request().getGrantType());
        }

        response.putAll(token.getAdditionalInformation());

        response.put(clientIdAttribute, clientToken.getClientId());
        if (clientToken.getResourceIds() != null && !clientToken.getResourceIds().isEmpty()) {
            response.put(AUD, clientToken.getResourceIds());
        }
        return response;
    }

通過JwtHelper的encode方法將content中的內容進行編碼,先創(chuàng)建JwtHeader header = {"alg":"RS256","typ":"JWT"}琴锭,對header和content用"."進行組合晰甚,然后用base64加密,再使用秘鑰進行簽名决帖。最后將header厕九,content和crypto作為參數創(chuàng)建JwtImpl對象返回,在bytes()方法中分別將header地回,content和crypto通過base64編碼后用"."進行連接扁远。最終得到token的value值。refreshToken生成方式相同(會多一個ati刻像,ati是accessToken的jti的值)畅买,只是程序中會先生成一個沒有exp的refreshToken,然后從原refreshToken中獲取過期時間后重新生成帶有exp的refreshToken细睡。

public class JwtHelper {
    static byte[] PERIOD = utf8Encode(".");

    public static Jwt encode(CharSequence content, Signer signer) {
        return encode(content, signer, Collections.<String, String>emptyMap());
    }

    public static Jwt encode(CharSequence content, Signer signer,
            Map<String, String> headers) {
        JwtHeader header = JwtHeaderHelper.create(signer, headers);
        byte[] claims = utf8Encode(content);
        byte[] crypto = signer
                .sign(concat(b64UrlEncode(header.bytes()), PERIOD, b64UrlEncode(claims)));
        return new JwtImpl(header, claims, crypto);
    }

}

class JwtImpl implements Jwt {
    final JwtHeader header;

    private final byte[] content;

    private final byte[] crypto;

    private String claims;

    JwtImpl(JwtHeader header, byte[] content, byte[] crypto) {
        this.header = header;
        this.content = content;
        this.crypto = crypto;
        claims = utf8Decode(content);
    }

    @Override
    public byte[] bytes() {
        return concat(b64UrlEncode(header.bytes()), JwtHelper.PERIOD,
                b64UrlEncode(content), JwtHelper.PERIOD, b64UrlEncode(crypto));
    }

    @Override
    public String getEncoded() {
        return utf8Decode(bytes());
    }
}

最終得到的token如下

21580557-90d99d11931bd2ad.png

freshToken相比accessToken除了jti不同谷羞,相比多了"ati":"3463f614-d84d-431b-bc08-dac0c29d9417",該ati就是accessToken的jti的值纹冤。

accessToken:
{"alg":"RS256","typ":"JWT"}{"aud":["res1"],"user_name":"1000","scope":["ROLE_ADMIN"],"exp":1600068422,"authorities":["hifun"],"jti":"a891bd48-5828-4572-bbc0-5c1f0c0449ba","client_id":"c1"}
refreshToken:
{"alg":"RS256","typ":"JWT"}{"aud":["res1"],"user_name":"1000","scope":["ROLE_ADMIN"],"ati":"a891bd48-5828-4572-bbc0-5c1f0c0449ba","exp":1600082822,"authorities":["hifun"],"jti":"341c06ce-5027-4304-961b-a97a9d1364ec","client_id":"c1"}

最最后在TokenEndpoint類中調用getResponse方法將OAuth2AccessToken 設置到返回參數中:

//TokenEndpoint類的getResponse方法
    private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control", "no-store");
        headers.set("Pragma", "no-cache");
        headers.set("Content-Type", "application/json;charset=UTF-8");
        return new ResponseEntity<OAuth2AccessToken>(accessToken, headers, HttpStatus.OK);
    }

③驗證token流程

攜帶token訪問資源洒宝,需要對token進行驗證。

21580557-63a59f0c9f8b78ff.png

流程概述:
1.從request中獲取token萌京,調用authenticate方法進行驗證雁歌。
2.對token進行解析、驗證簽名是否有效知残、是否過期等信息靠瞎,從token中獲取用戶和客戶端信息。
3.通過從token中獲取的信息創(chuàng)建OAuth2Request 和 已認證的UsernamePasswordAuthenticationToken求妹,將這兩個作為參數創(chuàng)建OAuth2Authentication乏盐。
4.判斷客戶端是否有訪問資源的權限(判斷OAuth2Authentication中的ResourceIds是否包含配置類中的ResourceId),然后將OAuth2Authentication設置為已認證(將authenticated屬性設為true)制恍。
5.將OAuth2Authentication設置到安全上下文中父能。完成校驗,后續(xù)過濾器放行净神。
6.判斷是否有權限

進入到OAuth2AuthenticationProcessingFilter過濾器的doFilter方法何吝,從HttpServletRequest中獲取Authorization或access_token(從請求頭獲取Authentication:Bearer xxxxxxxx--xxx溉委,如果為null,則從請求參數獲取access_token=xxxx-xxxx-xxxx)爱榕,拼接成PreAuthenticatedAuthenticationToken(Authentication子類)瓣喊。包含了獲取的accessToken的value,未經過認證黔酥。
21580557-9c6015dc6fc5ba74.png

將PreAuthenticatedAuthenticationToken強轉為AbstractAuthenticationToken并設置Details藻三。
21580557-203209e69b1b5779.png

然后通過OAuth2AuthenticationManager的authenticate方法對該AbstractAuthenticationToken進行認證。代碼如下跪者。
//OAuth2AuthenticationProcessingFilter的doFilter方法
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,ServletException {

        final boolean debug = logger.isDebugEnabled();
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        try {

            Authentication authentication = tokenExtractor.extract(request);
            
            if (authentication == null) {
                if (stateless && isAuthenticated()) {
                    if (debug) {
                        logger.debug("Clearing security context.");
                    }
                    SecurityContextHolder.clearContext();
                }
                if (debug) {
                    logger.debug("No token in request, will continue chain.");
                }
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
                if (authentication instanceof AbstractAuthenticationToken) {
                    AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                    needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
                }
                Authentication authResult = authenticationManager.authenticate(authentication);

                if (debug) {
                    logger.debug("Authentication success: " + authResult);
                }

                eventPublisher.publishAuthenticationSuccess(authResult);
                SecurityContextHolder.getContext().setAuthentication(authResult);

            }
        }
        catch (OAuth2Exception failed) {
            SecurityContextHolder.clearContext();

            if (debug) {
                logger.debug("Authentication request failed: " + failed);
            }
            eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                    new PreAuthenticatedAuthenticationToken("access-token", "N/A"));

            authenticationEntryPoint.commence(request, response,
                    new InsufficientAuthenticationException(failed.getMessage(), failed));

            return;
        }

        chain.doFilter(request, response);
    }

BearerTokenExtractor的extract方法從參數中獲取PreAuthenticatedAuthenticationToken棵帽。

public class BearerTokenExtractor implements TokenExtractor {

    private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);

    //從HttpServletRequest中獲取access_token
    @Override
    public Authentication extract(HttpServletRequest request) {
        String tokenValue = extractToken(request);
        if (tokenValue != null) {
            PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
            return authentication;
        }
        return null;
    }

    //從請求參數中獲取access_token=xxxx-xxxx-xxxx,并在請求頭中添加token類型坑夯;
    protected String extractToken(HttpServletRequest request) {
        // first check the header...
        String token = extractHeaderToken(request);

        // bearer type allows a request parameter as well
        if (token == null) {
            logger.debug("Token not found in headers. Trying request parameters.");
            token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
            if (token == null) {
                logger.debug("Token not found in request parameters.  Not an OAuth2 request.");
            }
            else {
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
            }
        }

        return token;
    }

    //從請求頭中獲取Authentication:Bearer xxxxxxxx--xxx岖寞,并在請求頭中添加token類型。
    protected String extractHeaderToken(HttpServletRequest request) {
        Enumeration<String> headers = request.getHeaders("Authorization");
        while (headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
            String value = headers.nextElement();
            if ((value.toLowerCase().startsWith(OAuth2AccessToken.BEARER_TYPE.toLowerCase()))) {
                String authHeaderValue = value.substring(OAuth2AccessToken.BEARER_TYPE.length()).trim();
                // Add this here for the auth details later. Would be better to change the signature of this method.
                request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE,
                        value.substring(0, OAuth2AccessToken.BEARER_TYPE.length()).trim());
                int commaIndex = authHeaderValue.indexOf(',');
                if (commaIndex > 0) {
                    authHeaderValue = authHeaderValue.substring(0, commaIndex);
                }
                return authHeaderValue;
            }
        }

        return null;
    }
}

與獲取驗證碼或獲取accessToken調用的autenticate方法不同柜蜈,此處不是驗證賬號密碼而是直接驗證token是否有效仗谆。此處調用了OAuth2AuthenticationManager 類的authenticate方法進行驗證。
對PreAuthenticatedAuthenticationToken中的token進行解碼淑履、簽名驗證隶垮,返回得到OAuth2Authentication,然后對設置的RESOURCE_ID進行判斷秘噪,設置如下狸吞。

    @Configuration
    @EnableResourceServer
    public class OrderServerConfig extends ResourceServerConfigurerAdapter {
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId(RESOURCE_ID)
                    .tokenStore(tokenStore)
                    .stateless(true);
        }

        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
//                    .antMatchers("/order/**").access("#oauth2.hasScope('ROLE_ADMIN')");
            .antMatchers("/order/**").permitAll();
        }
    }

進入OAuth2AuthenticationManager 類的authenticate方法。通過DefaultTokenServices 類的loadAuthentication方法獲取OAuth2Authentication指煎,如果OAuth2Authentication的resourceIds中不包含設置的RESOURCE_ID蹋偏,驗證失敗拋出異常。最后將OAuth2Authentication的authenticated屬性設為true表示已驗證完成至壤,并返回威始。

public class OAuth2AuthenticationManager implements AuthenticationManager, InitializingBean {
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {

        if (authentication == null) {
            throw new InvalidTokenException("Invalid token (token not found)");
        }
        String token = (String) authentication.getPrincipal();
        OAuth2Authentication auth = tokenServices.loadAuthentication(token);
        if (auth == null) {
            throw new InvalidTokenException("Invalid token: " + token);
        }

        Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
        if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
            throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
        }

        checkClientDetails(auth);   //該方法在此場景下相當于空方法,直接跳過像街。

        if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
            OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
            // Guard against a cached copy of the same details
            if (!details.equals(auth.getDetails())) {
                // Preserve the authentication details from the one loaded by token services
                details.setDecodedDetails(auth.getDetails());
            }
        }
        auth.setDetails(authentication.getDetails());
        auth.setAuthenticated(true);
        return auth;

    }
    // ...
}

此處和前面生成OAuth2AccessToken使用的是相同的類黎棠,包括DefaultTokenServices ,JwtTokenStore镰绎,jwtTokenEnhancer和JwtHelper脓斩。在如下DefaultTokenServices 類的loadAuthentication方法中,完成了對token的解析畴栖,簽名驗證随静,生成OAuth2Authentication 對象并判斷是否過期。然后通過readAuthentication方法通過OAuth2AccessToken對象獲取OAuth2Authentication對象吗讶。因為沒有在配置類中設置ClientDetailsService燎猛,所以不讀取數據庫直接返回OAuth2Authentication對象叼丑。

public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,ConsumerTokenServices, InitializingBean {
    public OAuth2Authentication loadAuthentication(String accessTokenValue) throws AuthenticationException,
            InvalidTokenException {
        OAuth2AccessToken accessToken = tokenStore.readAccessToken(accessTokenValue);
        if (accessToken == null) {
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        else if (accessToken.isExpired()) {
            tokenStore.removeAccessToken(accessToken);
            throw new InvalidTokenException("Access token expired: " + accessTokenValue);
        }

        OAuth2Authentication result = tokenStore.readAuthentication(accessToken);
        if (result == null) {
            // in case of race condition
            throw new InvalidTokenException("Invalid access token: " + accessTokenValue);
        }
        if (clientDetailsService != null) {   //未在配置類中設置clientDetailsService,不進入
            String clientId = result.getOAuth2Request().getClientId();
            try {
                clientDetailsService.loadClientByClientId(clientId);
            }
            catch (ClientRegistrationException e) {
                throw new InvalidTokenException("Client not valid: " + clientId, e);
            }
        }
        return result;
    }

    public OAuth2AccessToken extractAccessToken(String value, Map<String, ?> map) {
        DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(value);
        Map<String, Object> info = new HashMap<String, Object>(map);
        info.remove(EXP);
        info.remove(AUD);
        info.remove(clientIdAttribute);
        info.remove(scopeAttribute);
        if (map.containsKey(EXP)) {
            token.setExpiration(new Date((Long) map.get(EXP) * 1000L));
        }
        if (map.containsKey(JTI)) {
            info.put(JTI, map.get(JTI));
        }
        token.setScope(extractScope(map));
        token.setAdditionalInformation(info);
        return token;
    }
    // ...
}

loadAuthentication方法中通過readAccessToken方法獲取OAuth2AccessToken扛门,在readAccessToken中又調用了DefaultTokenServices類的extractAccessToken方法。在該方法中將token解析出來值()設置到新建的DefaultOAuth2AccessToken對象中纵寝,并返回论寨。返回的DefaultOAuth2AccessToken值如下
21580557-2e45927164538279.png

對DefaultOAuth2AccessToken進行校驗,判斷是否是refreshToken(如果包含ATI爽茴,則是refreshToken)葬凳,如果不是則返回DefaultOAuth2AccessToken。

public class JwtTokenStore implements TokenStore {
    @Override
    public OAuth2Authentication readAuthentication(OAuth2AccessToken token) {
        return readAuthentication(token.getValue());
    }

    @Override
    public OAuth2Authentication readAuthentication(String token) {
        return jwtTokenEnhancer.extractAuthentication(jwtTokenEnhancer.decode(token)); //調用了JwtAccessTokenConverter中的extractAuthentication方法
    }

    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        OAuth2AccessToken accessToken = convertAccessToken(tokenValue);
        if (jwtTokenEnhancer.isRefreshToken(accessToken)) {
            throw new InvalidTokenException("Encoded token is a refresh token");
        }
        return accessToken;
    }

    private OAuth2AccessToken convertAccessToken(String tokenValue) {
        return jwtTokenEnhancer.extractAccessToken(tokenValue, jwtTokenEnhancer.decode(tokenValue));  //調用了JwtAccessTokenConverter中的extractAccessToken方法
    }
}

JwtAccessTokenConverter類在生成OAuth2AccessToken時調用enhance方法添加jti(將value作為jti)室奏、更改了value和refreshToken火焰。而在驗證token時,該類的decode方法對token進行解碼胧沫,驗證簽名昌简,并返回一個Map包含了令牌中的客戶端和用戶信息。該Map用于后續(xù)生成OAuth2AccessToken和OAuth2Authentication 绒怨。

public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {
    protected Map<String, Object> decode(String token) {
        try {
            Jwt jwt = JwtHelper.decodeAndVerify(token, verifier);
            String claimsStr = jwt.getClaims();
            Map<String, Object> claims = objectMapper.parseMap(claimsStr);
            if (claims.containsKey(EXP) && claims.get(EXP) instanceof Integer) {
                Integer intValue = (Integer) claims.get(EXP);
                claims.put(EXP, new Long(intValue));
            }
            this.getJwtClaimsSetVerifier().verify(claims); //空方法纯赎,因為在decodeAndVerify已經完成簽名校驗
            return claims;
        }
        catch (Exception e) {
            throw new InvalidTokenException("Cannot convert access token to JSON", e);
        }
    }

    @Override
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        return tokenConverter.extractAuthentication(map);  //調用了DefaultAccessTokenConverter 類中的extractAuthentication方法
    }
    // ...
}

JwtHelper的decodeAndVerify方法對token進行解碼并使用公鑰驗證簽名是否有效,返回生成的Jwt對象南蹂。返回的Jwt對象和邏輯代碼如下犬金。
21580557-1317d4aa88d1ae36.png
public class JwtHelper {
    public static Jwt decodeAndVerify(String token, SignatureVerifier verifier) {
        Jwt jwt = decode(token);
        jwt.verifySignature(verifier);

        return jwt;
    }

    public static Jwt decode(String token) {
        int firstPeriod = token.indexOf('.');
        int lastPeriod = token.lastIndexOf('.');

        if (firstPeriod <= 0 || lastPeriod <= firstPeriod) {
            throw new IllegalArgumentException("JWT must have 3 tokens");
        }
        CharBuffer buffer = CharBuffer.wrap(token, 0, firstPeriod);
        // TODO: Use a Reader which supports CharBuffer
        JwtHeader header = JwtHeaderHelper.create(buffer.toString());

        buffer.limit(lastPeriod).position(firstPeriod + 1);
        byte[] claims = b64UrlDecode(buffer);
        boolean emptyCrypto = lastPeriod == token.length() - 1;

        byte[] crypto;

        if (emptyCrypto) {
            if (!"none".equals(header.parameters.alg)) {
                throw new IllegalArgumentException(
                        "Signed or encrypted token must have non-empty crypto segment");
            }
            crypto = new byte[0];
        }
        else {
            buffer.limit(token.length()).position(lastPeriod + 1);
            crypto = b64UrlDecode(buffer);
        }
        return new JwtImpl(header, claims, crypto);
    }
    // ...
}

DefaultAccessTokenConverter 類中的extractAuthentication方法將JwtAccessTokenConverter的decode方法返回的包含了令牌中的客戶端和用戶信息的Map作為參數

21580557-aea5cbdddf41f994.png

調用extractAuthentication方法返回已經認證的UsernamePasswordAuthenticationToken對象,然后將該對象與Map中的用戶信息作為參數構建OAuth2Authentication 對象并返回六剥。
21580557-995c4ee3be6c47af.png
public class DefaultAccessTokenConverter implements AccessTokenConverter {
    public OAuth2Authentication extractAuthentication(Map<String, ?> map) {
        Map<String, String> parameters = new HashMap<String, String>();
        Set<String> scope = extractScope(map);
        Authentication user = userTokenConverter.extractAuthentication(map);
        String clientId = (String) map.get(clientIdAttribute);
        parameters.put(clientIdAttribute, clientId);
        if (includeGrantType && map.containsKey(GRANT_TYPE)) {
            parameters.put(GRANT_TYPE, (String) map.get(GRANT_TYPE));
        }
        Set<String> resourceIds = new LinkedHashSet<String>(map.containsKey(AUD) ? getAudience(map)
                : Collections.<String>emptySet());
        
        Collection<? extends GrantedAuthority> authorities = null;
        if (user==null && map.containsKey(AUTHORITIES)) {
            @SuppressWarnings("unchecked")
            String[] roles = ((Collection<String>)map.get(AUTHORITIES)).toArray(new String[0]);
            authorities = AuthorityUtils.createAuthorityList(roles);
        }
        OAuth2Request request = new OAuth2Request(parameters, clientId, authorities, true, scope, resourceIds, null, null,
                null);
        return new OAuth2Authentication(request, user);
    }
    // ...
}

在extractAuthentication方法中調用了DefaultUserAuthenticationConverter的extractAuthentication方法晚顷。在該方法中,因為我們沒有寫UserDetailsService的實現類疗疟,所以跳過去數據庫校驗的username的步驟该默。直接創(chuàng)建已經認證的UsernamePasswordAuthenticationToken對象并返回。

public class DefaultUserAuthenticationConverter implements UserAuthenticationConverter {
    public Authentication extractAuthentication(Map<String, ?> map) {
        if (map.containsKey(USERNAME)) {
            Object principal = map.get(USERNAME);
            Collection<? extends GrantedAuthority> authorities = getAuthorities(map);
            if (userDetailsService != null) {
                UserDetails user = userDetailsService.loadUserByUsername((String) map.get(USERNAME));
                authorities = user.getAuthorities();
                principal = user;
            }
            return new UsernamePasswordAuthenticationToken(principal, "N/A", authorities);
        }
        return null;
    }
}


1.2.3 刷新token流程

待續(xù)......

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
禁止轉載秃嗜,如需轉載請通過簡信或評論聯系作者权均。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市锅锨,隨后出現的幾起案子叽赊,更是在濱河造成了極大的恐慌,老刑警劉巖必搞,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件必指,死亡現場離奇詭異,居然都是意外死亡恕洲,警方通過查閱死者的電腦和手機塔橡,發(fā)現死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門梅割,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人葛家,你說我怎么就攤上這事户辞。” “怎么了癞谒?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵底燎,是天一觀的道長。 經常有香客問我弹砚,道長双仍,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任桌吃,我火速辦了婚禮朱沃,結果婚禮上,老公的妹妹穿的比我還像新娘茅诱。我一直安慰自己逗物,他們只是感情好,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布瑟俭。 她就那樣靜靜地躺著敬察,像睡著了一般。 火紅的嫁衣襯著肌膚如雪尔当。 梳的紋絲不亂的頭發(fā)上莲祸,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天,我揣著相機與錄音椭迎,去河邊找鬼锐帜。 笑死,一個胖子當著我的面吹牛畜号,可吹牛的內容都是我干的缴阎。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼简软,長吁一口氣:“原來是場噩夢啊……” “哼蛮拔!你這毒婦竟也來了?” 一聲冷哼從身側響起痹升,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤建炫,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后疼蛾,有當地人在樹林里發(fā)現了一具尸體肛跌,經...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現自己被綠了衍慎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片转唉。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖稳捆,靈堂內的尸體忽然破棺而出赠法,到底是詐尸還是另有隱情,我是刑警寧澤乔夯,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布期虾,位于F島的核電站,受9級特大地震影響驯嘱,放射性物質發(fā)生泄漏。R本人自食惡果不足惜喳坠,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一鞠评、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧壕鹉,春花似錦剃幌、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至脊凰,卻和暖如春抖棘,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背狸涌。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工切省, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人帕胆。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓朝捆,卻偏偏與公主長得像,于是被迫代替她去往敵國和親懒豹。 傳聞我的和親對象是個殘疾皇子芙盘,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355

推薦閱讀更多精彩內容