springcloud整合oauth2-----异常配置总结

异常配置

【1】认证服务的异常

  • 用户名,密码错误异常、授权类型异常
  • 客户端ID、秘钥异常

1、用户名,密码错误异常、授权类型异常

针对用户名、密码、授权类型错误的异常解决方式比较复杂,需要定制的比较多。

一、定制提示信息、响应码

这部分根据自己业务需要定制,举个例子,代码如下:

public enum ResultCode {

    CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),

    USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),

    UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式"),

    NO_PERMISSION(1005,"无权限访问!"),
    UNAUTHORIZED(401, "系统错误"),

    INVALID_TOKEN(1004,"无效的token");
二、自定义WebResponseExceptionTranslator
  • 需要自定义一个异常翻译器,默认的是DefaultWebResponseExceptionTranslator,此处必须重写,其中有一个需要实现的方法,如下:

    ResponseEntity<T> translate(Exception e) throws Exception;
    

    这个方法就是根据传递过来的Exception判断不同的异常返回特定的信息,这里需要判断的异常的如下:

    • UnsupportedGrantTypeException:不支持的授权类型异常
    • InvalidGrantException:用户名或者密码错误的异常
  • 创建一个OAuthServerWebResponseExceptionTranslator实现WebResponseExceptionTranslator,代码如下:

  • public class OAuthServerWebResponseExceptionTranslator implements WebResponseExceptionTranslator{
        /**
         * 业务处理方法,重写这个方法返回客户端信息
         */
        @Override
        public ResponseEntity<ResultMsg> translate(Exception e){
            ResultMsg resultMsg = doTranslateHandler(e);
            return new ResponseEntity<>(resultMsg, HttpStatus.UNAUTHORIZED);
        }
    
        /**
         * 根据异常定制返回信息
         * TODO 自己根据业务封装
         */
        private ResultMsg doTranslateHandler(Exception e) {
            //初始值,系统错误,
            ResultCode resultCode = ResultCode.UNAUTHORIZED;
            //判断异常,不支持的认证方式
            if(e instanceof UnsupportedGrantTypeException){
                resultCode = ResultCode.UNSUPPORTED_GRANT_TYPE;
                //用户名或密码异常
            }else if(e instanceof InvalidGrantException){
                resultCode = ResultCode.USERNAME_OR_PASSWORD_ERROR;
            }
            return new ResultMsg(resultCode.getCode(),resultCode.getMsg(),null);
        }
    }
    
三、认证服务配置文件中配置

需要将自定义的异常翻译器OAuthServerWebResponseExceptionTranslator在配置文件中配置,很简单,一行代码的事。

AuthorizationServerConfig配置文件指定,代码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0dh3IeZ-1662780344834)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203102659.png)]

四、这么配置的原因
  • 我们知道获取令牌的接口为 /oauth/token,这个接口定义在TokenEndpoint#postAccessToken()(POST请求)方法中,如下图
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AiZNlL02-1662780344835)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203752062.png)]
  • 是不是都继承了OAuth2Exception,那么尝试在TokenEndpoint这个类中找找有没有处理OAuth2Exception这个异常的处理器,果然找到了一个 handleException() 方法,如下:
  • [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eHeizoig-1662780344836)(C:\Users\CSEN\AppData\Roaming\Typora\typora-user-images\image-20220906203840232.png)]
  • 可以看到,这里的异常翻译器已经使用了我们自定义的OAuthServerWebResponseExceptionTranslator。可以看下默认的异常翻译器是啥,代码如下:

2、客户端ID、秘钥异常

这部分比较复杂,想要理解还是需要些基础的,解决这个异常的方案很多,陈某只是介绍其中一种,下面详细介绍。

一、定制提示信息、响应码

这部分根据自己业务需要定制,和第一步一样。

二、自定义AuthenticationEntryPoint

这个AuthenticationEntryPoint是不是很熟悉,前面的文章已经介绍过了,此处需要自定义来返回定制的提示信息。

创建OAuthServerAuthenticationEntryPoint,实现AuthenticationEntryPoint,重写其中的方法,代码如下:

  • public class OAuthServerAuthenticationEntryPoint implements AuthenticationEntryPoint {
    
        /**
         * 认证失败处理器会调用这个方法返回提示信息
         * TODO 实际开发中可以自己定义,此处直接返回JSON数据:客户端认证失败错误提示
         */
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
            ResponseUtils.result(response,new ResultMsg(ResultCode.CLIENT_AUTHENTICATION_FAILED.getCode(),ResultCode.CLIENT_AUTHENTICATION_FAILED.getMsg(),null));
        }
    }
    
三、改造ClientCredentialsTokenEndpointFilter

ClientCredentialsTokenEndpointFilter这个过滤器的主要作用就是校验客户端的ID、秘钥,代码如下:

public class OAuthServerClientCredentialsTokenEndpointFilter extends ClientCredentialsTokenEndpointFilter {

    private final AuthorizationServerSecurityConfigurer configurer;

    private AuthenticationEntryPoint authenticationEntryPoint;

    /**
     * 构造方法
     * @param configurer AuthorizationServerSecurityConfigurer对昂
     * @param authenticationEntryPoint 自定义的AuthenticationEntryPoint
     */
    public OAuthServerClientCredentialsTokenEndpointFilter(AuthorizationServerSecurityConfigurer configurer, AuthenticationEntryPoint authenticationEntryPoint) {
        System.out.println("自定义的客户端认证的过滤器的构造方法");
        this.configurer = configurer;
        this.authenticationEntryPoint=authenticationEntryPoint;
    }

    @Override
    public void setAuthenticationEntryPoint(AuthenticationEntryPoint authenticationEntryPoint) {
        System.out.println("setAuthenticationEntryPoint");
        this.authenticationEntryPoint = authenticationEntryPoint;
    }

