[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

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