springboot集成oauth2 jwt,自定义access_token和错误返回格式

首先认证服务器端pom.xml文件引入


    2.3.3.RELEASE
    1.1.1.RELEASE


    
        org.springframework.boot
        spring-boot-starter-security
    
    
        org.springframework.security.oauth
        spring-security-oauth2
        ${spring.security.version}
    
    
        org.springframework.security
        spring-security-jwt
        ${spring-security-jwt.version}
    

下面只讲述关键过程,具体源代码请参阅https://github.com/coralloc8/springboot-example
github代码中包含的不止oauth2验证这一块,还包含一些基础组件,i18n国际化,线程池,本地缓存,eventbus组件等,可能有些杂乱。

认证配置config具体路径为com.example.spring.web.auth.config.SecurityConfig

首先来说明下在security oauth2中错误返回分为好几种情况:

  • ExceptionTranslationFilter返回异常错误信息,这种情况下在认证服务器端会直接重定向到error接口中,重新定义errror接口可以解决

  • ClientCredentialsTokenEndpointFilter这个过滤器是只有在认证方式为password且通过表单提交clientId和clientSecret字段时会触发,错误信息格式为

    {
    "error": "invalid_client",
    "error_description": "invalid client_id"
    }

    该过滤器返回的异常错误信息不回走errror接口,而是通过DefaultOAuth2ExceptionRenderer来处理,这里有个问题就是ClientCredentialsTokenEndpointFilter里面的point已经写死了

位置:org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer

    @Override
    public void configure(HttpSecurity http) throws Exception {
        
        // ensure this is initialized
        frameworkEndpointHandlerMapping();
        if (allowFormAuthenticationForClients) {
            clientCredentialsTokenEndpointFilter(http);
        }

        for (Filter filter : tokenEndpointAuthenticationFilters) {
            http.addFilterBefore(filter, BasicAuthenticationFilter.class);
        }

        http.exceptionHandling().accessDeniedHandler(accessDeniedHandler);
    }

    private ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter(HttpSecurity http) {
        ClientCredentialsTokenEndpointFilter clientCredentialsTokenEndpointFilter = new ClientCredentialsTokenEndpointFilter(
                frameworkEndpointHandlerMapping().getServletPath("/oauth/token"));
        clientCredentialsTokenEndpointFilter
                .setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
        authenticationEntryPoint.setTypeName("Form");
        authenticationEntryPoint.setRealmName(realm);
        clientCredentialsTokenEndpointFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
        clientCredentialsTokenEndpointFilter = postProcess(clientCredentialsTokenEndpointFilter);
        http.addFilterBefore(clientCredentialsTokenEndpointFilter, BasicAuthenticationFilter.class);
        return clientCredentialsTokenEndpointFilter;
    }

上面这段代码中已经默认实现为OAuth2AuthenticationEntryPoint,而OAuth2AuthenticationEntryPoint中默认的OAuth2ExceptionRenderer即为DefaultOAuth2ExceptionRenderer,虽然也提供了set方法来改变renderer,但源头已经写死了。也试过自己注册ClientCredentialsTokenEndpointFilter这个过滤器,但测试过程中发现没有起到正常作用,这一点有待后续debug跟踪。官方的建议是尽量不开启表单输入clientId和clientSecret,而采用basic的方式来携带clientId和clientSecret。在后续的例子中这一处没有制定自己的通用格式,有点遗憾。

需要特殊说明的是如果不是通过表单提交的而是通过basic 走http header的话,这时候如果clientId或者clientSecret错误的话,最终错误还是由ExceptionTranslationFilter来抛出,但是不会走error接口,而是走默认的一套AuthenticationEntryPoint机制,这个时候可以通过自定义CustomAuthExceptionHandler来实现它的commence方法来处理自己想要的格式

  • filter链处理完后,由TokenEndpoint来处理的时候,如果在这个阶段发生了各种异常,此时是经由WebResponseExceptionTranslator翻译器来翻译对应的错误信息,它默认有一套i18n国际化翻译,这里如果想自定义返回格式的话可以自己重新继承WebResponseExceptionTranslator来实现自己的国际化翻译

默认的security的用户认证是在代码中配置死,因此这里需要重新实现用户权限从数据库读取。

/**
 * 注册一个UserDetailsService用于用户身份认证
 *
 * @param oauth2Service
 * @param passwordEncoder
 * @return
 */
