上文地址: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信息

携带client信息

2、自定义异常返回格式
OAuth2自带的异常返回格式是:
 {
     "error": "invalid_grant",
     "error_description": "Bad credentials"
 }
这个格式对前端来说不是很友好,我们期望的格式是:
{
   "code":401,
   "msg":"msg"
}
下面是具体实现:
新建MyOAuth2WebResponseExceptionTranslator实现 WebResponseExceptionTranslator接口 重写ResponseEntity
/**
 * @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);  // 自定义异常返回
    }
演示效果:

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); // 无权处理器

因为我在请求上增加了注解权限只能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 {
             HttpUtilsResultVO.writerError(new ResponseVo(401, authException.getMessage()), response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
在 资源配置中ResourceServerConfigurerAdapter中注入:
@Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.authenticationEntryPoint(tokenExceptionEntryPoint); // token失效处理器
        resources.resourceId("auth"); // 设置资源id  通过client的 scope 来判断是否具有资源权限
    }
展示效果:

 
  
  
  
 