SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

上文地址:SpringSecurityOAuth2(1)(password,authorization_code,refresh_token,client_credentials)获取token

上一篇博客写了一个至简的OAuth2的token认证服务器,只实现了4种获取token的方式 ,对于异常处理,以及无权处理,生成token前的数据完整性校验等等没有涉及,该篇文章对于这些内容做一些补充:

本文源码地址

OAUth2的认证适配器AuthorizationServerConfigurerAdapter有三个主要的方法:

    AuthorizationServerSecurityConfigurer:

    配置令牌端点(Token Endpoint)的安全约束

    ClientDetailsServiceConfigurer:

    配置客户端详细服务, 客户端的详情在这里进行初始化

    AuthorizationServerEndpointsConfigurer:

    配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)

1、请求前客户端信息完整校验

对于携带数据不完整的请求,可以直接返回给前端,不需要经过后面的验证 client信息一般以Base64编码放在Authorization 中 例如编码前为

client_name:111  (client_id:client_secret Base64编码) 
Basic Y2xpZW50X25hbWU6MTEx

新建一个ClientDetailsAuthenticationFilter继承OncePerRequestFilter

/**
 * @Description 客户端不带完整client处理
 * @Author wwz
 * @Date 2019/07/30
 * @Param
 * @Return
 */
@Component
public class ClientDetailsAuthenticationFilter extends OncePerRequestFilter {

    private ClientDetailsService clientDetailsService;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 只有获取token的时候需要携带携带客户端信息,放过其他
        if (!request.getRequestURI().equals("/oauth/token")) {
            filterChain.doFilter(request, response);
            return;
        }
        String[] clientDetails = this.isHasClientDetails(request);

        if (clientDetails == null) {
            ResponseVo resultVo = new ResponseVo(HttpStatus.UNAUTHORIZED.value(), "请求中未包含客户端信息");
            HttpUtilsResultVO.writerError(resultVo, response);
            return;
        }
        this.handle(request, response, clientDetails, filterChain);
    }
    private void handle(HttpServletRequest request, HttpServletResponse response, String[] clientDetails, FilterChain filterChain) throws IOException, ServletException {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

        if (authentication != null && authentication.isAuthenticated()) {
            filterChain.doFilter(request, response);
            return;
        }


        MyClientDetails details = (MyClientDetails) this.getClientDetailsService().loadClientByClientId(clientDetails[0]);
        UsernamePasswordAuthenticationToken token =
                new UsernamePasswordAuthenticationToken(details.getClientId(), details.getClientSecret(), details.getAuthorities());

        SecurityContextHolder.getContext().setAuthentication(token);


        filterChain.doFilter(request, response);
    }
    /**
     * 判断请求头中是否包含client信息,不包含返回null  Base64编码
     */
    private String[] isHasClientDetails(HttpServletRequest request) {

        String[] params = null;

        String header = request.getHeader(HttpHeaders.AUTHORIZATION);

        if (header != null) {

            String basic = header.substring(0, 5);

            if (basic.toLowerCase().contains("basic")) {

                String tmp = header.substring(6);
                String defaultClientDetails = new String(Base64.getDecoder().decode(tmp));

                String[] clientArrays = defaultClientDetails.split(":");

                if (clientArrays.length != 2) {
                    return params;
                } else {
                    params = clientArrays;
                }

            }
        }
        String id = request.getParameter("client_id");
        String secret = request.getParameter("client_secret");

        if (header == null && id != null) {
            params = new String[]{id, secret};
        }
        return params;
    }
    public ClientDetailsService getClientDetailsService() {
        return clientDetailsService;
    }

    public void setClientDetailsService(ClientDetailsService clientDetailsService) {
        this.clientDetailsService = clientDetailsService;
    }
}