@Bean
public UserDetailsService userDetailsService(IOauth2Service oauth2Service, PasswordEncoder passwordEncoder) {
    return username -> {
        List users = oauth2Service.findOauth2UserByUsername(username);
        if (users == null || users.isEmpty()) {
            throw new UsernameNotFoundException("invalid username");
        }
        SysUser user = users.get(0);
        // String passwordAfterEncoder = passwordEncoder.encode(user.getPassword());
        String passwordAfterEncoder = user.getPassword();

        return User.withUsername(username).password(passwordAfterEncoder).roles(user.getRole().split(",")).build();
    };
}     

/**  
 * 注册一个AuthenticationManager用来password模式下用户身份认证
 *
 * @return
 */
@Override
@Bean
public AuthenticationManager authenticationManager() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());

    return new ProviderManager(Collections.singletonList(provider));
}

/**
 * 重写PasswordEncoder 接口中的方法,实例化加密策略
 * 这里采用bcrypt的加密策略,采用这种策略的好处是Bcrypt是单向Hash加密算法,类似Pbkdf2算法 不可反向破解生成明文,而且还兼容之前采用别的加密算法
 */
@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

接下来security认证配置

@Override
protected void configure(HttpSecurity http) throws Exception {
    //禁用csrf,在Security的默认拦截器里,默认会开启CSRF处理,判断请求是否携带了token,如果没有就拒绝访问。并且,在请求为(GET|HEAD|TRACE|OPTIONS)时,则不会开启
    http.csrf().disable();
    //将/oauth 开头的url都取消验证,任何人都可以自由访问
    http.authorizeRequests().antMatchers("/oauth/**").permitAll()
        //其它的所有url都需要携带token验证
        .and().authorizeRequests().anyRequest().authenticated();

    // ExceptionTranslationFilter返回的错误信息格式重新定义
    http.exceptionHandling()
        //
        .authenticationEntryPoint(customAuthExceptionHandler)
        //
        .accessDeniedHandler(customAuthExceptionHandler);

    // ExceptionTranslationFilter返回的错误信息格式重新定义
    http.exceptionHandling()
        //
        .authenticationEntryPoint(customAuthExceptionHandler)
        //
        .accessDeniedHandler(customAuthExceptionHandler);




}

