В Keyhole мы опубликовали несколько блогов о микросервисах . Мы говорили об архитектурных шаблонах, используемых в среде микросервисов, таких как обнаружение служб и автоматический выключатель . Мы даже разместили блоги на платформах и инструментах, таких как недавний блог на Service Fabric.
Одним из важных элементов архитектуры, который мы замаскировали, является безопасность вокруг микросервисов. В частности, шаблоны аутентификации и авторизации.
Есть несколько вариантов при рассмотрении аутентификации в Microservices, но этот блог будет специально посвящен использованию веб-токенов JSON.
Веб-токены JSON
По сути, JSON Web Token (JWT) — это автономный токен аутентификации, который может содержать такую информацию, как идентификатор пользователя, роли и разрешения пользователя, а также все, что вы можете захотеть сохранить в нем. Он может быть легко прочитан и проанализирован любым пользователем и может быть проверен как подлинный с секретным ключом. Для краткого ознакомления с JSON Web Tokens посетите эту страницу .
Одно из преимуществ использования веб-токенов JSON с микросервисами состоит в том, что мы можем настроить его так, чтобы он уже содержал любые полномочия, которыми обладает пользователь. Это означает, что каждому сервису не нужно обращаться к нашему сервису авторизации для авторизации пользователя.
Другое преимущество, которое имеют JWT, состоит в том, что они сериализуемы и достаточно малы, чтобы помещаться в заголовок запроса.
Как это работает
Рабочий процесс довольно прост. Первый запрос — это POST к незащищенной конечной точке аутентификации с именем пользователя и паролем.
При успешной аутентификации ответ содержит JWT. Все дальнейшие запросы идут с заголовком HTTP, который содержит этот токен JWT в форме Authorization: xxxxx.yyyyy.zzzzz .
Любые сервисные запросы передают этот заголовок так, чтобы любая из служб могла применить авторизацию на своем пути.
Теперь к Кодексу!
Первое, что нам нужно сделать, это выяснить, как генерировать эти JWT. К счастью, мы не первые, кто пытается это сделать, и есть несколько библиотек на выбор.
Я выбрал Java JWT . Вот моя реализация:
|
01
02
03
04
05
06
07
08
09
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
|
public class JsonWebTokenUtility { private SignatureAlgorithm signatureAlgorithm; private Key secretKey; public JsonWebTokenUtility() { // THIS IS NOT A SECURE PRACTICE! // For simplicity, we are storing a static key here. // Ideally, in a microservices environment, this key would kept on a // config server. 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 который мы можем расширить.
|
01
02
03
04
05
06
07
08
09
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); }} |
На этом этапе заголовок был преобразован в объект Spring Authentication в форме PreAuthenticatedAuthenticationToken .
Теперь нам нужен поставщик аутентификации, который будет читать этот токен, аутентифицировать его и преобразовывать в наш собственный объект аутентификации.
|
01
02
03
04
05
06
07
08
09
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 в качестве параметра.
|
01
02
03
04
05
06
07
08
09
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 прекрасно вписываются в распределенную среду микросервисов и предоставляют много возможностей. При разработке архитектуры безопасности для вашего следующего проекта Microservices рассмотрите JSON Web Tokens.
| Ссылка: | Веб-токены JSON с Spring Cloud Microservices от нашего партнера JCG Томаса Кендлалла в блоге Keyhole Software . |