JWT公司的主流Json Web Token 令牌 如何使用,取代session,还可以运用分布式认证
JWT简介:
JWT(JSON WEB TOKEN):JSON网络令牌,JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。它是在Web环境下两个实体之间传输数据的一项标准。实际上传输的就是一个字符串。广义上讲JWT是一个标准的名称;狭义上JWT指的就是用来传递的那个token字符串
jwt特点:
- JWT无需存储在服务器(不使用Session/Cookie),不占用服务器资源(也就是
Stateless无状态
的),也就不存在多服务器共享Session的问题 - 使用简单,用户在
登录
成功拿到Token
后,一般访问需要权限的请求时,在Header附上Token即可。
**JWT公司官网: **
jwt的token结构:
JWT的数据结构以及签发的过程
JWT由三部分构成:header(头部)、payload(载荷)和signature(签名)。
xxxxx.yyyyy.zzzzz
1、Header 头部信息:指定类型和算法
{
"alg": "HS256",
"typ": "JWT"
}
typ:用来标识整个token字符串是一个JWT字符串
alg:用来说明这个JWT签发的时候所使用的签名和摘要算法
常用的值以及对应的算法如下:
一般签发JWT的时候,header对应的json结构只需要typ和alg属性就够了。JWT的header部分是把前面的json结构,经过Base64Url编码之后生成出来的
2、Payload 荷载信息:存放Claims声明信息既主体信息组成。用来存储JWT基本信息,或者是我们的信息。
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
payload用来承载要传递的数据,它的json结构实际上是对JWT要传递的数据的一组声明,这些声明被JWT标准称为claims。当后面对JWT进行验证的时候,这些claim都能发挥特定的作用。
sub代表这个token的所有人,存储的是所有人的ID;
name表示这个所有人的名字;
admin表示所有人是否管理员的角色。
根据JWT的标准,这些claims可以分为以下三种类型:
- Reserved claims
保留的claims都是可选的,JWT标准里面针对它自己规定的claim都提供了有详细的验证规则描述,每个实现库都会参照这个描述来提供JWT的验证实现。
JWT标准的claim:
属性 | 全称 | 描述 | 认证 |
---|---|---|---|
iss | Issuser | 代表签发主体 | 完全匹配 |
sub | Subject | 代表的主体,它的所有人 | 完全匹配 |
aud | Audience | 代表JWT的接收对象 | 包含即可 |
exp | Expiration time | 代表这个JWT的过期时间 | 过期失败 |
nbf | Not Before | 生效时间 | 早于该时间失败 |
iat | Issued at | 签发时间,maxAge之类的验证 | 大于指定值失败 |
jti | JWT ID | JWT的唯一标识 | 完全匹配 |
Public claims
不重要Private claims
自定义的claim不会验证,除非明确告诉接收方要对这些claim进行验证以及规则才行;标准的claim知道如何进行验证。
把这个json结构做base64url编码之后,就能生成payload部分的串:
3、Signature
把前两者对应的Json结构进行base64url编码之后的字符串拼接起来和密钥放一起加密后的签名,验证是否是我们服务器发起的Token,secret是我们的密钥。使用base64拼接很容易破解,所以建议在传输过程中采用ssl加密是最稳妥的。
组成方式为: header.payload.signature
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJpc3MiOiLlvKDlvLoiLCJuYW1lIjoiNzg5IiwiZXhwIjoxNTI4MzY0MjU5LCJpYXQiOjE1MjgzNjMwNTl9.574koY-c9SqMNNzfvAWQuKEnimWeZAcoFQ5XudNWF3o
下面例子使用HMAC SHA256算法进行签名
HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
拿前面的header和payload串来测试,并把“secret”这个字符串就当成密钥来测试:
最后的结果B其实就是JWT需要的signature。不过对比我在介绍JWT的开始部分给出的JWT的举例:
在线工具生成的signature有细微区别,在于最后是否有“=”字符。这些实现库最后编码用base64url编码,在线工具都是bas64编码,导致编码结果有区别。
JWT的验证过程
WT的验证规则主要包括签名验证和payload里面各个标准claim的验证。验证成功的JWT,才能当做有效的凭证来使用。
- 签名验证:对JWT的完整性进行验证就是签名认证。
- 验证方法:把header做base64url解码,获取JWT签名算法,用该算法再次用同样的逻辑对header和payload做一次签名。获得的串与JWT本身的第三个部分的串比较。不同认为这个JWT被篡改过,验证失败了。
- 生成签名:接收方生成签名须使用跟JWT发送方相同的密钥,做好密钥的安全传递或共享。
签名认证是每个实现库都会自动做的,但是payload的认证是由使用者来决定的。因为JWT里面可能不会包含任何一个标准的claim,所以它不会自动去验证这些claim。
登录认证在签发JWT时,可以用sub存储用户的id,用exp存储它本次登录之后的过期时间,然后在验证的时候仅验证exp这个claim,以实现会话的有效期管理。
JWT 令牌注销或者销毁
痛点:当用户点击了“注销”按钮,用户的令牌在客户端会从授权认证服务移除,但此令牌仍旧是有效,可以被攻击者窃取到用于API调用,直至jsonwebtoken的有效时间结束。
解决方案:利用Redis撤销JSON Web Token产生的令牌,当用户点击注销按钮时。且令牌在Redis存储的时间与令牌在jsonwebtoken中定义的有效时间相同。当有效时间到了后,令牌会自动被Redis删除。应用检查各终端上传的令牌在Redis中是否存在。
客户端发起请求
服务器端获取Token
你该如何去理解JWT帮你做些什么?
首先前端登录后端通过用户名与密码验证成功后,使用jwt生成一个具备有效期的token令牌, 这个token里面存储着 token本身的结构,加上token的过期时间,和一个自己定义的字符串类似于加密签名所用的盐,这个盐只有你自己知道,还可以包含一些自定义的用户信息放在荷载信息里。
所涉及的jar包如下:
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 阿里巴巴json -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.4</version>
</dependency>
生成Token
public class JwtUtil {
private static Logger logger = Logger.getLogger(JwtUtil.class);
token的key 也是名 不要写成token这样,要按照规范来
public static final String TOKEN_HEADER = "Authorization";
token值的前缀,这是一种规范
public static final String TOKEN_PREFIX = "Bearer ";
加密时候用 是对称的秘钥盐
private static final String SECRET = "mrLang";
获取用户的功能使用的key
public static final String FUNCTS = "FUNCTS";
获取用户使用的key
public static final String USERINFO = "USER";
token的生命周期30分
private static final long EXPIRATION = 1800L;
/**
* 创建token令牌 以下为参数都是自定义信息
* @param loginName 一般我们放用户的唯一标识登录名
* @param functs 当前用户的功能集合,
* 本人的rbac权限比较个性化且很负责,一般你们放role角色就可以了
* @param user 当前用户
* @return
*/
public static String createToken(String loginName,
List<Object> functs, Users user) {
Map<String, Object> map = new HashMap<>(); //当前用户拥有的功能
map.put(FUNCTS, JsonUtil.set(functs)); //当前用户信息
map.put(USERINFO, JsonUtil.set(user));
设置HS256加密,并且把你的盐 放里,这里推荐使用SH256证书加密
String token = Jwts.builder()
.setSubject(loginName)//主题 主角是谁? 赋值登录名
.setClaims(map)
.setIssuedAt(new Date())//设置发布时间,也是生成时间
.setExpiration(new Date(System.currentTimeMillis()
+ EXPIRATION * 1000))//设置过期时间
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();//创建完成
return token;
}
公共获取自定义数据
public static Claims getTokenBody(String token) {
return Jwts.parser().setSigningKey(SECRET)
.parseClaimsJws(token).getBody();
}
验证Token是否过期
public static boolean isExpiration(String token) {
try {
return getTokenBody(token).getExpiration().before(new Date());
} catch (Exception e) {
return true;
}
}
获取自定义的信息
// 获取主角,登录名
public static String getUserName(String token) {
return getTokenBody(token).getSubject();
}
// 获取token中存储的功能
public static List<Object> getUserFuncts(String token) {
String str = getTokenBody(token).get(FUNCTS).toString();
List<Object> list = JsonUtil.getArray(str);
return list;
}
// 获取token存储的用户
public static Object getUser(String token) {
String str = getTokenBody(token).get(USERINFO).toString();
return JsonUtil.getObj(str);
}
刷新Token
public static String refreshToken(String token) {
if (isExpiration(token)) {
logger.info("token刷新失败!! 过期了!!"); return null;
}
// 获取用户 权限信息
String functs = getTokenBody(token).get(FUNCTS).toString();
String user = getTokenBody(token).get(USERINFO).toString();
String username = getTokenBody(token).getSubject();
Map<String, Object> map = new HashMap<>();
map.put(FUNCTS, JsonUtil.set(functs));
map.put(USERINFO, JsonUtil.set(user));
token = Jwts.builder().signWith(SignatureAlgorithm.HS256, SECRET)
.setClaims(map).setSubject(username)
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION * 1000))
.compact();
return token;
}
token发送给前端
传入当前用户的功能与用户信息,登录名生成token,写入response的返回头中,前端获取后保存在前端的本地缓存中,后续前端请求要把token放在头header里。
//登录成功之后
List<Object> functs=(List<Object>) authResult.getAuthorities();
//当前功能列表
String loginName=authResult.getName();//登录名
Users obj=(Users)authResult.getPrincipal();//用户信息
String token=JwtUtil.createToken(loginName,functs,obj);
//生成token TOKEN_HEADER= Authorization TOKEN_PREFIX=Bearer token值
response.setHeader(JwtUtil.TOKEN_HEADER,JwtUtil.TOKEN_PREFIX+token);
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_OK); //个人编写的视图对象
DTO dto=new DTO<>();
dto.setCode("000000");
dto.setMessage("认证通过");
PrintWriter pw=response.getWriter();
pw.write(JsonUtil.set(dto));//写入json
pw.flush();//强制刷新
pw.close();//关闭流
验证用户请求携带token
你可以自定义一个过滤器,或者使用某某框架,自定义拦截器或者aop。
String header = request.getHeader(JwtUtil.TOKEN_HEADER);
if (null == header || !header.toLowerCase().startsWith(JwtUtil.TOKEN_PREFIX)) {
// 如果头部 Authorization 未设置或者不是 basic 认证头部,则当前
// 请求不是该过滤器关注的对象,直接放行,继续filter chain 的执行
chain.doFilter(request, response);
return;
}
try {
String token = header.replace(JwtUtil.TOKEN_PREFIX, "");
// 验证token是否过期
if (JwtUtil.isExpiration(token)) {
throw new javax.security.sasl.AuthenticationException("token 验证不通过");
}
//檢查token是否能解析
Users user = (Users) JwtUtil.getUser(token);
if (null == user) {
throw new javax.security.sasl.AuthenticationException("token 验证不通过");
}
//验证成功
JWT签名算法中HS256和RS256有什么区别?
JWT签名算法:HS256 和 RS256。
签名实际上是一个加密的过程,生成一段标识(也是JWT的一部分)作为接收方验证信息是否被篡改的依据。
RS256 (采用SHA-256 的 RSA 签名) 是一种非对称算法, 它使用公共/私钥对: 标识提供方采用私钥生成签名, JWT 的使用方获取公钥以验证签名。由于公钥 (与私钥相比) 不需要保护,因此大多数标识提供方使其易于使用方获取和使用 (通常通过一个元数据URL)。
HS256 (带有 SHA-256 的 HMAC对称算法, 双方之间仅共享一个密钥。由于使用相同的密钥生成签名和验证签名,因此必须注意确保密钥不被泄密。
在开发应用的时候启用JWT,使用RS256更加安全,你可以控制谁能使用什么类型的密钥。另外,如果你无法控制客户端,无法做到密钥的完全保密,RS256会是个更佳的选择,JWT的使用方只需要知道公钥。
建议:
为了双重保险,建议您使用RS256,最好是生成一个证书读取使用证书里面的公钥加密,私钥留着以后每次前端请求验证token的合法性就可以了
jwt提供了方法可以验证token的合法性,过期时间,还可以根据token读取用户信息,这样你把token存储前端的localstorage中,
后端的缓存都省了
这样做安全么?
你要知道 RS256非对称加密都是一套体系的,公钥私钥是一组, 都存在的后端,别人不进你服务器盗取你就是安全的,要是真能登录你服务器那就直接删你库了,同样本文用大的对称加密也是 你自定定义的秘钥 盐 本文的mrlang都是存在你自己的服务器中,别人是不知道的,
分布式中如何验证Token?
你可以把token工具类copy到不同的机器上,让每台机器自己去做验证,只要你每台机器token工具类里存储的 盐或者一对秘钥 都一致 那么验证就会得到一致通过 ,这样做的好处就是 你不必远调用远程验证服务器了,所以真的很好用,换个思维想想 你去调用验证服务器还需要花费很多时间,这对访问量特别大的项目来说,压力真的不小,所以本文的JWT Token 不管作为单机,集群,分布式 你都值得拥有