[8] RequestCacheAwareFilter

RequestCacheAwareFilter

介紹

這個Filter官方解釋爲:“用於用戶登錄成功後,重新恢復因爲登錄被打斷的請求”,被打斷也是有前提條件的,支持打斷後可以被恢復的異常有AuthenticationException、AccessDeniedException,這個操作是ExceptionTranslationFilter中觸發的,並且RequestCacheAwareFilter只支持GET方法,而默認TokenEndpoint支持Post獲取Token信息,進行登錄,我們帶着這些問題去分析代碼。

代碼分析

步驟1

RequestCacheAwareFilter從requestCache命中緩存是有一定的機制的,如果盲目去觸發接口,可能發現requestCache永遠都不能生效。RequestCacheAwareFilter中注入了一個RequestCache,它的實現也有2中方式,分別是HttpSessionRequestCache和NullRequestCache,默認是HttpSessionRequestCache,也就不用考慮如何注入HttpSessionRequestCache的問題了。要想命中緩存,必須先寫入緩存,RequestCache#saveRequest()就是執行寫入緩存請求的操作。很容易發現,saveRequest()僅在ExceptionTranslationFilter#sendStartAuthentication()有調用,並且sendStartAuthentication()又都是在handleSpringSecurityException()調用的。僅在異常屬於AuthenticationException或AccessDeniedException纔會調用sendStartAuthentication(),而這2種異常往往是在授權認證服務認證失敗時拋出的異常,只要能觸發這兩種異常,requestCache就能會將請求信息寫入緩存,筆者發現繼承AuthenticationException還挺多的,以InsufficientAuthenticationException(其他異常筆者發現諸如LockedException等會被轉成非AuthenticationException)爲例,可以自定一個UserDetailsService重寫loadUserByUsername方法,當用戶不存在時拋出一個InsufficientAuthenticationException即可,代碼以及截圖如下:

//RequestCacheAwareFilter
public void doFilter(ServletRequest request, ServletResponse response,
        FilterChain chain) throws IOException, ServletException {
	//從HttpSessionRequestCache中取一下緩存
    HttpServletRequest wrappedSavedRequest = requestCache.getMatchingRequest(
            (HttpServletRequest) request, (HttpServletResponse) response);

    chain.doFilter(wrappedSavedRequest == null ? request : wrappedSavedRequest,
            response);
}
public void saveRequest(HttpServletRequest request, HttpServletResponse response) {
    //這裏requestMatcher 匹配的是["/**",GET], 對於POST登錄請求無法匹配
    if (requestMatcher.matches(request)) {
        DefaultSavedRequest savedRequest = new DefaultSavedRequest(request,
                portResolver);

        if (createSessionAllowed || request.getSession(false) != null) {
            request.getSession().setAttribute(this.sessionAttrName, savedRequest);
            logger.debug("DefaultSavedRequest added to Session: " + savedRequest);
        }
    }
}
private void handleSpringSecurityException(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain, RuntimeException exception)
        throws IOException, ServletException {
    if (exception instanceof AuthenticationException) {
        //遇到AuthenticationException[身份認證異常]緩存請求
        sendStartAuthentication(request, response, chain,
                (AuthenticationException) exception);
    }
    else if (exception instanceof AccessDeniedException) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authenticationTrustResolver.isAnonymous(authentication) || authenticationTrustResolver.isRememberMe(authentication)) {
            //遇到AccessDeniedException[訪問權限受限異常],並且認證信息時匿名的或者認證信息屬於"記住我"身份認證緩存請求
            sendStartAuthentication(
                    request,
                    response,
                    chain,
                    new InsufficientAuthenticationException(
                        messages.getMessage(
                            "ExceptionTranslationFilter.insufficientAuthentication",
                            "Full authentication is required to access this resource")));
        }
        else {
            accessDeniedHandler.handle(request, response,
                    (AccessDeniedException) exception);
        }
    }
}

protected void sendStartAuthentication(HttpServletRequest request,
        HttpServletResponse response, FilterChain chain,
        AuthenticationException reason) throws ServletException, IOException {
    SecurityContextHolder.getContext().setAuthentication(null);
    //緩存請求,把請求信息放到Session中
    requestCache.saveRequest(request, response);
    //處理異常
    authenticationEntryPoint.commence(request, response, reason);
}

image.png

@Override
public Oauth2User loadUserByUsername(String username) throws UsernameNotFoundException {
    final String key = RedisKey.userDetails(username);
    RBucket<Oauth2User> bucket = redissonClient.getBucket(key);
    if (!bucket.isExists()) {
        R<UserRetDTO> ret;
        try {
            ret = userFeignService.match(username, CryptoUtil.sign(60L));
        } catch (Throwable e) {
            throw new AccessDeniedException("請求用戶信息失敗", e);
        }
        //拋出InsufficientAuthenticationException就能被ExceptionTranslationFilter處理
        if (!R.isRequestSuccessCanNotNullData(ret)) {
            throw new InsufficientAuthenticationException("用戶不存在");
        }
        Oauth2User user = this.toCarpUser(ret.getData());
        if (user != null) {
            bucket.set(user);
            bucket.expire(carpAuthClientProperties.getAccessTokenValiditySeconds().longValue(), TimeUnit.SECONDS);
        }
        return user;
    }
    return bucket.get();
}

步驟2

經過上一步後,我發送一個獲取token的請求,發現仍然無法命中緩存,關鍵在這行代碼saveRequest()方法的requestMatcher.matches(request),此處requestMatcher只匹配get請求。默認的TokenEndpoint的請求token的請求只能是POST方式,我們需要進一步改造,通過bean的後置處理改變TokenEndpoint的允許的請求方式就可以了。筆者這裏自定義一個TokenEndpoint,請求Token信息的url不能Spring Security重複,不然項目可能都無法啓動,而且在配置類需要配置現請求路徑與原有請求路徑關係纔行,這個系列文章FrameworkEndpointHandlerMapping會做講解,這裏不在贅述,自定義TokenEndpoint也不是必須的,只是這裏提到了,加上一些知識點。截圖以及自定義TokenEndpoint代碼如下:
image.png

@RestController
@RequestMapping("/oauth2")
public class CarpTokenEndpoint implements InitializingBean {

    @Autowired
    private TokenEndpoint tokenEndpoint;

    @ApiOperation(value = "登錄接口", httpMethod = "GET")
    @RequestMapping(value = "/token/access", method = RequestMethod.GET)
    public R<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam
        Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        ResponseEntity<OAuth2AccessToken> ret = tokenEndpoint.getAccessToken(principal, parameters);

        return R.success(ret.getBody());
    }

    @ApiOperation(value = "登錄接口", httpMethod = "POST")
    @RequestMapping(value = "/token/access", method = RequestMethod.POST)
    public R<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
        Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
        ResponseEntity<OAuth2AccessToken> ret = tokenEndpoint.postAccessToken(principal, parameters);

        return R.success(ret.getBody());
    }

	...
    //使其支持GET方法
    @Override
    public void afterPropertiesSet() throws Exception {
        tokenEndpoint.setAllowedRequestMethods(new HashSet<>(
            Arrays.asList(HttpMethod.POST, HttpMethod.GET)));
    }
}

步驟3

我們可以藉助Postman發送一個獲取token的請求,故意輸錯用戶名,第一次請求時遇到異常,請求信息會被寫入緩存,再次請求時,我們發現緩存中已經可以命中,取出信息如下:
image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章