然后在AuthorizationServerSecurityConfigurer中加入过滤链

   /**
     * 配置令牌端点(Token Endpoint)的安全约束
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        // 加载client的 获取接口
        clientDetailsAuthenticationFilter.setClientDetailsService(clientDetailsService);
        // 客户端认证之前的过滤器
        oauthServer.addTokenEndpointAuthenticationFilter(clientDetailsAuthenticationFilter);
        oauthServer
                .tokenKeyAccess("permitAll()")
                .checkTokenAccess("isAuthenticated()")
                .allowFormAuthenticationForClients();   // 允许表单登录
    }

验证效果:

未携带client信息

SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

携带client信息

SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

2、自定义异常返回格式

OAuth2自带的异常返回格式是:

 {
 	"error": "invalid_grant",
 	"error_description": "Bad credentials"
 }

这个格式对前端来说不是很友好,我们期望的格式是:

{
   "code":401,
   "msg":"msg"
}

下面是具体实现:

新建MyOAuth2WebResponseExceptionTranslator实现 WebResponseExceptionTranslator接口 重写ResponseEntity<Oauth2Exception> translate(Exception e)方法; 认证发送的异常在这里捕获,认证发生的异常在这里能捕获到,在这里我们可以将我们的异常信息封装成统一的格式返回即可,这里怎么处理因项目而异,这里我直接复制了DefaultWebResponseExceptionTranslator 实现方法

/**
 * @Description WebResponseExceptionTranslator
 * @Author wwz
 * @Date 2019/07/30
 * @Param
 * @Return
 */
@Component
public class MyOAuth2WebResponseExceptionTranslator implements WebResponseExceptionTranslator<OAuth2Exception> {

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    @Override
    public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {

        // Try to extract a SpringSecurityException from the stacktrace
        Throwable[] causeChain = throwableAnalyzer.determineCauseChain(e);

        // 异常栈获取 OAuth2Exception 异常
        Exception ase = (OAuth2Exception) throwableAnalyzer.getFirstThrowableOfType(
                OAuth2Exception.class, causeChain);

        // 异常栈中有OAuth2Exception
        if (ase != null) {
            return handleOAuth2Exception((OAuth2Exception) ase);
        }
        ase = (AuthenticationException) throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class,
                causeChain);
        if (ase != null) {
            return handleOAuth2Exception(new UnauthorizedException(e.getMessage(), e));
        }

        ase = (AccessDeniedException) throwableAnalyzer
                .getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        if (ase instanceof AccessDeniedException) {
            return handleOAuth2Exception(new ForbiddenException(ase.getMessage(), ase));
        }

        ase = (HttpRequestMethodNotSupportedException) throwableAnalyzer
                .getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
        if (ase instanceof HttpRequestMethodNotSupportedException) {
            return handleOAuth2Exception(new MethodNotAllowed(ase.getMessage(), ase));
        }

        // 不包含上述异常则服务器内部错误
        return handleOAuth2Exception(new ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
    }

    private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {

        int status = e.getHttpErrorCode();
        HttpHeaders headers = new HttpHeaders();
        headers.set("Cache-Control", "no-store");
        headers.set("Pragma", "no-cache");
        if (status == HttpStatus.UNAUTHORIZED.value() || (e instanceof InsufficientScopeException)) {
            headers.set("WWW-Authenticate", String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
        }

        MyOAuth2Exception exception = new MyOAuth2Exception(e.getMessage(), e);

        ResponseEntity<OAuth2Exception> response = new ResponseEntity<OAuth2Exception>(exception, headers,
                HttpStatus.valueOf(status));

        return response;

    }

    public void setThrowableAnalyzer(ThrowableAnalyzer throwableAnalyzer) {
        this.throwableAnalyzer = throwableAnalyzer;
    }

    @SuppressWarnings("serial")
    private static class ForbiddenException extends OAuth2Exception {

        public ForbiddenException(String msg, Throwable t) {
            super(msg, t);
        }

        public String getOAuth2ErrorCode() {
            return "access_denied";
        }

        public int getHttpErrorCode() {
            return 403;
        }

    }

    @SuppressWarnings("serial")
    private static class ServerErrorException extends OAuth2Exception {

        public ServerErrorException(String msg, Throwable t) {
            super(msg, t);
        }

        public String getOAuth2ErrorCode() {
            return "server_error";
        }

        public int getHttpErrorCode() {
            return 500;
        }

    }

    @SuppressWarnings("serial")
    private static class UnauthorizedException extends OAuth2Exception {

        public UnauthorizedException(String msg, Throwable t) {
            super(msg, t);
        }

        public String getOAuth2ErrorCode() {
            return "unauthorized";
        }

        public int getHttpErrorCode() {
            return 401;
        }

    }

    @SuppressWarnings("serial")
    private static class MethodNotAllowed extends OAuth2Exception {

        public MethodNotAllowed(String msg, Throwable t) {
            super(msg, t);
        }

        public String getOAuth2ErrorCode() {
            return "method_not_allowed";
        }

        public int getHttpErrorCode() {
            return 405;
        }

    }
}

定义自己的OAuth2Exception格式 MyOAuth2Exception

/**  
* @Description 异常格式
* @Author wwz
* @Date 2019/07/30
* @Param   
* @Return   
*/ 
@JsonSerialize(using = MyOAuthExceptionJacksonSerializer.class)
public class MyOAuth2Exception extends OAuth2Exception {
    public MyOAuth2Exception(String msg, Throwable t) {
        super(msg, t);

    }
    public MyOAuth2Exception(String msg) {
        super(msg);

    }
}

定义异常的MyOAuth2Exception的序列化类 MyOAuth2ExceptionJacksonSerializer

/**  
* @Description 定义异常MyOAuth2Exception的序列化
* @Author wwz
* @Date 2019/07/11 
* @Param   
* @Return   
*/ 
public class MyOAuthExceptionJacksonSerializer extends StdSerializer<MyOAuth2Exception> {

