最近开始研究 OAuth2.0 协议,发现大部分都基于 JWT(JSON Web Token) 去实现,放弃了传统的 cookie/session 模式。cookie/session 模式不仅需要前端存储 cookie,后端服务也需要存储对应的 session,在分布式场景下,就需要依赖于集中存储去维护 session。而 JWT 则无需维护,在 token 中已经存储了用户相关的信息。
JWT JWS JWE
JWT
JSON Web 令牌(JWT)是一个开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息
也就是说 JWT 实际上是一种规范,并确定使用 JSON 作为表达,JWS 和 JWE 则是对这种规范的实现以及增强。
JWS (JSON Web Signature)
JWS 使用 Base64 进行编码,它包含三个部分,分别以 . 进行分割,如xxxx.yyyy.zzz的形式,分别对应 Header, Payload, Signature。
- Header: 存储一些元数据:类型以及其签名使用的算法
- Payload:按照标准存储相关信息,包括 iss-签发者,exp-过期时间,sub-对象主体(一般是用户信息),iat-签发时间,aud-接收方
- Signature:签名信息,由密钥签署,可以使用公钥来验证。
JWS 一般用于交换非敏感信息,若 token 中包含用户敏感信息,则需要对其加密,这就用到了 JWE
JWE(JSON Web Encryption)
JWE 本质上是对 Jwt 中的 Payload 进行加密。当然 JWE 是可以包含 JWS 的,也就是说当前 token 既有签名保证完整性,又有加密来保证安全性。
既然要加密,则需要公钥和私钥了,密钥一般使用 OpenSSL 生成 x509 格式。这里又引伸出 JWKs 的概念:
JSON Web 密钥集(JWKS)包含公钥,用于验证授权服务器发布并使用 RS256 签名算法签名的 JWT。
JWKs 包含一系列的公钥,授权服务器可以通过接口对外暴露相关的公钥,用于资源服务器进行认证。更多的 JWK 规范可以参考self-issued.info/docs/draft-…
Spring OAuth2 集成
资源服务器(Resource Server)
根据上一篇文章,可以直接引用 spring-boot-starter-oauth2-resource-server 依赖。首先需要配置一个获取 JWK 的接口:
spring:
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://localhost:8080/.well-known/jwks.json
这个接口的返回数据如下:
{
"keys": [
{
// key 使用的算法族(RSA/EC)
"kty": "RSA",
"e": "AQAB",
// 用途: sig-签名 enc-加密
"use": "sig",
// 自定义的 key id,用于验证解密方使用
"kid": "jwt-test",
// 加密算法
"alg": "RS256",
// 公钥
"n": "kyGzFh-nlzeYoTfmi3Tn0zhX3b2dL7OBczVVLornTg7SlmIC7xbx-A8t5HZebCfwesseKrfYO1J__bgsFJzVnDSMKlpyGVSoyGAKK46MUgDd_6_MmH_S2x4JcmOOdryw1zghpMXMO8C7i7fKdWr8hvQfxeoj0rK9A37Mtywlu2ur1GxEmUuiGEufOpiHhIldyJmEK5K4wD7woAoko6NvYx4kR-uGCij_RuyW_4_a737t4I57Ab50RoCknLJUj2_q355A0q-LANiskfRYUDGsqSxGX_0l7O7u_kU-GicY0mUsfC19nQ6MkzcTGcIbxhdLmIUGmSLZ3AFI4EyyfvtEhw"
}
]
}
有一点需要注意,JWK 其实是一组公钥的集合,key id 可以支持密钥在数据库中存储或更新迭代。在授权服务器中,暴露出来的应该是仅用于验证的公钥。
在配置类里,简单的将 JWT 解析方法配置进去就好。
@Configuration
@EnableWebSecurity(debug = true)
public class MySecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${spring.security.oauth2.resourceserver.jwt.jwk-set-uri}")
private String jwkSetUri;
@Override
protected void configure(HttpSecurity http) throws Exception{
http
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt)
.csrf().disable()
.authorizeRequests()
.antMatchers("/hello/**").hasAuthority("SCOPE_all")
.anyRequest().authenticated();
}
@Bean
JwtDecoder jwtDecoder() {
return NimbusJwtDecoder.withJwkSetUri(this.jwkSetUri).build();
}
}
授权服务器
授权服务器相对要麻烦一些,首先要定义 JWT 的一系列操作,这里用到的是nimbus-jose-jwt 依赖(spring-boot-starter-oauth2-resource-server也是引用了此jar包)。
@Configuration
public class JwtConfig {
/**
* 配置 接口返回的 JWKs 的参数, 包括用途、keyId、算法族。
*/
@Bean
public JWKSet jwkSet() {
RSAKey.Builder builder = new RSAKey.Builder((RSAPublicKey) keyPair().getPublic())
.keyUse(KeyUse.SIGNATURE)
.algorithm(JWSAlgorithm.RS256)
.keyID("jwt-test");
return new JWKSet(builder.build());
}
/**
* 加载由 openssl 生成的证书,需要 jks 格式。
*/
@Bean
public KeyPair keyPair(){
ClassPathResource ksFile = new ClassPathResource("jwt-test.jks");
KeyStoreKeyFactory ksFactory =
new KeyStoreKeyFactory(ksFile, "jwt-test".toCharArray());
return ksFactory.getKeyPair("jwt-test");
}
/**
* Spring oauth2 需要的 token转换器。
*/
@Bean
public JwtAccessTokenConverter converter(){
Map<String, String> customHeaders =
Collections.singletonMap("kid", "jwt-test");
return new CustomJwtAccessConvertor(
customHeaders,
keyPair());
}
@Bean
public TokenStore jwtTokenStore(){
return new JwtTokenStore(converter());
}
}
此外,JWT 的生成步骤也需要重写,这里是继承了 JwtAccessTokenConvertor 重写encode()方法,并塞入我们自己生成的 Jwt header。
@Override
protected String encode(OAuth2AccessToken accessToken,
OAuth2Authentication authentication) {
String content;
try {
content = this.objectMapper
.formatMap(getAccessTokenConverter()
.convertAccessToken(accessToken, authentication));
} catch (Exception ex) {
throw new IllegalStateException(
"Cannot convert access token to JSON", ex);
}
return JwtHelper.encode(
content,
this.signer,
this.customHeaders).getEncoded();
}