Oauth2.0 自定义响应值以及异常处理

自用的响应信息主体

import cn.hutool.http.ContentType;
import cn.hutool.http.HttpStatus;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;

import javax.servlet.http.HttpServletResponse;
import java.io.Serializable;
import java.nio.charset.Charset;


@Builder
@ToString
@NoArgsConstructor
@AllArgsConstructor
@Accessors(chain = true)
@ApiModel(value = "响应信息主体")
@JsonInclude(JsonInclude.Include.NON_NULL)
public class R<T> implements Serializable {

    private static final long serialVersionUID = 1L;
    private static final String SUCCESS = "SUCCESS";
    private static final String FAILED = "FAILED";

    @Getter
    @Setter
    @ApiModelProperty(value = "返回标记:成功标记=200,失败标记=500")
    private int code;

    @Getter
    @Setter
    @ApiModelProperty(value = "返回信息")
    private String msg;

    @Getter
    @Setter
    @ApiModelProperty(value = "数据")
    private T data;

    public static <T> R<T> ok() {
        return restResult(null, HttpStatus.HTTP_OK, SUCCESS, true);
    }

    public static <T> R<T> ok(T data) {
        return restResult(data, HttpStatus.HTTP_OK, SUCCESS, true);
    }

    public static <T> R<T> ok(T data, String msg) {
        return restResult(data, HttpStatus.HTTP_OK, msg, true);
    }

    public static <T> R<T> ok(ResultCode result) {
        return restResult(null, result.code, result.msg, true);
    }

    public static <T> R<T> fail() {
        return restResult(null, HttpStatus.HTTP_INTERNAL_ERROR, FAILED, false);
    }

    public static <T> R<T> fail(String msg) {
        return restResult(null, HttpStatus.HTTP_INTERNAL_ERROR, msg, false);
    }

    public static <T> R<T> fail(T data) {
        return restResult(data, HttpStatus.HTTP_INTERNAL_ERROR, FAILED, false);
    }

    public static <T> R<T> fail(ResultCode result) {
        return restResult(null, result.code, result.msg, false);
    }

    public static <T> R<T> fail(int code, String msg) {
        return restResult(null, code, msg, false);
    }

    private static <T> R<T> restResult(T data, int code, String msg, boolean success) {
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        return apiResult;
    }

    public static void failRender(int code, String msg, HttpServletResponse response, int status) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            response.setContentType(ContentType.build(ContentType.JSON.getValue(), Charset.defaultCharset()));
            response.setStatus(status);
            response.getWriter().write(mapper.writeValueAsString(R.fail(code, msg)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void failRender(ResultCode resultCode, HttpServletResponse response, int status) {
        try {
            ObjectMapper mapper = new ObjectMapper();
            response.setContentType(ContentType.build(ContentType.JSON.getValue(), Charset.defaultCharset()));
            response.setStatus(status);
            response.getWriter().write(mapper.writeValueAsString(R.fail(resultCode)));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void cast(ResultCode resultCode) {
        throw new BusinessException(resultCode);
    }

    public static void cast(int code, String msg) {
        throw new BusinessException(code, msg);
    }
}

自定义无异常情况下请求 /oauth/token 获取 token 的响应格式

@Slf4j
@Aspect
@Component
public class CustomOAuthTokenAspect {

    @Around("execution(* org.springframework.security.oauth2.provider.endpoint.TokenEndpoint.postAccessToken(..))")
    public ResponseEntity response(ProceedingJoinPoint point) throws Throwable {
        Object proceed = point.proceed();
        ResponseEntity<OAuth2AccessToken> responseEntity = (ResponseEntity<OAuth2AccessToken>) proceed;
        return ResponseEntity.ok(R.ok(responseEntity.getBody()));
    }

}

处理 grant_type、username、password 错误的异常响应

  • 默认情况是使用 WebResponseExceptionTranslator接口的实现类 DefaultWebResponseExceptionTranslator对抛出的异常进行处理
  • 本文处理方法就是通过实现WebResponseExceptionTranslator接口来入手,来达到对异常信息的处理
  • 实现之后要记得将其添加到认证服务器核心配置 AuthorizationServerConfig 的端点配置 (AuthorizationServerEndpointsConfigurer.exceptionTranslator) 中,往下看会有写,不急
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.CacheControl;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.oauth2.common.DefaultThrowableAnalyzer;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.common.exceptions.InsufficientScopeException;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.security.oauth2.provider.error.WebResponseExceptionTranslator;
import org.springframework.security.web.util.ThrowableAnalyzer;
import org.springframework.stereotype.Component;
import org.springframework.web.HttpRequestMethodNotSupportedException;

import java.io.IOException;

@Slf4j
@Component
public class CustomWebResponseExceptionTranslator implements WebResponseExceptionTranslator {

    private ThrowableAnalyzer throwableAnalyzer = new DefaultThrowableAnalyzer();

    @Override
    public ResponseEntity translate(Exception e) throws Exception {
        Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(e);
        Exception ase = (OAuth2Exception) this.throwableAnalyzer.getFirstThrowableOfType(OAuth2Exception.class, causeChain);
        //异常链中有OAuth2Exception异常
        if (ase != null) {
            return this.handleOAuth2Exception((OAuth2Exception) ase);
        }
        //身份验证相关异常
        ase = (AuthenticationException) this.throwableAnalyzer.getFirstThrowableOfType(AuthenticationException.class, causeChain);
        if (ase != null) {
            return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.UnauthorizedException(e.getMessage(), e));
        }
        //异常链中包含拒绝访问异常
        ase = (AccessDeniedException) this.throwableAnalyzer.getFirstThrowableOfType(AccessDeniedException.class, causeChain);
        if (ase instanceof AccessDeniedException) {
            return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.ForbiddenException(ase.getMessage(), ase));
        }
        //异常链中包含Http方法请求异常
        ase = (HttpRequestMethodNotSupportedException) this.throwableAnalyzer.getFirstThrowableOfType(HttpRequestMethodNotSupportedException.class, causeChain);
        if (ase instanceof HttpRequestMethodNotSupportedException) {
            return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.MethodNotAllowed(ase.getMessage(), ase));
        }

        return this.handleOAuth2Exception(new CustomWebResponseExceptionTranslator.ServerErrorException(HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), e));
    }

