Spring Security OAuth2(2) requests carry client information verification, custom exception returns, no right to process, token invalidation processing

The above address: SpringSecurityOAuth2(1) (password,authorization_code,refresh_token,client_credentials) to get token

The last blog wrote a simple OAuth2 token authentication server, which only implements 4 ways to obtain tokens. For exception handling, unauthorized handling, data integrity verification before token generation, etc. are not involved, the This article makes some additions to these contents:

GitHub address

Code cloud address

OAUth2's authentication adapter AuthorizationServerConfigurerAdapter has three main methods:

  1. AuthorizationServerSecurityConfigurer:

    Configuring Security Constraints for Token Endpoints

  2. ClientDetailsServiceConfigurer:

    Configure the client details service, the client details are initialized here

  3. AuthorizationServerEndpointsConfigurer:

    Configure authorization and token access endpoints and token services

1. Complete verification of client information before requesting

For requests with incomplete data, it can be directly returned to the front end, without the need for subsequent verification. Client information is generally encoded in Base64 and placed in Authorization. For example, before encoding, it is

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

Create a new ClientDetailsAuthenticationFilter to inherit 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;
    }
}

Then add the filter chain to AuthorizationServerSecurityConfigurer

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

Verify the effect:

Does not carry client information

Carry client information

2. Custom exception return format

The exception return format that comes with OAuth2 is:

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

This format is not very friendly to the front end, the format we expect is:

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

The following is the specific implementation:

Create a new MyOAuth2WebResponseExceptionTranslator to implement the WebResponseExceptionTranslator interface and rewrite the ResponseEntity<Oauth2Exception> translate(Exception e) method; the exception sent by authentication is captured here, the exception that occurs in authentication can be captured here, and here we can encapsulate our exception information into a unified format Just return, how to deal with it varies from project to project, here I directly copied the DefaultWebResponseExceptionTranslator implementation method

/**
 * @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;
        }

    }
}

Define your own OAuth2Exception format 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);

    }
}

The serializer class MyOAuth2ExceptionJacksonSerializer that defines the exception's MyOAuth2Exception

/**  
* @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();
    }
}

Add the defined exception handling to the AuthorizationServerEndpointsConfigurer configuration of the authorization configuration

    /**
     * 配置授权(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);  // 自定义异常返回
    }

Demonstration effect:

3. Custom does not have permission to access the processor

The default no-access return format is:

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

The format we expect is:

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

Create a new MyAccessDeniedHandler to implement AccessDeniedHandler and customize the return information:

/**
 * @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);
    }
}

Added in ResourceServerConfigurerAdapter resource configuration

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

Because I added the annotation permission to the request, it can only be accessed by the ROLE_USER user, and then I am logged in as the ROLE_ADMIN user, so I have no right to process it.

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

4. Custom token invalid handler

The default token invalid return information is:

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

The format we expect is:

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

Create a new MyTokenExceptionEntryPoint to implement 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();
        }
    }
}

Inject in ResourceServerConfigurerAdapter in resource configuration:

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

Display of results:

{{o.name}}
{{m.name}}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=324029829&siteId=291194637