JWT
概念:它是json web token的缩写,它将用户信息保存到token中,服务器不保存任何用户信息,服务器通过使用保存的秘钥验证token的正确性,只要正确即通过验证。
优点:
1.在分布式系统中很好的解决了单点登录的问题,很容易解决session共享的问题
2.因为json的通用性,所以JWT是可以跨语言支持的,像C#,JavaScript,NodeJS,PHP等许多语言都可以使用
3.因为由了payload部分,所以JWT可以在自身存储一些其它业务逻辑所必要的非敏感信息
4.便于传输,jwt的构成非常简单,字节占用很小,所以它是非常便于传输的
5.它不需要在服务端保存会话信息,所以它易于应用的扩展
缺点:
1.无法作废已颁布的令牌不易应对过期的数据
2.不应该在jwt的payload部分存储敏感信息,因为该部分是客户端可解密的部分
3.保护好secret私钥。该私钥非常重要
jwt消息构成,一个token 分为三部分:
头部:两部分组成:声明类型,声明加密的算法 通常直接使用HMAC SHA256
完整的头部就像下面这样的JSON
{
'typ':'JWT',
'alg':'HS256'
}
然后将头部进行base64加密(该加密是可以对称解密的),构成了第一部分
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
荷载 :基本上有两种类型的数据,标准中注册的声明的数据,自定义数据 两部分内部做base64加 密,最终进入jwt的chaims里存放
签证 :
jwt的第三部分是一个签证信息,这个签证信息由三部分组成:
header(base64后的)
payload(base64后的)
secred
这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行secret组合加密,然后就构成了jwt的第三部分。
注意:secret是保存在服务器端的,jwt的签发也是在服务端的,secret就是用来进行jwt的签发和jwt的验证,所以它就是你服务端的私钥,在任何场景都不应该流露出去,一旦客户端得知这个secret,那就意味着客户端可以自我签发jwt了
三者中间用 . 隔开 例:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
基于token的鉴权机制:
类似于http的无状态 ,它不需要在服务端保存用户的认证信息或回话信息,这样的话 就不用去考虑在哪一台服务器登录了,方便扩展。
大致流程:
用户通过名字和密码请求服务器
服务器进行用户信息验证
服务器通过验证发送给用户一个token
客户端存储这个token,并在每次请求时附加这个token值
服务器验证token 返回数据
这个token在每次请求时 必须发送给服务器,它应该放在请求头中,另外服务器要支持跨域请求策略
应用
一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token 这下我知道为何要加这个Bearer
}
})
那么接下来 咱们操作一把:
pom依赖:(jjwt框架可以更好的耍jwt)
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.10.5</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.10.5</version>
<scope>runtime</scope>
</dependency>
2.配置yml
auth:
security:
authentication:
jwt:
# This token must be encoded using Base64 (you can type `echo 'secret-key'|base64` on your command line)
secret: MDFjODJhZDU1MmQyOWM2Mjg4ZDc1Y2YxZDk5ODNkMGQzZDMzOTZhMTFlYTQ2MTIyNTlkMmYyNzkzNjhlZDM0ZTg3NzJlMTUyMjIzMGQ2NzY0M2EyOTgwZTJiODNkYjVjMzBhNTI3NTY5NGRlNjRhZmNhZWYwYjRjNjE2MmY5YjA= #这个secret在这个项目中唯一
# Token is valid 24 hours
token-validity-in-seconds: 86400
token-validity-in-seconds-for-remember-me: 2592000
创建解析token:
@Component
public class TokenProvider {
/**
* 也可以将这三个字段放在一个实体类中去取对应的yml中的值
*/
@Value("${auth.security.authentication.jwt.secret}")
private String secret;
@Value("${auth.security.authentication.jwt.token-validity-in-seconds}")
private Long validityInSeconds;
@Value("${auth.security.authentication.jwt.token-validity-in-seconds-for-remember-me}")
private Long tokenValidityInSecondsForme;
/**
* 将secret值取到进行加密之后的值给了这个key 用加密之后的key值作为签证
*/
private Key key;
@PostConstruct //构造方法之后,servlet的init之前执行
public void init() {
System.out.println("执行啦。。init。。");
byte[] keyBytes = Decoders.BASE64.decode(secret);
//将secret加密之后在初始化时就给了key 加密解密这个值得相同唯一
this.key = Keys.hmacShaKeyFor(keyBytes);
System.out.println(key);
validityInSeconds = 1000 * validityInSeconds;
tokenValidityInSecondsForme = 1000 * tokenValidityInSecondsForme;
}
/**
* 解析jwt字符串
* @param token
* @return
* @throws NullPointerException
*/
public Map<String,Object> parseToken(String token) throws NullPointerException{
System.out.println("我在调用 这个解析的方法");
token = token.replaceAll(" ", "").substring(6);
System.out.println(token);
System.out.println(key);
Claims claims = Jwts.parser()
.setSigningKey(key)
.parseClaimsJws(token)
.getBody();
if(claims.isEmpty()){
throw new NullPointerException();
}
//取出生成时存入的map的值
return claims;
}
public String pusuCreateToken(Map<String, Object> claimMap) {
String topicKey = (String) claimMap.get("topicKey");
return getJwtString(topicKey, claimMap);
}
/**
* 生成jwt字符串
* @param str 自定义str 作为sub条件
* @param claimMap 用户登录之后返回的信息字段 进行加密 进行传输 解析之后会取到
* @return
*/
public String getJwtString(String str,Map<String, Object> claimMap){
//当前毫秒数加过期时间 就是这个jwt的过期时间
long time = System.currentTimeMillis() + this.validityInSeconds;
Date validity = new Date(time);
return Jwts.builder()
.setSubject(str)
.addClaims(claimMap)
.signWith(key, SignatureAlgorithm.HS512)
.setExpiration(validity)
.compact();
}
}
JWT之token过期 ------ 检验token是否过期的方法:
public boolean validateToken(String authToken) {
try {
Jwts.parser().setSigningKey(key).parseClaimsJws(authToken);
return true;
} catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature.");
log.trace("Invalid JWT signature trace: {}", e);
} catch (ExpiredJwtException e) {
log.info("Expired JWT token.");
log.trace("Expired JWT token trace: {}", e);
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token.");
log.trace("Unsupported JWT token trace: {}", e);
} catch (IllegalArgumentException e) {
log.info("JWT token compact of handler are invalid.");
log.trace("JWT token compact of handler are invalid trace: {}", e);
}
return false;
}
如果token过期的话 就需要将其中的荷载信息 即生成token时的map信息取出来,然后拿着这个新的map去重新生成新的token。
坑:如果token已经过期了,那么调用解析token的方法,就会报错,无法解析,只能通过自己手动去取出token的第二部分荷载,然后去生成新的token。
因为token的第二部分是由base64加密而来,所以直接取到第二部分解密就行
@PostMapping(value = "/pubSub/replaceToken")
@Timed
public ResponseEntity<?> replaceToken(@Validated @RequestBody ClientTokenVM vm) {
String newToken = null;
try {
//这是校验
tokenProvider.validate(vm.getToken());//如果过期直接执行catch
} catch (ExpiredJwtException e) {
//如果是过期token ,取出map信息。
String[] splitToken = vm.getToken().split("\\.");
try {
byte[] bt = (new BASE64Decoder()).decodeBuffer(splitToken[1]);
//将其转成map
Gson gson = new Gson();
Map claims = gson.fromJson( new String(bt), HashMap.class);
//重新去更换token并返回
newToken = tokenProvider.pusuCreateToken(claims);
} catch (IOException e1) {
log.error("base64解码异常");
}
}
return ResponseEntity.ok(JsonResult.ok().build(newToken));
}
(内涵小知识:将string类型的数据转换为map,前提是该字符串必须为json格式,利用gson进行转换很方便呀)