    private ResponseEntity<OAuth2Exception> handleOAuth2Exception(OAuth2Exception e) throws IOException {
        int status = e.getHttpErrorCode();
        HttpHeaders headers = new HttpHeaders();
        headers.setCacheControl(CacheControl.noCache());
        headers.setPragma(CacheControl.noCache().getHeaderValue());
        if (status == HttpStatus.UNAUTHORIZED.value() || e instanceof InsufficientScopeException) {
            headers.set(HttpHeaders.WWW_AUTHENTICATE, String.format("%s %s", OAuth2AccessToken.BEARER_TYPE, e.getSummary()));
        }
        ResponseEntity<OAuth2Exception> response = new ResponseEntity(R.fail(e.getMessage()), headers, HttpStatus.valueOf(status));
        return response;
    }

    private static class MethodNotAllowed extends OAuth2Exception {
        public MethodNotAllowed(String msg, Throwable t) {
            super(msg, t);
        }

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

        @Override
        public int getHttpErrorCode() {
            return 405;
        }
    }

    private static class UnauthorizedException extends OAuth2Exception {
        public UnauthorizedException(String msg, Throwable t) {
            super(msg, t);
        }

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

        @Override
        public int getHttpErrorCode() {
            return 401;
        }
    }

    private static class ServerErrorException extends OAuth2Exception {
        public ServerErrorException(String msg, Throwable t) {
            super(msg, t);
        }

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

        @Override
        public int getHttpErrorCode() {
            return 500;
        }
    }

    private static class ForbiddenException extends OAuth2Exception {
        public ForbiddenException(String msg, Throwable t) {
            super(msg, t);
        }

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

        @Override
        public int getHttpErrorCode() {
            return 403;
        }
    }
}

自定义客户端异常处理过滤器 CustomClientCredentialsTokenEndpointFilter

  • 自定义客户端异常处理过滤器: {“error”: “invalid_client”, “error_description”: “Bad client credentials”}
  • 通过配置到认证服务器 (AuthorizationServerConfig) 的 client 认证异常过滤器中 (client_id、client_secret 错误时会执行)
  • 配置的同时要设置 CustomAuthenticationEntryPoint 以此来格式化异常返回值
  • 注意:AuthenticationEntryPoint 没有实例,需要我们自己实现这个接口才能进行注入
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter;
import org.springframework.security.web.AuthenticationEntryPoint;

public class CustomClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {
    private final AuthorizationServerSecurityConfigurer configurer;
    private AuthenticationEntryPoint authenticationEntryPoint;

    public CustomClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer) {
        this.configurer = configurer;
    }

    @Override
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    @Override
    protected AuthenticationManager getAuthenticationManager() {
        return configurer.and().getSharedObject(AuthenticationManager.class);
    }

    @Override
    public void afterPropertiesSet() {
        setAuthenticationFailureHandler((request, response, exception) -> authenticationEntryPoint.commence(request, response, exception));
        setAuthenticationSuccessHandler((request, response, authentication) -> {
            // no-op - just allow filter chain to continue to token endpoint
        });
    }

}

两个公共异常处理类(未认证、未授权)

