JWT(JSON Web Token)是一种开放标准,它以一种紧凑且独立的方式,可以在各方之间作为JSON对象安全地传输信息。
其认证原理是,客户端向服务器申请授权,服务器认证以后,生成一个token字符串并返回给客户端,此后客户端在请求受保护的资源时携带这个token,服务端进行验证再从这个token中解析出用户的身份信息。
一个JWT是一个字符串,其由Header(头部)、Payload(负载)和Signature(签名)三个部分组成,中间以.
号分隔,其格式为Header.Payload.Signature
,如下所示:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJpc3MiOiJNSU5HIiwiZXhwIjoxNTQ3MzQ1MjgxLCJ1c2VyTmFtZSI6ImFkbWluIiwiaWF0IjoxNTQ3MzQ1MjIxfQ.
NfsnnoORftR4oP7hFHjmDgj5HYxKd-RXjEW9upn9Tgk
Header
Header本质是一个JSON对象,由包含了两个属性:
{
"alg": "HS256",
"typ": "JWT"
}
alg
表示签名的算法,typ
则表示token的类型。然后,将Header进行Base64URL 编码转成字符串作为JWT的第一组成部分。
Payload
JWT的第二组成部分被称作载荷,其本质也是一个JSON对象,用来存放认证所需的一些额外声明,其包含有一种类型:标准中注册的声明(registered),公共的声明(public), 和私有的声明(private claims),其中公有声明和私有声明可自定义字段。
标准中注册的声明 (建议但不强制使用) :
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
仅有的声明和私有的声明可以添加一些额外的用户属性,例如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
由于JWT 是不加密的,所以不应该把用户敏感类信息放在这个部分。
将Payload
部分进行Base64URL 编码转成字符串,即为JWT的第二组成部分。
Signature
JWT的第三组成部分是签名,它是对前面两个部分的签名。比如,如果JWT中Header指定的算法是 MAC SHA256,那么Signature为,其中secret加解密使用的密钥:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
关于JWT更详细的介绍,可到官网查看:https://jwt.io/introduction/
由于JWT传输过程中可携带额外的信息(如用户信息),使用JWT后,服务端不再需要保存用户的状态信息,服务端需要保存的只有加解密使用的密钥。由于JWT的无状态特性(不需存储),在一个分布式的面向服务的框架中,使用JWT做接口鉴权是再适合不过的了。
由于JWT的天然无状态,在JWT生成的一刻,其过期时间就已经注定了,并且不可更改,所以,如果要对JWT实现token续签的话,一般的做法是额外生成一个refreshToken用于获取新token,refreshToken需存储于服务端,其过期时间可以比JWT的过期时间要稍长。
在Spring Cloud微服务框架下,要实现接口的鉴权,最好的方式就是通过Spring Cloud Gateway网关服务来完成。网关提供了统一服务的访问接口,客户端在调用服务时,须先经过网关,网关进行权限的校验,对非法请求进行过滤后再进行请求的转发,以此来完成鉴权。
接下来,我们通过在网关中使用JWT实现一个简单的鉴权服务。
其授权基本流程:
1 用户提交账号密码,服务生成token和refreshToken并返回,其中refreshToken设置过期时间,并关联用户身份和当前的token;
2 用户请求服务时,前置网关对请求进行过滤,从请求中取出token,在验证通过后将token中包含的身份信息作为参数所加到当前请求;
3 用户请求到达具体的服务后,取出包含的身份信息,进行具体的业务处理;
token的刷新流程:
1 用户携带refreshToken参数请求token刷新接口,服务端在判断refreshToken未过期后,取出关联的用户信息和当前token,使用当前用户信息重新生成token,并将旧的token置于黑名单中,返回新的token;
2 用户携带新token重新进行请求;
该示例中JWT的实现使用了开源的Java JWT,地址为:https://github.com/auth0/java-jwt
public Map login(@RequestParam String userName,
@RequestParam String password){
Map resultMap = new HashMap<>();
//账号密码校验
if(StringUtils.equals(userName, "admin")&&
StringUtils.equals(password, "admin")){
//生成JWT
String token = buildJWT(userName);
//生成refreshToken
String refreshToken = UUID.randomUUID().toString().replaceAll("-","");
//保存refreshToken至redis,使用hash结构保存使用中的token以及用户标识
String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
stringRedisTemplate.opsForHash().put(refreshTokenKey,
"token", token);
stringRedisTemplate.opsForHash().put(refreshTokenKey,
"userName", userName);
//refreshToken设置过期时间
stringRedisTemplate.expire(refreshTokenKey,
refreshTokenExpireTime, TimeUnit.MILLISECONDS);
//返回结果
Map dataMap = new HashMap<>();
dataMap.put("token", token);
dataMap.put("refreshToken", refreshToken);
resultMap.put("code", "10000");
resultMap.put("data", dataMap);
return resultMap;
}
resultMap.put("isSuccess", false);
return resultMap;
}
private String buildJWT(String userName){
//生成jwt
Date now = new Date();
Algorithm algo = Algorithm.HMAC256(secretKey);
String token = JWT.create()
.withIssuer("MING")
.withIssuedAt(now)
.withExpiresAt(new Date(now.getTime() + tokenExpireTime))
.withClaim("userName", userName)//保存身份标识
.sign(algo);
return token;
}
public Map refreshToken(@RequestParam String refreshToken){
Map resultMap = new HashMap<>();
String refreshTokenKey = String.format(jwtRefreshTokenKeyFormat, refreshToken);
String userName = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
"userName");
if(StringUtils.isBlank(userName)){
resultMap.put("code", "10001");
resultMap.put("msg", "refreshToken过期");
return resultMap;
}
String newToken = buildJWT(userName);
//替换当前token,并将旧token添加到黑名单
String oldToken = (String)stringRedisTemplate.opsForHash().get(refreshTokenKey,
"token");
stringRedisTemplate.opsForHash().put(refreshTokenKey, "token", newToken);
stringRedisTemplate.opsForValue().set(String.format(jwtBlacklistKeyFormat, oldToken), "",
tokenExpireTime, TimeUnit.MILLISECONDS);
resultMap.put("code", "10000");
resultMap.put("data", newToken);
return resultMap;
}
@Component
public class AuthFilter implements GlobalFilter, Ordered {
private static final Logger LOGGER = LoggerFactory.getLogger(AuthFilter.class);
@Value("${jwt.secret.key}")
private String secretKey;
@Value("${auth.skip.urls}")
private String[] skipAuthUrls;
@Value("${jwt.blacklist.key.format}")
private String jwtBlacklistKeyFormat;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public int getOrder() {
return -100;
}
@Override
public Mono filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String url = exchange.getRequest().getURI().getPath();
//跳过不需要验证的路径
if(Arrays.asList(skipAuthUrls).contains(url)){
return chain.filter(exchange);
}
//从请求头中取出token
String token = exchange.getRequest().getHeaders().getFirst("Authorization");
//未携带token或token在黑名单内
if (token == null ||
token.isEmpty() ||
isBlackToken(token)) {
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"401\",\"msg\": \"401 Unauthorized.\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//取出token包含的身份,用于业务处理
String userName = verifyJWT(token);
if(userName.isEmpty()){
ServerHttpResponse originalResponse = exchange.getResponse();
originalResponse.setStatusCode(HttpStatus.OK);
originalResponse.getHeaders().add("Content-Type", "application/json;charset=UTF-8");
byte[] response = "{\"code\": \"10002\",\"msg\": \"invalid token.\"}"
.getBytes(StandardCharsets.UTF_8);
DataBuffer buffer = originalResponse.bufferFactory().wrap(response);
return originalResponse.writeWith(Flux.just(buffer));
}
//将现在的request,添加当前身份
ServerHttpRequest mutableReq = exchange.getRequest().mutate().header("Authorization-UserName", userName).build();
ServerWebExchange mutableExchange = exchange.mutate().request(mutableReq).build();
return chain.filter(mutableExchange);
}
/**
* JWT验证
* @param token
* @return userName
*/
private String verifyJWT(String token){
String userName = "";
try {
Algorithm algorithm = Algorithm.HMAC256(secretKey);
JWTVerifier verifier = JWT.require(algorithm)
.withIssuer("MING")
.build();
DecodedJWT jwt = verifier.verify(token);
userName = jwt.getClaim("userName").asString();
} catch (JWTVerificationException e){
LOGGER.error(e.getMessage(), e);
return "";
}
return userName;
}
/**
* 判断token是否在黑名单内
* @param token
* @return
*/
private boolean isBlackToken(String token){
assert token != null;
return stringRedisTemplate.hasKey(String.format(jwtBlacklistKeyFormat, token));
}
}
完整代码见:GitHub
参考链接:
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html