Spring Security + OAuth 2.0 + JWT 开发随笔

JWT 签名与验签

公钥与私钥生成

使用 JDK 提供的 keytool 工具生成 JKS 密钥库 (Java Key Store),认证授权服务器会使用私钥对 Token 进行签名,一般将生成的 shop.jks 文件放在 resources 目录下

1
keytool -genkey -alias shop -keyalg RSA -keypass 123456 -keystore shop.jks -storepass 123456

根据私钥生成公钥,将其保存在 public.crt 文件中,用于对 Token 进行验签,一般将其放 resources 目录下

1
keytool -list -rfc --keystore shop.jks | openssl x509 -inform pem -pubkey -noout
1
2
3
4
5
6
7
8
9
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAtXKXj3JGNJNWVXg4+++4
FtNTJre+8kHLdPLwHJJcRw4aV7oMMjI1nesyj75w/kjRZImhbNo0poEu1jj+sDO9
UbLUHSy59zoDDMZTYmbkboDEpkFq3ZUhAoLtt5DtAgI8DkOK22RlSxXpcMvkeL8X
ziFizWf/HatSgAat/SfX+5dH3KX40piPv9kI5YVJz1GyD8xO4dN95tr0Ld7FDmdK
JBPWfkM+CMlKRhYqB+sAlaQW5/L3xb3WNftucC/RhdKT8/mmgMsIBhUZOS/1iFnD
KuPsEwU5xEQxK9pWX2bWsSkeOgQYJmQa6hiWBuujPUyOs4rICvniopxsW2yyPOFX
ZQIDAQAB
-----END PUBLIC KEY-----

认证授权服务器加载 JKS 秘钥库

认证授权服务器加载 JKS 秘钥库,从中获取密钥对(公钥 + 私钥),Java 示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
/**
* 从ClassPath下的密钥库中获取密钥对(公钥+私钥)
*
* @return
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory factory = new KeyStoreKeyFactory(new ClassPathResource("shop.jks"), "123456".toCharArray());
KeyPair keyPair = factory.getKeyPair("shop", "123456".toCharArray());
return keyPair;
}

认证授权服务器暴露获取公钥的接口

对外暴露 JWK Set URI 接口,让其他应用系统可以获取到公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
@RequestMapping("/oauth")
public class JwkSetController {

@Autowired
private KeyPair keyPair;

/**
* 获取公钥
*
* @return
*/
@GetMapping("/.well-known/jwks.json")
public Map<String, Object> publicKey() {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAKey key = new RSAKey.Builder(publicKey).build();
return new JWKSet(key).toJSONObject();
}

}

或者通过 KeyPair 来获取公钥

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@RestController
@RequestMapping("/oauth")
public class PublicKeyController {

@Autowired
private KeyPair keyPair;

/**
* 获取公钥
*
* @return
*/
@GetMapping("/publicKey")
public String publicKey() {
return Base64.encode(new String(keyPair.getPublic().getEncoded()));
}

}

资源服务器指定公钥文件的路径

在 YML 配置里指定认证授权服务器暴露的 JWK Set URI 接口,以此来获取公钥,值得一提的是,默认情况下 jwk-set-uri 指定的 URL 无法使用 Ribbon 来实现负载均衡访问(除非利用 DNS 的域名解析,即单个域名绑定多个 IP,通过 DNS 服务器做负载均衡)

1
2
3
4
5
6
7
8
spring:
application:
name: gateway-server
security:
oauth2:
resourceserver:
jwt:
jwk-set-uri: http://127.0.0.1:8005/oauth/.well-known/jwks.json

或者将上面通过 keytool 工具获取到的公钥拷贝到 src/main/resources/public.crt 文件中,然后在 YML 配置里指定公钥文件的路径

1
2
3
4
5
6
7
8
spring:
application:
name: gateway-server
security:
oauth2:
resourceserver:
jwt:
public-key-location: classpath:public.crt

Cannot convert access token to JSON 错误

应用启动后,出现 Cannot convert access token to JSON 这个错误,主要是 OAuth 2.0 的资源服务器缺少了加载公钥的配置,解决方法如下:

1
2
3
4
5
6
7
8
9
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 获取公钥
String publicKey = getPublicKey();
// 加载公钥
converter.setVerifier(new RsaVerifier(publicKey));
return converter;
}
★展开完整的示例代码★
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
@Configuration
public class OAuthTokenConfig {

private static final Logger loggerr = LoggerFactory.getLogger(OAuthTokenConfig.class);

@Autowired
private OAuth2ResourceServerProperties resourceServerProperties;

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

@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
// 获取公钥
String publicKey = getPublicKey();
// 加载公钥
converter.setVerifier(new RsaVerifier(publicKey));
loggerr.info("success to load public key");
return converter;
}

/**
* 通过读取本地文件获取非对称加密公钥
*
* @return 公钥
*/
private String getPublicKey() {
Resource resource = getPublicKeyFile();
try (BufferedReader br = new BufferedReader(new InputStreamReader(resource.getInputStream()))) {
return br.lines().collect(Collectors.joining("\n"));
} catch (Exception e) {
loggerr.error(e.getLocalizedMessage());
}
return getKeyFromAuthorizationServer();
}

/**
* 通过访问授权服务器获取非对称加密公钥
*
* @return 公钥
*/
private String getKeyFromAuthorizationServer() {
try {
ObjectMapper objectMapper = new ObjectMapper();
String pubKey = new RestTemplate().getForObject(resourceServerProperties.getJwt().getJwkSetUri(), String.class);
Map map = objectMapper.readValue(pubKey, Map.class);
return map.get("n").toString();
} catch (Exception e) {
loggerr.error(e.getLocalizedMessage());
}
return null;
}

/**
* 获取公钥文件
*
* @return 公钥
*/
public Resource getPublicKeyFile() {
try {
// 读取YML配置里指定的本地公钥文件,对应的YML配置如下:
// spring.security.oauth2.resourceserver.jwt.public-key-location = public.crt
Resource resource = resourceServerProperties.getJwt().getPublicKeyLocation();
if (FileUtil.exist(resource.getFile())) {
return resource;
}
} catch (Exception e) {
loggerr.error(e.getLocalizedMessage());
}
// 读取默认路径下的本地公钥文件
return new ClassPathResource("public.crt");
}

}

OAuth 2.0 资源服务器

资源服务器鉴权配置

默认情况下,OAuth 2.0 的权限是从 Client 的 scope 中获取,示例代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
/**
* 资源服务器配置
*/
@Configuration
@EnableResourceServer
public class OAuthResouceServer extends ResourceServerConfigurerAdapter {

@Autowired
private TokenStore tokenStore;

/**
* 资源配置
*/
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId("school")
.tokenStore(tokenStore)
.stateless(true)
.accessDeniedHandler(new CustomAccessDeniedHandler());
}

/**
* 对HTTP请求鉴权
*/
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/**").access("#oauth2.hasScope('teacher')")
.and().csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}

}

若权限存在于 authorities 中,需要替代 OAuth2ResourceServerWebSecurityConfiguration 的配置,示例代码如下:

  • 弃用方法安全
  • 通过自定义 Converter 来指定权限,Converter 是函数接口,当前上下问参数为 JWT 对象
  • 获取 JWT 中的 authorities
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer().jwt().jwtAuthenticationConverter(jwt -> {
Collection<SimpleGrantedAuthority> authorities =
((Collection<String>) jwt.getClaims()
.get("authorities")).stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toSet());
return new JwtAuthenticationToken(jwt, authorities);
});
}

}

参考博客