    protected MyOAuthExceptionJacksonSerializer() {
        super(MyOAuth2Exception.class);
    }

    @Override
    public void serialize(MyOAuth2Exception value, JsonGenerator jgen, SerializerProvider serializerProvider) throws IOException {
        jgen.writeStartObject();
        jgen.writeObjectField("code", value.getHttpErrorCode());
        jgen.writeStringField("msg", value.getSummary());
        jgen.writeEndObject();
    }
}

将定义好的异常处理 加入到授权配置的 AuthorizationServerEndpointsConfigurer配置中

    /**
     * 配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .tokenStore(tokenStore())  // 配置token存储
                .userDetailsService(userDetailsService)  // 配置自定义的用户权限数据,不配置会导致token无法刷新
                .authenticationManager(authenticationManager)
                .tokenServices(defaultTokenServices())// 加载token配置
                .exceptionTranslator(webResponseExceptionTranslator);  // 自定义异常返回
    }

演示效果:

SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

3、自定义无权访问处理器

默认的无权访问返回格式是:

{
    "error": "access_denied",
    "error_description": "不允许访问"
}

我们期望的格式是:

{
   "code":401,
   "msg":"msg"
}

新建一个MyAccessDeniedHandler 实现AccessDeniedHandler,自定义返回信息:

/**
 * @Description 无权访问处理器
 * @Author wwz
 * @Date 2019/07/30
 * @Param
 * @Return
 */
@Component
public class MyAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        ResponseVo resultVo = new ResponseVo();
        resultVo.setMessage("无权访问!");
        resultVo.setCode(403);
        HttpUtilsResultVO.writerError(resultVo, response);
    }
}

在ResourceServerConfigurerAdapter资源配置中增加

http.exceptionHandling().accessDeniedHandler(accessDeniedHandler); // 无权处理器

SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理

因为我在请求上增加了注解权限只能ROLE_USER用户访问,然后我登录的是ROLE_ADMIN用户,所以无权处理。

 @GetMapping("/hello")
    @PreAuthorize("hasRole('ROLE_USER')")
    public String hello(Principal principal) {
        return principal.getName() + " has hello Permission";
    }

4、自定义token无效处理器

默认的token无效返回信息是:

{
    "error": "invalid_token",
    "error_description": "Invalid access token: 78df4214-8e10-46ae-a85b-a8f5247370a"
}

我们期望的格式是:

{
   "code":403,
   "msg":"msg"
}

新建MyTokenExceptionEntryPoint 实现AuthenticationEntryPoint

/**
 * @Description 无效Token返回处理器
 * @Author wwz
 * @Date 2019/07/30
 * @Param
 * @Return
 */
@Component
public class MyTokenExceptionEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        Throwable cause = authException.getCause();
        response.setStatus(HttpStatus.OK.value());
        response.setHeader("Content-Type", "application/json;charset=UTF-8");
        try {
            if (cause instanceof InvalidTokenException) {
                HttpUtilsResultVO.writerError(new ResponseVo(405, "token失效"), response);
                //response.getWriter().write(JSONObject.toJSONString(new ResultVo(405, "token失效")));
            }else {
                HttpUtilsResultVO.writerError(new ResponseVo(405, "token缺失"), response);
//                response.getWriter().write(JSONObject.toJSONString(new ResultVo(405, "token缺失")));
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

在 资源配置中ResourceServerConfigurerAdapter中注入:

@Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(tokenExceptionEntryPoint); // token失效处理器
        resources.resourceId("auth"); // 设置资源id  通过client的 scope 来判断是否具有资源权限
    }

展示效果:

SpringSecurityOAuth2(2)请求携带客户端信息校验,自定义异常返回,无权处理,token失效处理