Spring Cloud 微服務安全架構

JSON Web Token

JSON Web Token(JWT)本質上是一個獨立的身份驗證令牌,可以包含用戶標識、用戶角色和權限等信息,以及您可以存儲任何其他信息。任何人都可以輕鬆讀取和解析,並使用密鑰來驗證真實性。

JWT 的另一個優點是它們是可序列化的,足夠短的長度使得它可以放置在請求頭中。

工作原理

JWT 的工作流程相當簡單。第一次請求是一個帶有用戶名和密碼的無身份驗證端點的 POST。

認證成功後,響應將包含 JWT。之後所有的請求都附帶一個 HTTP 頭,其包含了 JWT 令牌:Authorization: xxxxx.yyyyy.zzzzz

開始編碼

我們需要做的第一件事是弄清楚如何生成 JWT。幸運的是,我們不是第一個踩坑的人,有幾個現成的類庫可供我們選擇。

我選擇了 Java JWT。這是我的實現:

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,我們可以繼承它。

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 對象。

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 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 作為參數。

@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 findAllRoles(@RequestHeader("Authorization") String authorizationToken); @RequestMapping(method = RequestMethod.GET, value = "/users") Collection 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 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 中抓取它:

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。


分享到:


相關文章: