JWT 与 Spring Cloud 微服务

https://keyholesoftware.com/2016/06/20/json-web-tokens-with-spring-cloud-microservices/
作者:THOMAS KENDALL
译者:oopsguy.com

微服务安全是架构的一个重要部分。具体来说,就是认证和授权模式。

微服务认证和授权处理方式有几种选择,但本文只介绍 JSON Web Token 的使用。

JSON Web Token

JSON Web Token(JWT)本质上是一个独立的身份验证令牌,可以包含用户标识、用户角色和权限等信息,以及您可以存储任何其他信息。任何人都可以轻松读取和解析,并使用密钥来验证真实性。有关 JSON Web Token 的简要介绍,请查看此页面(https://jwt.io/introduction/)。

微服务使用 JSON Web Token 的一个优点是,我们可以配置它以便包含用户拥有的任何权限。这意味着每个服务不需要与授权服务交互才能授权用户。

JWT 的另一个优点是它们是可序列化的,足够短的长度使得它可以放置在请求头中。

工作原理

JWT 的工作流程相当简单。第一次请求是一个带有用户名和密码的无身份验证端点的 POST。

认证成功后,响应将包含 JWT。之后所有的请求都附带一个 HTTP 头,其包含了 JWT 令牌:Authorization: xxxxx.yyyyy.zzzzz

任何服务间的请求都要通过该头,以便其他服务可以应用授权。

开始编码

我们需要做的第一件事是弄清楚如何生成 JWT。幸运的是,我们不是第一个踩坑的人,有几个现成的类库可供我们选择。

我选择了 Java JWT(https://github.com/jwtk/jjwt)。这是我的实现:

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
public class JsonWebTokenUtility {

private SignatureAlgorithm signatureAlgorithm;
private Key secretKey;

public JsonWebTokenUtility() {

// 这不是一个安全的实践
// 为了简化,我存储了一个静态的 key 在这里
// 实际上,在微服务环境中,key 是由配置服务器持有的
signatureAlgorithm = SignatureAlgorithm.HS512;
String encodedKey = "L7A/6zARSkK1j7Vd5SDD9pSSqZlqF7mAhiOgRbgv9Smce6tf4cJnvKOjtKPxNNnWQj+2lQEScm3XIUjhW+YVZg==";
secretKey = deserializeKey(encodedKey);
}

public String createJsonWebToken(AuthTokenDetailsDTO authTokenDetailsDTO) {
String token = Jwts.builder().setSubject(authTokenDetailsDTO.userId).claim("email", authTokenDetailsDTO.email)
.claim("roles", authTokenDetailsDTO.roleNames).setExpiration(authTokenDetailsDTO.expirationDate)
.signWith(getSignatureAlgorithm(), getSecretKey()).compact();
return token;
}

private Key deserializeKey(String encodedKey) {
byte[] decodedKey = Base64.getDecoder().decode(encodedKey);
Key key = new SecretKeySpec(decodedKey, getSignatureAlgorithm().getJcaName());
return key;
}

private Key getSecretKey() {
return secretKey;
}

public SignatureAlgorithm getSignatureAlgorithm() {
return signatureAlgorithm;
}

public AuthTokenDetailsDTO parseAndValidate(String token) {
AuthTokenDetailsDTO authTokenDetailsDTO = null;
try {
Claims claims = Jwts.parser().setSigningKey(getSecretKey()).parseClaimsJws(token).getBody();
String userId = claims.getSubject();
String email = (String) claims.get("email");
List roleNames = (List) claims.get("roles");
Date expirationDate = claims.getExpiration();

authTokenDetailsDTO = new AuthTokenDetailsDTO();
authTokenDetailsDTO.userId = userId;
authTokenDetailsDTO.email = email;
authTokenDetailsDTO.roleNames = roleNames;
authTokenDetailsDTO.expirationDate = expirationDate;
} catch (JwtException ex) {
System.out.println(ex);
}
return authTokenDetailsDTO;
}

private String serializeKey(Key key) {
String encodedKey = Base64.getEncoder().encodeToString(key.getEncoded());
return encodedKey;
}

}

现在我们有了这个工具类,之后需要在每个微服务中配置 Spring Security。

为此,我们需要自定义一个验证过滤器,如果存在请求头,则读取它。Spring 有一个认证过滤器 RequestHeaderAuthenticationFilter,我们可以继承它。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class JsonWebTokenAuthenticationFilter extends RequestHeaderAuthenticationFilter {

public JsonWebTokenAuthenticationFilter() {
// Don't throw exceptions if the header is missing
this.setExceptionIfHeaderMissing(false);

// This is the request header it will look for
this.setPrincipalRequestHeader("Authorization");
}

@Override
@Autowired
public void setAuthenticationManager(AuthenticationManager authenticationManager) {
super.setAuthenticationManager(authenticationManager);
}
}

此时,头已经以 PreAuthenticatedAuthenticationToken 的形式转换为 Spring Authentication 对象。

我们现在需要一个 AuthenticationProvider 用于读取令牌,从而进行身份验证,并将其转换为我们自己自定义的 Authentication 对象。

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
public class JsonWebTokenAuthenticationProvider implements AuthenticationProvider {

private JsonWebTokenUtility tokenService = new JsonWebTokenUtility();

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Authentication authenticatedUser = null;
// Only process the PreAuthenticatedAuthenticationToken
if (authentication.getClass().isAssignableFrom(PreAuthenticatedAuthenticationToken.class)
&& authentication.getPrincipal() != null) {
String tokenHeader = (String) authentication.getPrincipal();
UserDetails userDetails = parseToken(tokenHeader);
if (userDetails != null) {
authenticatedUser = new JsonWebTokenAuthentication(userDetails, tokenHeader);
}
} else {
// It is already a JsonWebTokenAuthentication
authenticatedUser = authentication;
}
return authenticatedUser;
}

private UserDetails parseToken(String tokenHeader) {

UserDetails principal = null;
AuthTokenDetailsDTO authTokenDetails = tokenService.parseAndValidate(tokenHeader);

if (authTokenDetails != null) {
List<GrantedAuthority> authorities = authTokenDetails.roleNames.stream()
.map(roleName -> new SimpleGrantedAuthority(roleName)).collect(Collectors.toList());
principal = new User(authTokenDetails.email, "", authorities);
}

return principal;
}

@Override
public boolean supports(Class<?> authentication) {
return authentication.isAssignableFrom(PreAuthenticatedAuthenticationToken.class)
|| authentication.isAssignableFrom(JsonWebTokenAuthentication.class);
}

}

有了这些组件,我们可以在 Spring Security 中使用 JWT 了。 在进行服务间通信时,我们需要传递 JWT。

我使用了一个 Feign 客户端,把 JWT 作为参数。

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
@FeignClient("user-management-service")
public interface UserManagementServiceAPI {

@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
AuthTokenDTO authenticateUser(@RequestBody AuthenticationDTO authenticationDTO);

@RequestMapping(method = RequestMethod.POST, value = "/roles")
RoleDTO createRole(@RequestHeader("Authorization") String authorizationToken, @RequestBody RoleDTO roleDTO);

@RequestMapping(method = RequestMethod.POST, value = "/users")
UserDTO createUser(@RequestHeader("Authorization") String authorizationToken, @RequestBody UserDTO userDTO);

@RequestMapping(method = RequestMethod.DELETE, value = "/roles/{id}")
void deleteRole(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id);

@RequestMapping(method = RequestMethod.DELETE, value = "/users/{id}")
void deleteUser(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id);

@RequestMapping(method = RequestMethod.GET, value = "/roles")
Collection<RoleDTO> findAllRoles(@RequestHeader("Authorization") String authorizationToken);

@RequestMapping(method = RequestMethod.GET, value = "/users")
Collection<UserDTO> findAllUsers(@RequestHeader("Authorization") String authorizationToken);

@RequestMapping(method = RequestMethod.GET, value = "/roles/{id}", produces = "application/json", consumes = "application/json")
RoleDTO findRoleById(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id);

@RequestMapping(method = RequestMethod.GET, value = "/users/{id}", produces = "application/json", consumes = "application/json")
UserDTO findUserById(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id);

@RequestMapping(method = RequestMethod.GET, value = "/users/{id}/roles")
Collection<RoleDTO> findUserRoles(@RequestHeader("Authorization") String authorizationToken,
@PathVariable("id") int id);

@RequestMapping(method = RequestMethod.PUT, value = "/roles/{id}")
void updateRole(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id,
@RequestBody RoleDTO roleDTO);

@RequestMapping(method = RequestMethod.PUT, value = "/users/{id}")
void updateUser(@RequestHeader("Authorization") String authorizationToken, @PathVariable("id") int id,
@RequestBody UserDTO userDTO);
}

为了传递 JWT,我在控制器的 Spring Security 中抓取它:

1
2
3
4
5
6
7
8
9
private String getAuthorizationToken() {
String token = null;
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.getClass().isAssignableFrom(JsonWebTokenAuthentication.class)) {
JsonWebTokenAuthentication jwtAuthentication = (JsonWebTokenAuthentication) authentication;
token = jwtAuthentication.getJsonWebToken();
}
return token;
}

JWT 可以很好地适应分布式微服务环境,并提供了大量功能。 如果您正想为下一个微服务项目设计一个安全架构,请考虑使用 JSON Web Token。