Spring Security5 Oauth2 自定义 OAuth2 Exception

前言

spring-security5 oauth 开箱既用,认证时返回的是oauth2 标准的返回值

先来看看,默认的返回值

## 未认证
{
  "error": "unauthorized",
  "error_description": "Full authentication is required to access this resource"
}
## 认证失败
{
"error": "invalid_client",
"error_description": "Bad client credentials"
}

## 认证成功
{
"access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6IndlYmFwcCIsInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sIkF1dGhvciI6InhpYW95YW8iLCJleHAiOjE1NjEwMjYxNDAsImF1dGhvcml0aWVzIjpbIlJPTEVfVEVTVCIsIlJPTEVfT0FVVEhfQURNSU4iXSwianRpIjoiYzhlMTA3YTItYWFlZS00NWNhLTk1NWYtM2UyZDgwZjhlZmM2IiwiY2xpZW50X2lkIjoid2ViYXBwIiwidGltZXN0YW1wIjoiMTU2MTAxODk0MDEwOCJ9.VgL4voycGdn4Fm_Ij3ZeAc9Rxdsmb_IXR14lgtJh1KE",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6IndlYmFwcCIsInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImM4ZTEwN2EyLWFhZWUtNDVjYS05NTVmLTNlMmQ4MGY4ZWZjNiIsIkF1dGhvciI6InhpYW95YW8iLCJleHAiOjE1NjIzMTQ5NDAsImF1dGhvcml0aWVzIjpbIlJPTEVfVEVTVCIsIlJPTEVfT0FVVEhfQURNSU4iXSwianRpIjoiMTRkNmQyMmMtZjc4OS00YzdlLTk0YjAtNTliYzhmYjlmY2NiIiwiY2xpZW50X2lkIjoid2ViYXBwIiwidGltZXN0YW1wIjoiMTU2MTAxODk0MDEwOCJ9.SOch9UKE078fRSxHYDZzXVTelVH3gpFOZUzM3KelKrk",
"expires_in": 7199,
"scope": "all",
"Author": "xiaoyao",
"clientId": "webapp",
"timestamp": "1561018940108",
"jti": "c8e107a2-aaee-45ca-955f-3e2d80f8efc6"
}

然而,这些虽是标准协议返回字段,公开给第三方做接口是没啥问题,但这个也会给我们前端去使用,这时候就会对前端造成一定的麻烦,在业务层,前端,一般会写一个拦截器,去通过一个code 或status字段,进行拦截,而认证阶段返回值就不统一了,所以我希望对于整个系统都是的,即看一下下面的期望结果的例子:

// code,msg,data 三个是我们与前端协定的整个系统中的返回值 
# 未认证
{
"msg": "token不正确或已过期",
"code": "401", 
"data": "Full authentication is required to access this resource",
// 下面两个是原始返回值
"error": "unauthorized", 
"error_description": "Full authentication is required to access this resource"
}

## 服务器错误

{
"error": "invalid_request",
"code": 400,
"msg": "Internal Server Error",
"error_description": "Internal Server Error",
"data": ""
}

// 登陆成功
{
"code": "200",
"msg": "success",
"data":{
    "access_token":         "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6IndlYmFwcCIsInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sIkF1dGhvciI6InhpYW95YW8iLCJleHAiOjE1NjEwMjYxNDAsImF1dGhvcml0aWVzIjpbIlJPTEVfVEVTVCIsIlJPTEVfT0FVVEhfQURNSU4iXSwianRpIjoiYzhlMTA3YTItYWFlZS00NWNhLTk1NWYtM2UyZDgwZjhlZmM2IiwiY2xpZW50X2lkIjoid2ViYXBwIiwidGltZXN0YW1wIjoiMTU2MTAxODk0MDEwOCJ9.VgL4voycGdn4Fm_Ij3ZeAc9Rxdsmb_IXR14lgtJh1KE",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnRJZCI6IndlYmFwcCIsInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiYWxsIl0sImF0aSI6ImM4ZTEwN2EyLWFhZWUtNDVjYS05NTVmLTNlMmQ4MGY4ZWZjNiIsIkF1dGhvciI6InhpYW95YW8iLCJleHAiOjE1NjIzMTQ5NDAsImF1dGhvcml0aWVzIjpbIlJPTEVfVEVTVCIsIlJPTEVfT0FVVEhfQURNSU4iXSwianRpIjoiMTRkNmQyMmMtZjc4OS00YzdlLTk0YjAtNTliYzhmYjlmY2NiIiwiY2xpZW50X2lkIjoid2ViYXBwIiwidGltZXN0YW1wIjoiMTU2MTAxODk0MDEwOCJ9.SOch9UKE078fRSxHYDZzXVTelVH3gpFOZUzM3KelKrk",
"expires_in": 7199,
"scope": "all",
"Author": "xiaoyao",
"clientId": "webapp",
"timestamp": "1561018940108",
"jti": "c8e107a2-aaee-45ca-955f-3e2d80f8efc6"
}
}

