在Keyhole,我们已经发布了几个有关微服务的博客 。 我们已经讨论了微服务环境中使用的架构模式,例如服务发现和断路器 。 我们甚至在平台和工具上发布了博客,例如最近关于Service Fabric的博客 。
我们已经介绍过的架构的重要组成部分是围绕微服务的安全性。 具体来说,是身份验证和授权模式。
在考虑微服务中的身份验证时,有多种选择,但是此博客将特别关注使用JSON Web令牌。
JSON Web令牌
本质上,JSON Web令牌(JWT)是一个独立的身份验证令牌,可以包含诸如用户标识符,用户的角色和权限以及您可能希望存储在其中的任何其他信息。 任何人都可以轻松读取和解析它,并可以使用密钥验证其真实性。 有关JSON Web令牌的简要介绍,请查看此页面 。
将JSON Web令牌与微服务一起使用的一个优势是,我们可以对其进行设置,使其已经包含用户拥有的所有权限。 这意味着每个服务都无需伸手我们的授权服务即可对用户进行授权。
JWT的另一个优点是它们是可序列化的,并且足够小以适合请求标头。
怎么运行的
工作流程非常简单。 第一个请求是使用用户名和密码的POST到不受保护的身份验证端点。
成功认证后,响应将包含JWT。 所有其他请求都带有一个HTTP标头,其中包含以Authorization: xxxxx.yyyyy.zzzzz
形式的JWT令牌Authorization: xxxxx.yyyyy.zzzzz
。
任何服务到服务的请求都将传递此标头,以便任何服务都可以沿途应用授权。
现在,到代码!
我们需要做的第一件事是弄清楚如何生成这些JWT。 幸运的是,我们不是第一个尝试此操作的人,并且有多个库可供选择。
我选择了Java JWT 。 这是我的实现:
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
,我们可以对其进行扩展。
public class JsonWebTokenAuthenticationFilter extends RequestHeaderAuthenticationFilter {public JsonWebTokenAuthenticationFilter() {// Don't throw exceptions if the header is missingthis.setExceptionIfHeaderMissing(false);// This is the request header it will look forthis.setPrincipalRequestHeader("Authorization");}@Override@Autowiredpublic void setAuthenticationManager(AuthenticationManager authenticationManager) {super.setAuthenticationManager(authenticationManager);}
}
此时,标头已以PreAuthenticatedAuthenticationToken
的形式转换为Spring Authentication对象。
现在,我们需要一个身份验证提供程序,它将读取此令牌,对其进行身份验证并将其转换为我们自己的自定义Authentication对象。
public class JsonWebTokenAuthenticationProvider implements AuthenticationProvider {private JsonWebTokenUtility tokenService = new JsonWebTokenUtility();@Overridepublic Authentication authenticate(Authentication authentication) throws AuthenticationException {Authentication authenticatedUser = null;// Only process the PreAuthenticatedAuthenticationTokenif (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 JsonWebTokenAuthenticationauthenticatedUser = 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;}@Overridepublic 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令牌。
翻译自: https://www.javacodegeeks.com/2016/06/json-web-tokens-spring-cloud-microservices.html