security配置完成后,接下来认证授权配置
具体路径为:com.example.spring.web.auth.config.AuthorizationServerConfig

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

    /**
     * @formatter:off
     * grant_type种类
     *
     * 1、authorization_code — 授权码模式(即先登录获取code,再获取token)
     *
     * 2、password — 密码模式(将用户名,密码传过去,直接获取token)
     *
     * 3、client_credentials — 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向’服务端’获取资源)
     *
     * 4、implicit — 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash)
     *
     * 5、refresh_token — 刷新access_token
     * @formatter:on
     * 
     *      * /oauth/authorize:授权端点
     *      * /oauth/token:令牌端点
     *      * /oauth/confirm_access:用户确认授权提交端点
     *      * /oauth/error:授权服务错误信息端点
     *      * /oauth/check_token:用于资源服务访问的令牌解析端点
     *      * /oauth/token_key:提供公有密匙的端点,如果使用JWT令牌的话
     */

    /**
     * 设置保存token的方式,一共有五种
     */
    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private JwtAccessTokenConverter jwtAccessTokenConverter;

    @Autowired
    private TokenEnhancer tokenEnhancer;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * 注入userDetailsService,开启refresh_token需要用到
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private WebResponseExceptionTranslator customWebResponseExceptionTranslator;

    @Autowired
    private ClientDetailsService myClientDetailsService;

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;

    @Autowired
    private CustomAuthExceptionHandler customAuthExceptionHandler;

    /**
     * 配置认证服务器
     *
     * 
     * @return
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security.authenticationEntryPoint(customAuthExceptionHandler).accessDeniedHandler(customAuthExceptionHandler);

        security
            // 允许所有资源服务器访问公钥端点(/oauth/token_key)
            // 只允许验证用户访问令牌解析端点(/oauth/check_token)
            .tokenKeyAccess("permitAll()")
            // isAuthenticated
            .checkTokenAccess("isAuthenticated()")
            // 允许客户端发送表单来进行权限认证来获取令牌
            //如果关闭的话默认会走basic认证 即clientId和clientSecret组合起来base加密后放在http header中传递
            .allowFormAuthenticationForClients();
            //
            .authenticationEntryPoint(customAuthExceptionHandler)
            //
            .accessDeniedHandler(customAuthExceptionHandler);
    }

    //此处设置我们自定义的myClientDetailsService
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.withClientDetails(myClientDetailsService);
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints.userDetailsService(userDetailsService);

        // 设置token
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter, tokenEnhancer));
        endpoints.tokenStore(tokenStore).tokenEnhancer(tokenEnhancerChain);
        endpoints.reuseRefreshTokens(true);

        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // token有效期设置2个小时
        tokenServices.setAccessTokenValiditySeconds(60 * 60 * 2);
        // Refresh_token:30天
        tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 30);

        endpoints.tokenServices(tokenServices);

        //
        endpoints.authorizationCodeServices(authorizationCodeServices);
        // 密码授权模式
        endpoints.authenticationManager(authenticationManager);
        //
        endpoints.setClientDetailsService(myClientDetailsService);
        // 处理 ExceptionTranslationFilter 抛出的异常
        //此处很关键,如果不自定义异常翻译的话,默认的oauth2也会有一套内置的翻译,但是我们的接口一般返回的都是通用格式,包含code,data,message等信息,这时候就必须要自己实现翻译了
        endpoints.exceptionTranslator(customWebResponseExceptionTranslator);

        // ClientCredentialsTokenEndpointFilter f = null;
        // BasicAuthenticationFilter b = null;
        // ExceptionTranslationFilter e = null;
        // AnonymousAuthenticationFilter a = null;
        // BasicErrorController basicErrorController;

    }

    /**
     * 注册一个ClientDetailsService用户clientId和clientSecret验证
     * 这里系统已经有默认注册了,所以强制指定它为主
     * @formatter:off
     * 
     * BaseClientDetails属性如下:
     *
     * getClientId:clientId,唯一标识,不能为空
     * getClientSecret:clientSecret,密码
     * isSecretRequired:是否需要验证密码
     * getScope:可申请的授权范围
     * isScoped:是否需要验证授权范围
     * getResourceIds:允许访问的资源id,这个涉及到资源服务器
     * getAuthorizedGrantTypes:可使用的Oauth2授权模式,不能为空
     * getRegisteredRedirectUri:回调地址,用户在authorization_code模式下接收授权码code
     * getAuthorities:授权,这个完全等同于SpringSecurity本身的授权
     * getAccessTokenValiditySeconds:access_token过期时间,单位秒。null等同于不过期
     * getRefreshTokenValiditySeconds:refresh_token过期时间,单位秒。null等同于getAccessTokenValiditySeconds,0或者无效数字等同于不过期
     * isAutoApprove:判断是否获得用户授权scope
     * 
     * @formatter:on
     * @param oauth2Service
     * @param passwordEncoder
     * @return
     */
    @Primary
    @Bean
    public ClientDetailsService myClientDetailsService(IOauth2Service oauth2Service, PasswordEncoder passwordEncoder) {
        return clientId -> {
            List clients1 = oauth2Service.findOauth2ClientByClientId(clientId);
            if (clients1 == null || clients1.isEmpty()) {
                throw new InvalidClientException("invalid client_id");
            }
            OauthClientDetails client = clients1.get(0);
            String clientSecretAfterEncoder = client.getClientSecret();
            // String clientSecretAfterEncoder = passwordEncoder.encode(client.getClientSecret());
            BaseClientDetails clientDetails = new BaseClientDetails();
            clientDetails.setClientId(client.getClientId());
            clientDetails.setClientSecret(clientSecretAfterEncoder);
            clientDetails
                .setRegisteredRedirectUri(new HashSet<>(Arrays.asList(client.getWebServerRedirectUri().split(","))));
            clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getAuthorizedGrantTypes().split(",")));
            clientDetails.setScope(Arrays.asList(client.getScope().split(",")));

            return clientDetails;
        };
    }

    /**
     * 注册一个AuthorizationCodeServices以保存authorization_code的授权码code
     *
     * @param redisConnectionFactory
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate myRedisTemplate = new RedisTemplate<>();
        myRedisTemplate.setConnectionFactory(redisConnectionFactory);
        myRedisTemplate.afterPropertiesSet();
        return new RandomValueAuthorizationCodeServices() {

            @Override
            protected void store(String code, OAuth2Authentication authentication) {
                myRedisTemplate.boundValueOps(code).set(authentication, 10, TimeUnit.MINUTES);
            }

            @Override
            protected OAuth2Authentication remove(String code) {
                OAuth2Authentication authentication = myRedisTemplate.boundValueOps(code).get();
                myRedisTemplate.delete(code);
                return authentication;
            }
        };
    }

}

这里oauth2实现方式使用jwt来实现

@Configuration
@Slf4j
public class AuthJwtTokenStore {

    /**
     * 存储token
     *
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        // return new RedisTokenStore(redisConnectionFactory);//RedisConnectionFactory redisConnectionFactory
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    /**
     * 生成JWT 中的 OAuth2 令牌
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        //此处很关键,之所以自定义CustomJwtAccessTokenConverter实现,
        //是由于oauth2默认认证成功后返回的数据格式很有可能不是我们想要的通用数据格式,
        //而要想自定义认证成功后返回的数据格式,就需要想办法处理了,
        //网上目前办法其实有的重写/oauth/token 认证接口,有的采用aop拦截,但都感觉侵入性太强了,
        //也不优雅。采用此种方式是在debug跟踪看了源码后,个人觉得侵入性相对较低。
        JwtAccessTokenConverter converter = new CustomJwtAccessTokenConverter();

        converter.setKeyPair(keyPair());
        return converter;
    }

    /**
     * @formatter:off
     * jks文件自己生成
     * 其实有两种实现方式,一种是采用对称加密的方式,直接converter.setSigningKey("")即可,
     * 在生产环境推荐使用公私匙加密的方式更安全,这种方式生成的access_token长度也会很长,至于jks文件生成方式可以使用
     * (1)生成*.jks
     * keytool -genkeypair -alias oauth2 -keyalg RSA -keypass oauth2 -keystore oauth2.jks -storepass oauth2
     * keytool -importkeystore -srckeystore oauth2.jks -destkeystore oauth2.jks -deststoretype pkcs12
     *(2)生成公钥,复制保存到pubkey.txt
     * keytool -list -rfc --keystore oauth2.jks | openssl x509 -inform pem -pubkey
     * @return
     * @formatter:on
     */
    private KeyPair keyPair() {
        return new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "oauth2".toCharArray()).getKeyPair("oauth2");

    }

    /**
     * 自定义令牌声明,添加额外的属性 需要注意的是此处添加的节点其实是和access_token属于同一父节点下的子节点,而不是包含在access_token里面
     *
     * @return
     */
    @Bean
    public TokenEnhancer tokenEnhancer() {
        return (OAuth2AccessToken accessToken, OAuth2Authentication authentication) -> {
            Map additionalInfo = new HashMap<>(16);

            ((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(additionalInfo);
            return accessToken;
        };
    }
}

前面认证服务器对大部分的异常错误已经自定义了通用格式返回,但是当你开发过程中会发现其实还有很多地方会跳出官方默认的错误异常格式:

{
"error": "invalid_client",
"error_description": "Bad client credentials"
}

这是由于security默认有个拦截器链

1、WebAsyncManagerIntegrationFilter
将Security上下文与Spring Web中用于处理异步请求映射的 WebAsyncManager 进行集成。

2、SecurityContextPersistenceFilter
在每次请求处理之前将该请求相关的安全上下文信息加载到SecurityContextHolder中,然后在该次请求处理完成之后,将SecurityContextHolder中关于这次请求的信息存储到一个“仓储”中,然后将SecurityContextHolder中的信息清除

例如在Session中维护一个用户的安全信息就是这个过滤器处理的。

3、HeaderWriterFilter
用于将头信息加入响应中

4、CsrfFilter
用于处理跨站请求伪造

5、LogoutFilter
用于处理退出登录

6、UsernamePasswordAuthenticationFilter
用于处理基于表单的登录请求,从表单中获取用户名和密码。默认情况下处理来自“/login”的请求。
从表单中获取用户名和密码时,默认使用的表单name值为“username”和“password”,这两个值可以通过设置这个过滤器的usernameParameter 和 passwordParameter 两个参数的值进行修改。

7、DefaultLoginPageGeneratingFilter
如果没有配置登录页面,那系统初始化时就会配置这个过滤器,并且用于在需要进行登录时生成一个登录表单页面。

8、BasicAuthenticationFilter
处理请求头信息,DigestAuthenticationFilter

9、RequestCacheAwareFilter
用来处理请求的缓存

10、SecurityContextHolderAwareRequestFilter

11、AnonymousAuthenticationFilter

12、SessionManagementFilter

13、ExceptionTranslationFilter
处理 AccessDeniedException 和 AuthenticationException 异常

14、FilterSecurityInterceptor
AbstractInterceptUrlConfigurer.createFilterSecurityInterceptor

其中的ExceptionTranslationFilter会直接拦截一部分错误,然后返回错误消息,这时候前面自定义的错误消息就不会起作用了,跟踪过源码后发现其实可以设置自定义的entryPoint

http.exceptionHandling().authenticationEntryPoint(customAuthExceptionHandler)
.accessDeniedHandler(customAuthExceptionHandler);

但是设置好后会发现其实根本没起作用,因为它自己源代码内部已经设置了一个默认的entryPoint,但同时也提供了set方法可以修改entryPoint,这一点我自己在调试过程中发现其实是由于父类的abstract启动方法中多次循环遍历调用所有子实现类的init()方法和configure()方法,n次重复调用后自定义的entryPoint就被替换为系统内置的DelegatingAuthenticationEntryPoint来处理错误返回。
另外我也尝试过采用过滤器的方式,在ExceptionTranslationFilter之前或者之后拦截错误信息,然后在自定义的filter中设置自定义的entryPoint来处理错误返回信息,这里又发生了一个现象就是,它第一次会走内置的12个过滤器,我自定义的过滤器不包含在里面,执行完之后又重新执行一次过滤器链,这次的过滤器链总数变成了13个,包含我自定义的过滤器链,然后这个时候已经为时已晚了,这一点我也很疑惑,为何会执行两次过滤器链。
最后没有深入debug下去,转而采用重定义error接口的方式来处理filter返回的错误信息。

@Configuration
public class MyErrorMvcAutoConfig {

    @Autowired
    private ServerProperties serverProperties;

    @Bean
    public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
        ObjectProvider errorViewResolvers) {
        return new CustomBasicErrorController(errorAttributes, this.serverProperties.getError(),
            errorViewResolvers.orderedStream().collect(Collectors.toList()));
    }
}

