Статьи

Веб-токены JSON с микросервисами Spring Cloud

В 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 .