OAuth2结合JWT

JWT,全称是 Json Web Token , 是一种 JSON 风格的轻量级的授权和身份认证规范,可实现无状态、分布式的 Web 应用授权
JWT 作为一种规范,并没有和某一种语言绑定在一起,常用的 Java 实现是 GitHub 上的开源项目 jjwt,地址如下

https://github.com/jwtk/jjwt

JWT 存在的问题
1.续签问题,这是被很多人诟病的问题之一,传统的 cookie+session 的方案天然的支持续签,但是 jwt 由于服务端不保存用户状态,因此很难完美解决续签问题,如果引入 redis,虽然可以解决问题,但是 jwt 也变得不伦不类了。
2.注销问题,由于服务端不再保存用户信息,所以一般可以通过修改 secret 来实现注销,服务端 secret 修改后,已经颁发的未过期的 token 就会认证失败,进而实现注销,不过毕竟没有传统的注销方便。
3.密码重置,密码重置后,原本的 token 依然可以访问系统,这时候也需要强制修改 secret。
4.基于第 2 点和第 3 点,一般建议不同用户取不同 secret。

在之前,授权服务器派发了 access_token 之后,客户端拿着 access_token 去请求资源服务器,资源服务器要去校验 access_token 的真伪,所以我们在资源服务器上配置了 RemoteTokenServices,让资源服务器做远程校验:

@Bean
RemoteTokenServices tokenServices() {
    RemoteTokenServices services = new RemoteTokenServices();
    services.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
    services.setClientId("user");
    services.setClientSecret("123");
    return services;
}

在高并发环境下这样的校验方式显然是有问题的,如果结合 JWT,用户的所有信息都保存在 JWT 中,这样就可以有效的解决上面的问题
首先我们来看对授权服务器的改造,我们来修改 AccessTokenConfig 类,如下

@Configuration
public class AccessTokenConfig {
    private String SIGNING_KEY = "javaboy";

    @Bean
    TokenStore tokenStore() {
        return new JwtTokenStore(jwtAccessTokenConverter());
    }

    @Bean
    JwtAccessTokenConverter jwtAccessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey(SIGNING_KEY);
        return converter;
    }
}

1.TokenStore 我们使用 JwtTokenStore 这个实例。之前我们将 access_token 无论是存储在内存中,还是存储在 Redis 中,都是要存下来的,客户端将 access_token 发来之后,我们还要校验看对不对。但是如果使用了 JWT,access_token 实际上就不用存储了(无状态登录,服务端不需要保存信息),因为用户的所有信息都在 jwt 里边,所以这里配置的 JwtTokenStore 本质上并不是做存储。
2.另外我们还提供了一个 JwtAccessTokenConverter,这个 JwtAccessTokenConverter 可以实现将用户信息和 JWT 进行转换(将用户信息转为 jwt 字符串,或者从 jwt 字符串提取出用户信息)。
3.另外,在 JWT 字符串生成的时候,我们需要一个签名,这个签名需要自己保存好。

我们还可以添加自己的信息

@Component
public class CustomAdditionalInformation implements TokenEnhancer {
    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
        Map<String, Object> info = accessToken.getAdditionalInformation();
        info.put("author", "江南飞鹏");
        ((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(info);
        return accessToken;
    }
}

自定义类 CustomAdditionalInformation 实现 TokenEnhancer 接口,并实现接口中的 enhance 方法。enhance 方法中的 OAuth2AccessToken 参数就是已经生成的 access_token 信息,我们可以从 OAuth2AccessToken 中取出已经生成的额外信息,然后在此基础上追加自己的信息。

温馨提示:我们配置的 JwtAccessTokenConverter 也是 TokenEnhancer 的一个实例

配置完成之后,我们还需要在 AuthorizationServer 中修改 AuthorizationServerTokenServices 实例,如下

@Autowired
JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
CustomAdditionalInformation customAdditionalInformation;
@Bean
AuthorizationServerTokenServices tokenServices() {
    DefaultTokenServices services = new DefaultTokenServices();
    services.setClientDetailsService(clientDetailsService());
    services.setSupportRefreshToken(true);
    services.setTokenStore(tokenStore);
    TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
    tokenEnhancerChain.setTokenEnhancers(Arrays.asList(jwtAccessTokenConverter, customAdditionalInformation));
    services.setTokenEnhancer(tokenEnhancerChain);
    return services;
}

这里主要是是在 DefaultTokenServices 中配置 TokenEnhancer,将之前的 JwtAccessTokenConverter 和 CustomAdditionalInformation 两个实例注入进来即可。

我们将 auth-server 中的 AccessTokenConfig 类拷贝到 user-server 中,然后在资源服务器配置中不再配置远程校验地址,而是配置一个 TokenStore 即可:

@Configuration
@EnableResourceServer
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
    @Autowired
    TokenStore tokenStore;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("res1").tokenStore(tokenStore);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("admin")
                .anyRequest().authenticated();
    }
}

这里配置好之后,会自动调用 JwtAccessTokenConverter 将 jwt 解析出来,jwt 里边就包含了用户的基本信息,所以就不用远程校验 access_token 了。

上面配置完成后,我们就可以启动 auth-server、user-server 进行测试,这里为了测试方便,配置了password 模式来测试

首先我们请求 auth-server 获取 token,如下

java 集成IoT通讯 oauth2集成jwt_用户信息

可以看到,jwt 的字符串还是挺长的,另外返回的数据中也有我们自定义的信息。根据本文第一小节的介绍,小伙伴们可以使用一些在线的 Base64 工具自行解码 jwt 字符串的前两部分,当然也可以通过 check_token 接口来解析:

java 集成IoT通讯 oauth2集成jwt_java 集成IoT通讯_02


解析后就可以看到 jwt 中保存的用户详细信息了。拿到 access_token 之后,我们就可以去访问 user-server 中的资源了,访问方式跟之前的一样,请求头中传入 access_token 即可:

java 集成IoT通讯 oauth2集成jwt_用户信息_03

如此之后,我们就成功的将 OAuth2 和 Jwt 结合起来了。