    /**
     * 需要重写这个方法,返回AuthenticationManager
     */
    @Override
    protected AuthenticationManager getAuthenticationManager() {
        System.out.println("getAuthenticationManager");
        return configurer.and().getSharedObject(AuthenticationManager.class);
    }

    /**
     * 设置AuthenticationEntryPoint主要逻辑
     */
    @Override
    public void afterPropertiesSet() {
        System.out.println("设置AuthenticationEntryPoint主要逻辑");
        //TODO 定制认证失败处理器,开发中可以自己修改
        setAuthenticationFailureHandler((request, response, exception) -> {
            if (exception instanceof BadCredentialsException) {
                exception = new BadCredentialsException(exception.getMessage(), new BadClientCredentialsException());
            }
            authenticationEntryPoint.commence(request, response, exception);
        });
        //成功处理器,和父类相同,为空即可。
        setAuthenticationSuccessHandler((request, response, authentication) -> {
        });
    }
}

有几个重要的部分需要讲一下,如下:

  • 构造方法中需要传入第2步自定义的 OAuthServerAuthenticationEntryPoint
  • 重写 getAuthenticationManager() 方法返回IOC中的AuthenticationManager
  • 重写afterPropertiesSet() 方法,用于自定义认证失败、成功处理器,失败处理器中调用OAuthServerAuthenticationEntryPoint进行异常提示信息返回
四、OAuth配置文件中指定过滤器

只需要将自定义的过滤器添加到AuthorizationServerSecurityConfigurer中,代码如下:

@Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        System.out.println("配置令牌访问的安全约束");
        //自定义ClientCredentialsTokenEndpointFilter,用于处理客户端id,密码错误的异常
        ①OAuthServerClientCredentialsTokenEndpointFilter endpointFilter = new OAuthServerClientCredentialsTokenEndpointFilter(security,authenticationEntryPoint);
       ① endpointFilter.afterPropertiesSet();
        ①security.addTokenEndpointAuthenticationFilter(endpointFilter);

        security
                .authenticationEntryPoint(authenticationEntryPoint)
                //开启/oauth/token_key验证端口权限访问
                .tokenKeyAccess("permitAll()")
                //开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("permitAll()");
               ② //一定不要添加allowFormAuthenticationForClients,否则自定义的OAuthServerClientCredentialsTokenEndpointFilter不生效
//                .allowFormAuthenticationForClients();
    }

部分是添加过滤器,其中authenticationEntryPoint使用的是第2步自定义的OAuthServerAuthenticationEntryPoint

部分一定要注意:一定要去掉这行代码,具体原因源码解释。

五、源码追踪
I、OAuthServerAuthenticationEntryPoint在何时调用?

OAuthServerAuthenticationEntryPoint这个过滤器继承了 AbstractAuthenticationProcessingFilter 这个抽象类,一切的逻辑都在 doFilter() 中,陈某简化了其中的关键代码如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
    try {
        	//调用子类的attemptAuthentication方法,获取参数并且认证
			authResult = attemptAuthentication(request, response);
		}
		catch (InternalAuthenticationServiceException failed) {
            //一旦认证异常,则调用unsuccessfulAuthentication方法,通过failureHandler处理
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		catch (AuthenticationException failed) {
            //一旦认证异常,则调用unsuccessfulAuthentication方法,通过failureHandler处理
			unsuccessfulAuthentication(request, response, failed);
			return;
		}
		//认证成功,则调用successHandler处理
		successfulAuthentication(request, response, chain, authResult);
}

关键代码在 unsuccessfulAuthentication() 这个方法中,代码如下

    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) throws IOException, ServletException {
        SecurityContextHolder.clearContext();
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication request failed: " + failed.toString(), failed);
            this.logger.debug("Updated SecurityContextHolder to contain null Authentication");
            this.logger.debug("Delegating to authentication failure handler " + this.failureHandler);
        }

        this.rememberMeServices.loginFail(request, response);
        this.failureHandler.onAuthenticationFailure(request, response, failed);
    }
II、自定义的过滤器如何生效的?

这个就要看 AuthorizationServerSecurityConfigurer#configure() 这个方法了,其中有一段代码如下:

while(var2.hasNext()) {
    Filter filter = (Filter)var2.next();
    http.addFilterBefore(filter, BasicAuthenticationFilter.class);
}

也就是说,我们自定义的过滤链被加到了BasicAuthenticationFilter里面

III、为什么不能加.allowFormAuthenticationForClients()?

还是在 AuthorizationServerSecurityConfigurer#configure() 这个方法中,一旦设置了 allowFormAuthenticationForClients 为true,则会创建 ClientCredentialsTokenEndpointFilter,此时自定义的自然失效了。

【2】资源服务自定义异常信息

下面针对上述两种异常分别定制异常提示信息,这个比认证服务定制简单。

1、自定义返回结果:没有权限访问时

@Component
public class RequestAccessDeniedHandler implements ServerAccessDeniedHandler {
    @Override
    public Mono<Void> handle(ServerWebExchange exchange, AccessDeniedException denied) {

        System.out.println("RequestAccessDeniedHandler");
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(HttpStatus.OK);
        response.getHeaders().add(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
        System.out.println("wuquan2");

        String body= JSONUtil.toJsonStr(new ResultMsg(ResultCode.NO_PERMISSION.getCode(),ResultCode.NO_PERMISSION.getMsg(),null));
        DataBuffer buffer =  response.bufferFactory().wrap(body.getBytes(Charset.forName("UTF-8")));
        return response.writeWith(Mono.just(buffer));
    }
}

2、用于处理没有登录或token过期时的自定义返回结果

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值