@Slf4j
@RequestMapping("${server.error.path:${error.path:/error}}")
public class CustomBasicErrorController extends BasicErrorController {

    private final ErrorProperties errorProperties;

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties) {
        super(errorAttributes, errorProperties);
        this.errorProperties = errorProperties;

    }

    public CustomBasicErrorController(ErrorAttributes errorAttributes, ErrorProperties errorProperties,
        List errorViewResolvers) {
        super(errorAttributes, errorProperties, errorViewResolvers);
        this.errorProperties = errorProperties;
    }

    @RequestMapping
    @Override
    public ResponseEntity> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        Result result = new Results().failure();
        if (status == HttpStatus.NO_CONTENT) {
            String resultJson = JsonUtil.toJson(result);
            return new ResponseEntity<>(JsonUtil.toMap(resultJson), status);
        }
        Map body = this.getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));

        log.error(">>>>>body error:{}", body);

        result = new Results().failure(OauthMessageEnum.UNAUTHORIZED);
        String resultJson = JsonUtil.toJson(result);
        Map resultJsonMap = JsonUtil.toMap(resultJson);

        return new ResponseEntity<>(resultJsonMap, status);
    }

    @Override
    @ExceptionHandler(HttpMediaTypeNotAcceptableException.class)
    public ResponseEntity mediaTypeNotAcceptable(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        Result result = new Results().failure(OauthMessageEnum.HTTP_MEDIA_TYPE_NOT_ACCEPT);
        String resultJson = JsonUtil.toJson(result);
        return new ResponseEntity<>(resultJson, status);
    }

    /**
     * Provide access to the error properties.
     * 
     * @return the error properties
     */
    @Override
    protected ErrorProperties getErrorProperties() {
        return this.errorProperties;
    }