知道自己希望想要什么,才好着手去撸代码

正文

这里面要分两步走,因为资源服务 和认证服务中是不一样的(刚开始做的我以为是一样的,后开跟了半天的源代码,才走出了这个坑)默认的主要有两个类`OAuth2AuthenticationEntryPoint` 和 `DefaultWebResponseExceptionTranslator`

//资源服务上,默认是交给这个类来处理的,包括未认证的,和错误的token
//认证服务器上未认证的也是这个
`OAuth2AuthenticationEntryPoint` 

//认证服务器上,上面的类不够用了,因为在security的过滤器中到处出现了
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
和
private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();
// 认证时产生的异常交给了这个类处理返回结果
`DefaultWebResponseExceptionTranslator`

先来看资源服务吧,这个比较简单

首先自定义一个`AuthenticationEntryPoint`

public class CustomAuthenticationEntryPoint extends OAuth2AuthenticationEntryPoint {

    private WebResponseExceptionTranslator exceptionTranslator = new DefaultWebResponseExceptionTranslator();

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
        //  super  ()  //doHandle(request, response, e)
        // log.debug("token 验证失败处理器")
        BaseResult res = new BaseResult<>();
        res.setCode(HttpStatus.UNAUTHORIZED.value());

        ResponseEntity result = null;
        Map resp = new HashMap<> (5);
        try {
            result = exceptionTranslator.translate(e);
            result = enhanceResponse(result, e);
            // 这里用的guava切分字符串转map,但不能直接返回赋值给 result ,不多做解释,跟踪一下源码就知道
            Map split = Splitter.on(",").trimResults().trimResults(CharMatcher.is('\"'))
                    .withKeyValueSeparator("=\"").split(result.getBody().toString());
            // 不去改变原来父类里面返回的属性和值
            split.forEach((x,y) ->
                    resp.put(x.trim(), y)
            );
        } catch (Exception ex) {
            log.error("结果解析异常", ex);
        }
        resp.put("code", res.getCode()+"");
        resp.put("msg", "token不正确或已过期");
        resp.put("data",e.getMessage());
        HttpResponseUtil.writeResult(response, resp, HttpStatus.UNAUTHORIZED);


    }

} 
  

然后在资源费器的配置中配置即可

public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {

    @Autowired
    private BaseAuthenticationConfig baseAuthenticationConfig;


    @Autowired(required = false)
    private AuthenticationEntryPoint authenticationEntryPoint;


    @Override
    public void configure(HttpSecurity http) throws Exception {
        log.debug("Using ResourceServerConfiguration configure(HttpSecurity). " );
        baseAuthenticationConfig.configure(http);
        http
                .authorizeRequests()
                .antMatchers(baseAuthenticationConfig.getIgnoreUrl()).permitAll()
                .antMatchers("/login","/error","/oauth/token",
                       "/process/login","/logout","/login.html").permitAll()
                // .antMatchers("/**").authenticated()
                .antMatchers("/sys/sys-user/info").authenticated()
                .and()
                .csrf().disable()
        ;

    }

// 在这儿配
    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        // 配置认证失败处理器
        if(authenticationEntryPoint != null){
            resources.authenticationEntryPoint(authenticationEntryPoint);
        }

    }

}

认证服务器,稍微麻烦点

首先将自定义的 `AuthenticationEntryPoint`在http config中配置上

public void configure(HttpSecurity http) throws Exception {
        http.formLogin()
                .loginPage("/login")
                .loginProcessingUrl("/user/login")
                .successHandler(authenticationSuccessHandler)
                .failureHandler(authenticationFailureHandler)
            .and()
                .authorizeRequests().antMatchers("/login","/user/login","/user/logout","/oauth/token").permitAll()
            .and()
                .exceptionHandling().accessDeniedHandler(accessDeniedHandler)
            .and()
                //  这里设置的 只是用户没有登录的 情况下 没有token 的情况下,异常给下面的处理
                .exceptionHandling().authenticationEntryPoint(authenticationEntryPoint)

        ;
    }

好了在登录时认证失败会产生各种异常,但最有都有一个`ExceptionTranslationFilter`处理,这个是spring security的基础知识(插张图)

Spring Security5 Oauth2 自定义 OAuth2 Exception_第1张图片

 这个是访问认证服务器 资源的时候,还是交给自定义的AuthenticationEntryPoint 处理的

看一下登陆的时候

登陆失败

Spring Security5 Oauth2 自定义 OAuth2 Exception_第2张图片

 

-- 未完 待续,主要原因在用户通过授权码登陆异常时没有走自定义的异常处理类

 

 

 

 

 

 

 

你可能感兴趣的:(java)