使用JSON Web Tokens和Spring实现微服务
JSON Web Token (JWT) 是一个自我认证的记号,能够包含用户标识、角色和用户权限等信息,能够被任何人方便解析和使用安全的key实现验证,具体可参考这里。
结合JWT的微服务能够避免会话Session开销,同时方便跨多机分布式系统,这样不需要每个服务都要调用专门的授权服务以确认用户操作本服务的权限。另外一个优点是JWT足够小,能够被序列化附加在请求头部内。
第一个带有用户名和密码的请求提交POST到一个未受保护的登录授权REST端点,一旦用户和密码通过授权,响应中将包含一个JWT,以后的请求都会把这个JWT记号带在HTTP头部中,形式像: Authorization: xxxxx.yyyyy.zzzzz
任何服务到服务的请求会一路传递这个请求头部,这样授权信息都可以应用在一路传递的服务中,这些服务都可以检查这个JWT,以决定是否接受访问。
下面以Spring cloud代码为案例,使用JWT的Java实现: 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中已经有一个这样的授权Filter称为: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);
}
}
在这里,头部信息将被转换为Spring Authentication 对象,名称为PreAuthenticatedAuthenticationToken
我们需要一个授权提供者读取这个记号,然偶验证它,然后转换为我们自己的定制授权对象:
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);
}
}
使用这些组件,我们现在就可以有使用JWT的标准Spring Securtiy安全机制,当我们进行服务对服务调用时,我们需要一路传输JWT。
这里虚构一个客户端,将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提供了灵活的授权。
项目源码:Github