jwt
- 1. JWT
- 什么是JWT
- JWT能做什么
- 为什么用JWT
- JWT结构
- JWT问题和趋势
- 2. jwt加解密工具,使用
- pom
- 工具类
- 测试
1. JWT
-
什么是JWT
- 官方定义 json web token是一个开放标准(rfc7519),它定义了一种紧凑的,自包含的方式,用于在各方之间以JSON对象安全的传输信息,此信息可以验证和信任,因为它是数字签名的。jwt可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公钥、私钥进行签名。
- 通俗解释 JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输,在传输过程中还可以完成数据加密、签名等相关处理。
-
JWT能做什么
- 授权
- 这是使用JWT的最常见的方案,一旦用户登录成功,系统生成一个JWT传递给用户,每个后续请求将包括JWT,从而允许用户访问该令牌允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。
- 信息交换
- JSON Web Token是在各方之间安全的传输信息的好方法,因为可以对JWT进行签名,所以您可以确保发件人是所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否被篡改。
- 授权
-
为什么用JWT
-
基于传统的session认证
- 我们知道http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能说明是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请
求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证
- 我们知道http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能说明是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请
-
认证流程
- cookie session
-
暴露的问题
- 每个用户经过我们的应用认证之后,我们的应用都要在服务做一次记录,以方便用户下次请求的别,通常而言session都是保存在内存中,而随着认证用户的增多,服务器的开销会明显增大
- 用户认证之后,服务器做认证记录,如果认证的记录被保存在内存中的话,这意妹着用户下次请求还必项要请求在这台服务器上,这样才能拿到授权的源,这样在分布式的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩属能力
- 因为是基于 cookie来进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
- 在前后端分离系统中就更加痛苦:前后端分离在应用解耦后增加了部署的复杂性,通常用户一次请求就要转发多次,如果用session每次携带 sessionid到服务器,服务器还要查询用户信息,如果同时用户很多,这些信息存储在服务器内存中,给服务器增加负担,还有就是CSRF(跨站请求伪造的攻击,session是基于 cookie进行用户识别的,cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击,还有就是
sessionid就是一个特征值,表达的信息不够丰富,不容易扩展,而且如果你后端应用是多节点部署,那么就需要实现 session共享机制,不方便集群应用
-
基于JWT认证
- 认证流程
-
认证流程
- 首先通过表单将自己的用户名和密码发送到后端的接口,这一过程一般是一个HTTP POST请求,建的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为 JWT Payload(负载),将其与头部分别进行bas64编码拼接后签名,形成一个JWT( Token),形成的JWT就是一个形同111.zzz,xxx的字符串
- token head. payload. singurater
- 后端将JWT作为登录成功的返回结果返回给前端,前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
- 前端在每次请求时将JWT放入 Http Header中的 Authorization位,(解决XSS和XSRF问题) HEADER
- 后端检查是否存在,如存在验证JWT的有效性,例如,检查签名是否正确:检查 Token是否过期:检查Token的接收方是否是自己(可选)
- 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果。
-
JWT优势
- 简洁:可以通过URL,POST参数或者在 HTTP header发送,因为数据量小,传输速度也很快
- 自包含:负载中包含了所有用户所需要的信息,避免了多次查询数据库
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持
- 不需要在服务器保存会话信息,特别适用于分布式微服务。
-
-
JWT结构
-
令牌组成
- 标头( Header)
- 有效负载( Payload)
- 签名( Sionature)
因此,JWT通常如下所示:xxx.yyy.zzz
-
Header
标头通常由两部分组成:令牌的类型(即JWT和所使用的签名算法,例如HMAC SHA256或RSA,它会使Base64编码组成JWT结构的第一部分。
注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程{"alg":"HS256","typ":"JWT" }
-
Payload
令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用Base64编码组成
JWT结构的第二部分{"id":666,"name":"bitqian666","age":19 }
-
Signature
前面两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息, Signature需要使用编码后的 header和 payload以及我们提供的一个密钥,然后使用 header中指定的签名算法(HS256)进行签名,签名的作用是保证JWT没有被慕改过 -
签名目的
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的
如果要对新的头部和负载进行签名,在不知道服务器加密时使用的密钥的话,得出来的签名也是不一样的 -
信息安全问题
在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。比如,我们传输的是用户的 User ID,这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现web应用的单点登录。
-
-
JWT问题和趋势
- JWT默认不加密,但可以加密。生成原始令牌后,可以使用改令牌再次对其进行加密。
- 当JWT未加密方法时,一些私密数据无法通过JWT传输。
- JWT不仅可用于认证,还可用于信息交换。善用JWT有助于减少服务器请求数据库的次数。
- JWT的最大缺点是服务器不保存会话状态,所以在使用期间不可能取消令牌或更改令牌的权限。也就是说,一旦JWT签发,在有效期内将会一直有效。
- JWT本身包含认证信息,因此一旦信息泄露,任何人都可以获得令牌的所有权限。为了减少盗用,JWT的有效期不宜设置太长。对于某些重要操作,用户在使用时应该每次都进行进行身份验证。
- 无感刷新JWT,后端接收JWT解析获取过期时间,如果快过期,生成新的令牌(业务数据和之前一致)
- 为了减少盗用和窃取,JWT不建议使用HTTP协议来传输代码,而是使用加密的HTTPS协议进行传输。
2. jwt加解密工具,使用
pom
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt --><dependency><groupId>com.auth0</groupId><artifactId>java-jwt</artifactId><version>3.10.3</version></dependency>
工具类
package top.bitqian.config;import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;/*** @author echo lovely* @date 2020/12/4 21:46*/public class JwtUtil {//密钥(不能泄露 、可多次加盐加密再存在合适的地方)public static final String SECRET = "defAu&TJHVhc$%WW^JJG";//过期时间:秒public static final int EXPIRE = 300;/*** JWT 添加至HTTP HEAD中的前缀*/private static final String JWT_SEPARATOR = "Bearer ";/*** 生成Token* @param userName userName* @param userPwd userPwd* @return token token* @throws Exception create token ex*/public static String createToken(String userName, String userPwd) throws Exception {try {//日历对象//当前时间Calendar nowTime = Calendar.getInstance();//加30秒nowTime.add(Calendar.SECOND, EXPIRE);Date expireDate = nowTime.getTime();//标头 可以不写,默认值就是下方所定义的mapMap<String, Object> map = new HashMap<>();// 算法map.put("alg", "HS256");// 类型 jwt加密map.put("typ", "JWT");return JWT.create().withHeader(map)//头 有默认值 可以不写//负载 业务数据.withClaim("userPwd", userPwd).withClaim("userName", userName).withClaim("myId", "aa").withSubject("subject")//可以不写.withIssuedAt(new Date())//签名时间.withExpiresAt(expireDate)//过期时间.sign(Algorithm.HMAC256(SECRET));//签名} catch (Exception e) {throw new Exception(e);}}/*** 验证Token* @param token token* @return token map* @throws RuntimeException 凭证已过期,请重新登录*/public static Map<String, Claim> verifyToken(String token) throws RuntimeException {JWTVerifier verifier = JWT.require(Algorithm.HMAC256(SECRET)).build();DecodedJWT jwt;try {jwt = verifier.verify(token);}catch (Exception e){throw new RuntimeException("凭证已过期,请重新登录");}return jwt.getClaims();}/*** 解析Token* @param token token* @return claim*/public static Map<String, Claim> parseToken(String token){DecodedJWT decodedJWT = JWT.decode(token);return decodedJWT.getClaims();}}
测试
public static void main(String[] args){try {// 创建token,传入参数String token = JwtUtil.createToken("your token param", "your token param");// 获取tokenSystem.out.println("token=" + token);//Thread.sleep(5000);Map<String, Claim> map = JwtUtil.verifyToken(token);//Map<String, Claim> map = JwtUtil.parseToken(token);//遍历for (Map.Entry<String, Claim> entry : map.entrySet()){if (entry.getValue().asString() != null){System.out.println(entry.getKey() + "===" + entry.getValue().asString());}else {System.out.println(entry.getKey() + "===" + entry.getValue().asDate());}}}catch (Exception e){e.printStackTrace();}}