1、前言
在《基于SpringSecurity OAuth2实现的统一认证中心》中,已经实现了基于opaqueToken的统一认证方式,这里我们将基于这篇内容,进行基于JWT方式的实现。相似的内容,这里将不再重复,所以看这篇内容的童鞋,请先移步《这里》了解基于opaqueToken方式的实现过程。
2、JWT 简介
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519)。该Token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该Token也可直接被用于认证,也可被加密。
2.1、JWT是什么?
JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串。其中,第一部分我们称它为头部(header),第二部分我们称其为载荷(payload, 类似于飞机上承载的物品),第三部分是签证(signature)。JWT字符串如下:
eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE2NDAxMTU3MjAsInVzZXJfbmFtZSI6ImFkbWluIiwianRpIjoiZTI1N2RhOWEtOGE3Yi00OWM1LTg1ZjctNWExYWI3NDA2NTBmIiwiY2xpZW50X2lkIjoicmVzb3VyY2UxIiwic2NvcGUiOlsiYWxsIl19.JqAIYcsUOkchj41PkBw3n0-pgZHzp55SGuanTQVjF9jpePEMh3PKRmkLk4jSbhGWCRb14DtIJlxTyNTOQdk3KCs_xaSlGTFXCOV5EO4_vS0dF23pPIN2zwWbVZEGP9gmAjOgS5Iv-CNP673QI1e5b1C6axnxTWjjXTstIgCJmFlCA1DbEzto8ghP1p6S-QLi3K3N2VU791EGgZcukM0AO70plNEOxUV7E9v2HxDXt8FYB1okPGn7IvV5luGhkkxL1Q0mPAMk5BJdRBJzVHysuv0Uc2QwdyrFK0xALbNnEMCZ92mbXOZYp8HfJzN1RLanSn6vHerkf54ku1wg33MYtw
2.2、JWT构成 – 头部(Header)
Header header典型的由两部分组成:token的类型(“JWT”)和算法名称(比如:HS256、HMAC SHA256或者RSA等等)。头部内容通过进行base64加密(该加密是可以对称解密的)生成。比如前面示例中的jwt字符串的头部“eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9”通过base64解密,得到如下内容:
{
'alg': "RS256",
'typ': "JWT"
}
2.3、JWT构成 – 载荷(playload)
JWT的第二部分内容是载荷(payload),载荷就是存放有效信息的地方,这些有效信息包含三个部分:
- 标准中注册的声明
- 公共的声明
- 私有的声明
其中,标准中注册的声明,是一组预定义的声明,它们不是强制的,但是推荐,如下所示:
- iss: jwt签发者
- sub: jwt所面向的用户
- aud: 接收jwt的一方
- exp: jwt的过期时间,这个过期时间必须要大于签发时间
- nbf: 定义在什么时间之前,该jwt都是不可用的.
- iat: jwt的签发时间
- jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
载荷(playload)内容,是由定义的json信息,通过base64加密得到的,也可以通过解密对应的内容得到原始数据,比如前面示例中的载荷解密后,内容如下:
{
"exp":1640115720,
"user_name":"admin",
"jti":"e257da9a-8a7b-49c5-85f7-5a1ab740650f",
"client_id":"resource1",
"scope":[
"all"
]
}
2.4、JWT构成 – 签证信息(signature)
JWT的第三部分内容是签证信息,这个签证信息由三部分组成:
- header (base64后的)
- payload (base64后的)
- secret
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
将这三部分用.连接成一个完整的字符串,构成了最终的JWT。需要注意的是:secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。
3、基于JWT实现对称加密的用法
对称加密,在实际中用的比较少,但是比较简单,这里简单梳理一下该用法。
3.1、授权服务 修改
基于《基于SpringSecurity OAuth2实现的统一认证中心》这一篇中授权服务配置的基础,只需要添加JWT依赖,并修改AuthorizationServerConfig类即可。
首先,需要增加对应的JWT依赖,如下所示:
<dependency>
<groupId>com.nimbusds</groupId>
<artifactId>nimbus-jose-jwt</artifactId>
<version>8.8</version>
<scope>compile</scope>
</dependency>
然后,修改授权服务配置类AuthorizationServerConfig,修改内容如下:
/**
* 基于Jwt时需要的对象,用于jwt与token的转化
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey("00367171843C185C043DDFB90AA97677F11D02B629DEAFC04F935419D832E697");
return converter;
}
/**
* 配置Token存储方式,这个不是必须的,可以使用JwtTokenStore,也可以使用其他方式
* @return
*/
@Bean
TokenStore tokenStore(){
//return new InMemoryTokenStore();
return new JdbcTokenStore(dataSource);
//return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置令牌的访问端点和令牌服务,需要在这里把JwtAccessTokenConverter 对象配置到授权服务器中
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
//.tokenServices(tokenServices());//指定token存储位置
}
3.2、客户端通用鉴权qriver-auth-client,修改
基于《基于SpringSecurity OAuth2实现的统一认证中心》这一篇中qriver-auth-client实现的基础,只需要添加JWT依赖,并修改AuthClientConfig类即可。
首先,添加对应的JWT依赖,具体代码如下:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
然后,修改AuthClientConfig配置类,具体内容如下:
/**
* 注入JwtDecoder 对象,需要注意的是对应的key,需要和服务器端的一直,而且必须大于32位。
*/
@Bean
JwtDecoder jwtDecoder() throws InvalidKeyException {
return NimbusJwtDecoder.withSecretKey(new SecretKeySpec("00367171843C185C043DDFB90AA97677F11D02B629DEAFC04F935419D832E697".getBytes(), "HMACSHA256")).build();
}
/**
* 配置 过滤器,需要把oauth2ResourceServer()的方法,使用jwt()方式,并配置jwtDecoder对象即可。
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().csrf().disable();
http.headers().frameOptions().disable();
//http.oauth2ResourceServer().opaqueToken();
//jwt 对称方式
http.oauth2ResourceServer().jwt().decoder(jwtDecoder());
}
最后,还需要把配置文件中关于opaquetoken的配置注释掉就可以了。到这里就完成了jwt对称加密的修改了,其实只修改了授权服务和客户端通用jar的配置内容即可,非常的简单。
4、基于JWT实现非对称加密的用法
4.1、使用keytool工具,生成秘钥文件
使用jdk自带的keytool工具生成秘钥文件,具体的命令如下:
keytool -genkey -alias jwt -keyalg RSA -keystore jwt.jks
参数如下:
- genkey:创建证书
- alias:证书的别名。在一个证书库文件中,别名是唯一用来区分多个证书的标识符
- keyalg:密钥的算法,非对称加密的话就是RSA
- keystore:证书库文件保存的位置和文件名。如果路径写错的话,会出现报错信息。如果在路径下,证书库文件不存在,那么就会创建一个
使用如上命令,会在当前目录下,生成jwt.jks文件,该文件在实现基于JWT非对称加密时需要用到。
4.2、授权服务器 修改
和前面的非对称方式类似,基于原有代码进行修改。首先,jwt相关依赖,和非对称的一样,这里不再重复添加了。
然后,把4.1中生成的秘钥文件,复制到授权服务的resource目录下,如下所示:
然后,再创建读取jwt.jks秘钥文件的类,提供了获取公钥、私钥的方法,其中包括秘钥文件路径名称、创建秘钥文件时输入的密码、别名等。具体实现如下:
public class KeyConfig {
//文件名
private static final String KEY_STORE_FILE = "jwt.jks";
//生成秘钥时的密码
private static final String KEY_STORE_PASSWORD = "123456";
//别买
private static final String KEY_ALIAS = "jwt";
private static KeyStoreKeyFactory KEY_STORE_KEY_FACTORY = new KeyStoreKeyFactory(
new ClassPathResource(KEY_STORE_FILE), KEY_STORE_PASSWORD.toCharArray());
public static RSAPublicKey getVerifierKey() {
return (RSAPublicKey) getKeyPair().getPublic();
}
public static RSAPrivateKey getSignerKey() {
return (RSAPrivateKey) getKeyPair().getPrivate();
}
private static KeyPair getKeyPair() {
return KEY_STORE_KEY_FACTORY.getKeyPair(KEY_ALIAS);
}
}
然后,完成了上述配置类后,在修改授权服务器的配置类AuthorizationServerConfig的内容,具体实现如下:
/**
* 基于Jwt时需要的对象,非对称加密时,JwtAccessTokenConverter对象的配置如下,这里使用RSA算法
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
//非对称加密
RsaSigner signer = new RsaSigner(KeyConfig.getSignerKey());
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigner(signer);
converter.setVerifier(new RsaVerifier(KeyConfig.getVerifierKey()));
return converter;
}
/**
* 配置Token存储方式,非必须修改
* @return
*/
@Bean
TokenStore tokenStore(){
//return new InMemoryTokenStore();
//return new JdbcTokenStore(dataSource);
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 配置令牌的访问端点和令牌服务,和对称加密配置一样
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authorizationCodeServices(authorizationCodeServices())
.tokenStore(tokenStore())
.accessTokenConverter(jwtAccessTokenConverter());
}
/**
* 注入JWKSet 对象,该对象不属于授权服务器的配置,为了方便放到了该配置类中,主要是提供了读取公钥的对象。
* @return
*/
@Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder(KeyConfig.getVerifierKey())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256);
return new JWKSet(builder.build());
}
最后,提供一个Oauth2Controller类,暴露公钥访问,具体实现如下:
@Controller
public class Oauth2Controller {
@Autowired
JWKSet jwkSet;
@RequestMapping("/oauth2/keys")
@ResponseBody
public String keys() {
return jwkSet.toString();
}
}
至此,授权服务器的修改就完成了,接下来修改客户端配置。
4.3、客户端修改
和对称加密一样,需要引入JWT依赖,这里不再贴出对应代码。和对称加密相比,非对称加密,不需要在AuthClientConfig类中注入JwtDecoder对象,只需要配置jwkSetUri即可,实现如下所示:
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and().csrf().disable();
http.headers().frameOptions().disable();
//http.oauth2ResourceServer().opaqueToken();
//jwt非对称方式
http.oauth2ResourceServer().jwt().jwkSetUri("http://localhost:8080/oauth2/keys");
//jwt 对称方式
//http.oauth2ResourceServer().jwt().decoder(jwtDecoder());
}
在上述代码中,通过jwkSetUri()方法配置了对应jwkSetUri参数,这个参数值就是授权服务器暴露的获取公钥的地址,或者可以通过配置文件配置,实现如下:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/oauth2/keys
至此,我们就完成了基于JWT实现对称加密和非对称加密两种方式的改造。重启所有的服务,然后访问模块A、B均可正常。