做者:THOMAS KENDALLgit
轉載自公衆號:stackgcgithub
微服務安全是架構的重要部分之一。具體來講,就是認證和受權模式。web
微服務認證和受權處理方式有幾種選擇,但本文只介紹 JSON Web Token 的使用。spring
JSON Web Token(JWT)本質上是一個獨立的身份驗證令牌,能夠包含用戶標識、用戶角色和權限等信息,以及您能夠存儲任何其餘信息。任何人均可以輕鬆讀取和解析,並使用密鑰來驗證真實性。有關 JSON Web Token 的簡要介紹,請查看此頁面(jwt.io/introductio…json
微服務使用 JSON Web Token 的一個優勢是,咱們能夠配置它以便包含用戶擁有的任何權限。這意味着每一個服務不須要與受權服務交互才能受權用戶。安全
JWT 的另一個優勢是它們是可序列化的,足夠短的長度使得它可放置在請求頭中。服務器
JWT 的工做流程至關簡單。第一次請求是一個帶有用戶名和密碼的無身份驗證端點的 POST。架構
認證成功後,響應將包含 JWT。以後全部的請求都附帶一個 HTTP 頭,其包含了 JWT 令牌:Authorization: xxxxx.yyyyy.zzzzz
。app
全部服務間的請求都要經過該頭,以便其餘服務能夠應用受權。
咱們須要作的第一件事是弄清楚如何生成 JWT。幸運的是,咱們不是第一個踩坑的人,有幾個現成的 JWT 類庫。
我選擇了 Java JWT(github.com/jwtk/jjwt)。…
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<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 做爲參數。
@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 中獲取它:
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。