以上,认证服务器端处理完毕,最终认证成功返回的效果为:

http status 200

{
    "code": 0,
    "message": "success",
    "data": {
        "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1OTI5MTQ1NzgsInVzZXJfbmFtZSI6ImFkbWluIiwiYXV0aG9yaXRpZXMiOlsiUk9MRV9BRE1JTiJdLCJqdGkiOiI1MzZjM2MxNy1hOTFiLTQ4ZWMtODBlMy05ZTBlZTZiYWM2ZDEiLCJjbGllbnRfaWQiOiJ0ZXN0MSIsInNjb3BlIjpbImFsbCJdfQ.SM4WjsVIjE3sqqKxUIf9cIxenj0oeLb7e4EepxRlQHDPbU3KJYxZ23t2FRs3kERuXTamZp4UufdmnlBXra9TEhCPEwBLOW56YcLk2fZUpxV0k_rXrqJPISP2VCdVcI8rdDLFB-0I8zSS-quNAkyO01lYBQf-8tyEsheSGoisGLSUQRNJ01SdIpCuHsCKgUn71rIv6xZOwFmvxOgdutXVVw4mfbbTd-JQzuMNtiV9X_bK7klifr5a0W-S7FIwfJl3QIl_Iw6c2HD1WlPniPu-BpCRkx5VZfEbvFdM2kiBfkWZ3K-Kvo1PY4VFFRV_BqEViBwvnxmL5z5nTCzuhOOUaA",
        "refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJhZG1pbiIsInNjb3BlIjpbImFsbCJdLCJhdGkiOiI1MzZjM2MxNy1hOTFiLTQ4ZWMtODBlMy05ZTBlZTZiYWM2ZDEiLCJleHAiOjE1OTU0OTkzNzgsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMjdhYmExMjItYWZmNy00YWRhLThmN2MtZTVhNDJlMTY4ZjM1IiwiY2xpZW50X2lkIjoidGVzdDEifQ.KcQB08fZ_VDfZXxjZQYag8E5Zpyf8zcKKGbvKv82-FQVO1IYpsSTON0IYg4niMQ3-DEFZXsKjbVq0FOUeT4KlBU_vOa7HWQYdJGbUlYjSqqfiVEG1hB-jwix8LSDBsW2zRDl442cpvBrHSGJLAamPhvX1vOjnj7mWWGK2j-YyO4zezixW4OPZQ72l5uxCFXmSF3vXRig44mhZw00NjG7V3__LWDkhwtIIHqApIIegRMUXD9kB_xrRRoxdbnXA9V8GFP40bS7lD89jlvXoW7epSvNzWlq63qPUFKx1aISB6OpHjNRq_jAOpy8uPlw1i3iWmhkEJJvcYYq9nT0A8GpwA",
        "scope": "all",
        "token_type": "bearer",
        "expires_in": 7199
    }
}

错误返回的结果为:

http status 400

{
    "code": 40004,
    "message": "wrong user name or password",
    "data": {}
}
http status 401

{
    "code": 40007,
    "message": "invalid token",
    "data": {}
}

现在资源服务器端pom.xml文件如下:

        
            org.springframework.boot
            spring-boot-starter-security
        
        
            org.springframework.security.oauth
            spring-security-oauth2
            ${spring.security.version}
        
        
            org.springframework.security
            spring-security-jwt
            ${spring-security-jwt.version}
        

资源服务器端的token转换器是使用公匙校验

/**
     * 生成JWT 中的 OAuth2 令牌
     *
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setVerifierKey(this.getPubKey());
        return converter;
    }

    private String getPubKey() {
        Resource resource = new ClassPathResource("pubkey.txt");
        try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {

            return br.lines().collect(Collectors.joining("\n"));
        } catch (IOException ioe) {
            // return getKeyFromAuthorizationServer();
            log.error("error:", ioe);
        }
        return "";
    }

接下来配置AuthResourceServerConfig,这里需要向认证服务器端进行token校验

    // 此处需要加上@Primary注解,强制指定他是第一顺位,系统默认已经注册了一个
   @Primary
    @Bean
    public ResourceServerTokenServices tokenServices() {
        final RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setCheckTokenEndpointUrl(checkTokenAccess);
        tokenService.setClientId(authorizationCodeResourceDetails.getClientId());
        tokenService.setClientSecret(authorizationCodeResourceDetails.getClientSecret());
        return tokenService;
    }

资源服务器端token校验过程中也会触发security内置的i18n国际化翻译。因此想要指定通用格式的话,需要重新翻译,以及自定义格式, CustomAuthExceptionHandler实现AuthenticationEntryPoint

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) {
        resources.authenticationEntryPoint(customAuthExceptionHandler).accessDeniedHandler(customAuthExceptionHandler);
    }



至此资源服务器配置完毕。

你可能感兴趣的:(springboot集成oauth2 jwt,自定义access_token和错误返回格式)