实现 AuthenticationEntryPoint 接口来处理认证异常的响应信息
  • 处理 Authentication 异常,如:token错误、过期
  • 配置一:配置到资源服务器核心配置中(ResourceServerConfig)对 token 进行校验
  • 配置二:配置到认证服务器核心配置中(AuthorizationServerConfig),当客户端异常(client_id、client_secret错误)时会执行
import cn.hutool.http.HttpStatus;
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        R.failRender(HttpStatus.HTTP_UNAUTHORIZED, exception.getMessage(), response, HttpStatus.HTTP_UNAUTHORIZED);
        log.error("Authentication异常: [{}], [{}], [{}]", request.getRequestURI(), exception.getMessage(), exception);
    }
}
实现 AccessDeniedHandler 接口来处理权限异常的响应信息
  • 处理权限异常的异常信息,只针对于资源服务器,认证服务器无需配置
  • 客户端权限异常 (resource_id),用户权限异常,自定义响应值
import cn.hutool.http.HttpStatus;
import cn.mowen.common.result.R;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Slf4j
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException exception) throws IOException, ServletException {
        R.failRender(HttpStatus.HTTP_UNAUTHORIZED, exception.getMessage(), response, HttpStatus.HTTP_UNAUTHORIZED);
        log.error("AccessDenied异常: [{}], [{}], [{}]", exception.getMessage(), exception.getLocalizedMessage(), exception.toString());
    }
}

将以上定义的异常处理类添加到认证服务器核心配置 AuthorizationServerConfig 中

@Configuration
@EnableAuthorizationServer
@AllArgsConstructor
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    /**
     * 用来配置令牌端点的安全约束, 密码校验方式等
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 自定义客户端异常处理过滤器: {"error": "invalid_client", "error_description": "Bad client credentials"}
        CustomClientCredentialsTokenEndpointFilter endpointFilter = new CustomClientCredentialsTokenEndpointFilter(security);
        endpointFilter.afterPropertiesSet();//初始化的时候执行
        endpointFilter.setAuthenticationEntryPoint(customAuthenticationEntryPoint);//格式化客户端异常的响应格式

        security
                //.allowFormAuthenticationForClients()
                .addTokenEndpointAuthenticationFilter(endpointFilter) //添加一个客户端认证之前的过滤器
        ;

        /*
         * allowFormAuthenticationForClients 的作用:
         * 允许表单认证(申请令牌), 而不仅仅是Basic Auth方式提交, 且url中有client_id和client_secret的会走 ClientCredentialsTokenEndpointFilter 来保护,
         * 也就是在 BasicAuthenticationFilter 之前添加 ClientCredentialsTokenEndpointFilter,使用 ClientDetailsService 来进行 client 端登录的验证。
         * 但是,在使用自定义的 CustomClientCredentialsTokenEndpointFilter 时,
         * 会导致 oauth2 仍然使用 allowFormAuthenticationForClients 中默认的 ClientCredentialsTokenEndpointFilter 进行过滤,致使我们的自定义 CustomClientCredentialsTokenEndpointFilter 不生效。
         * 因此在使用 CustomClientCredentialsTokenEndpointFilter 时,不再需要开启 allowFormAuthenticationForClients() 功能。
         */

    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.exceptionTranslator(customWebResponseExceptionTranslator)//自定义异常转换类(处理grant_type, username, password错误的异常)
        ;
    }

}

将那两个公共异常处理类配置到资源服务器的核心配置中 ResourceServerConfig

import cn.mowen.common.constant.OauthConstant;
import cn.mowen.common.constant.CommonWhiteConstant;
import cn.mowen.common.exception.oauth.CustomAuthenticationEntryPoint;
import cn.mowen.common.exception.oauth.CustomAccessDeniedHandler;
import lombok.AllArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.token.TokenStore;

@Configuration
@EnableResourceServer
@AllArgsConstructor
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {

    private final TokenStore jwtTokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId(OauthConstant.OAUTH_RESOURCE_ID)
                .tokenStore(jwtTokenStore)
                .authenticationEntryPoint(new CustomAuthenticationEntryPoint())
                .accessDeniedHandler(new CustomAccessDeniedHandler())
                .stateless(true)
        ;
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                //放行 url 在此配置
                .antMatchers(CommonWhiteConstant.white).permitAll()
                .antMatchers(white).permitAll()
                .anyRequest().authenticated()
        ;
    }

    //  白名单
    private static final String[] white = {
            "/test/**"
    };
    
}

Oauth2.0 系列文章

以下是同步到语雀的、可读性好一点,CSDN 继续看的点专栏就好。
Oauth2.0 核心篇
Oauth2.0 安全性(以微信授权登陆为例)
Oauth2.0 认证服务器搭建
Oauth2.0 添加验证码登陆方式
Oauth2.0 资源服务器搭建
Oauth2.0 自定义响应值以及异常处理
Oauth2.0 补充

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值