JWT快速入门
1.什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准(RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。官网:https://jwt.io
简单理解:jwt就是通过json形式作为web应用中心的令牌,用在各方之间安全的将信息作为json对象传输,在数据传输过程中还可以完成数据加密、签名等相关处理。
2.JWT能做什么
2.1 授权
这是使用jwt的最常见方案,每个后续请求包括jwt,从而允许用户访问该令牌的路由,服务和资源,单点登录是当今广泛使用jwt的一项功能,因为他的开销很小并且可以在不同的域中轻松使用。
2.2 信息交换
jwt是在各方之间安全的传输信息的好方法。因为可以对jwt进行签名,(例如,使用公钥/私钥时),所以您可以确保发件人是他们所说的人,此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到纂改。
3.为什么是JWT
3.1 基于传统的session认证
认证方式:
http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行。因为根据http协议,我们并不能知道是哪个用户发出来的请求,所以为了让我们的应用能识别是哪个用户发送的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的session认证。
暴露问题:
- 1.每个用户经过认证之后,我们的应用都要在服务端做一次记录,为方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
- 2.用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡的能力,这也意味着限制了应用的扩展能力
- 3.因为是基于cookie来进行识别的,cookie如果被拦截,用户就会很容易受到跨站请求伪造的攻击
3.2 基于JWT认证
认证流程:
- 首先,前端通过web表单将自己的用户名和密码发送到后端的接口,这一过程一般是post请求,建议的方式是ssl加密的传输(https协议),从而避免敏感信息被嗅探。
- 后端核对用户名和密码成功后,将用户的id等其他信息作为jwt payload(负载),将其与头部分分别进行Base64编码拼接后签名,形成一个jwt,形成的jwt就是一个形同111.zzz.xxx格式的字符串(Header.Payload.Signature)
- 后端将jwt字符串作为登录成功的返回结果返回给前端,前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的jwt即可
- 前端在每次请求时将jwt放入http header中的Authorization位(解决xss和xsrf问题防伪拦截攻击)
- 后端检查是否存在,如存在验证jwt的有效性,例如,检查签名是否正确,检查token是否过期,检查token的接收方是否是自己
- 验证通过后后端使用jwt中包含的用户信息进行其他逻辑操作,返回相应结果
jwt优势:
- 简洁:可以通过URL,post参数或者在http header发送,因为数据量小,传输速度也很快
- 自包含:负载中包含了所有用户所需的信息,避免了多次查询数据库
- 因为token是以json加密的形式保存在客户端的,所以jwt是跨语言的,原则上任何web形式都支持
- 不需要在服务端保存回话信息,特别适用于分布式微服务
4.JWT结构
4.1 令牌组成
令牌由三部分组成:标头(Header)、有效载荷(Payload)、签名(Signature)
因此,JWT通常如下所示:xxxx.yyyy.zzzz ; Header.Payload.Signature;例如:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
4.2 Header
标头通常由两部分组成:令牌的类型和所使用的的签名算法,例如HMAC SHA256或RSA。它会使用base64编码组成JWT结构的第一部分
注意:base64是一种编码,也就是说,它是可以被翻译会原来的样子的,它并不是一种加密过程。
{ "alg" : "HS256", "typ" : "JWT" }
4.3 Payload
令牌的第二部分是有效负载,其中包含声明,声明是有关实体(通常是用户)和其他数据的声明,同样的,它会使用base64编码组成JWT结构的第二部分
{ "sub" : "123456789", "name" : "Eric Fang", "admin" : true }
4.4 Signature
前面两部分都是使用base64编码的,即前端可以解开知道里面的信息,Signature 需要使用编码后的Header和Payload以及我们提供的一个密钥,然后使用Header中指定的签名算法(HS256)进行签名。签名的作用是保证JWT没有被纂改过,如:
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload).secret)
4.5 签名目的
最后一步签名的过程,实际上是对头部以及负载内容进行签名,防止内容被纂改,如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的,如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的。
5.JWT的使用
5.1 安装依赖
创建一个maven项目,在pom.xml安装依赖
<!--jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency>
5.2 创建工具类
这里我提供了一个工具类,使用jwt来生成token字符串,根据token获取用户信息等方法。
public class JwtUtils { //常量 public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间 public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥 //生成token字符串的方法 public static String getJwtToken(String id, String nickname){ String JwtToken = Jwts.builder() .setHeaderParam("typ", "JWT") .setHeaderParam("alg", "HS256") .setSubject("guli-user") .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .claim("id", id) //设置token主体部分 ,存储用户信息 .claim("nickname", nickname) .signWith(SignatureAlgorithm.HS256, APP_SECRET) .compact(); return JwtToken; } /** * 判断token是否存在与有效 * @param jwtToken * @return */ public static boolean checkToken(String jwtToken) { if(StringUtils.isEmpty(jwtToken)) return false; try { Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 判断token是否存在与有效 * @param request * @return */ public static boolean checkToken(HttpServletRequest request) { try { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return false; Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); } catch (Exception e) { e.printStackTrace(); return false; } return true; } /** * 根据token字符串获取会员id * @param request * @return */ public static String getMemberIdByJwtToken(HttpServletRequest request) { String jwtToken = request.getHeader("token"); if(StringUtils.isEmpty(jwtToken)) return ""; Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken); Claims claims = claimsJws.getBody(); return (String)claims.get("id"); } }
然后通过调用jwt工具类的方法就可以实现生成token
6.登录实现流程
6.1 实现步骤
- 调用接口返回token字符串;
- 把第一步返回的token字符串放到token里面;
- 创建前端拦截器,判断cookie里面是否有token字符串,如果有,把token字符串放到header(请求头)里面
- 根据token值,调用接口,根据token获取用户信息,为了首页面显示,把调用接口返回的用户信息放到cookie里面
- 在首页面显示用户信息,从第四步cookie里面获取用户信息
6.2 代码实现
前端代码
submitLogin(){ //第一步,调用接口返回token字符串; login(this.user).then(response => { if(response.data.success){ //第二步,把token存在cookie中、也可以放在localStorage中(参数的意义:cookie名称,token,作用范围) cookie.set('edu_token', response.data.data.token, { domain: 'localhost' }) //第四步,调用接口根据token获取用户信息 getLoginUserInfo().then(response => { this.loginInfo = response.data.data.item //将用户信息记录cookie cookie.set('edu_ucenter', this.loginInfo, { domain: 'localhost' }) //跳转页面 window.location.href = "/"; }) } }) },
前端拦截器
拦截器写在request.js里面
// http request 拦截器 service.interceptors.request.use( config => { //debugger if (cookie.get('edu_token')) { //把获取的值放到header里面 config.headers['token'] = cookie.get('edu_token'); } return config }, err => { return Promise.reject(err); })
跳转页面获取用户信息
//创建方法,获取cookie对象 showInfo() { //从cookie获取用户信息 var userStr = cookie.get('edu_ucenter') if (userStr) { this.loginInfo = JSON.parse(userStr) } }
一点点学习,一丝丝进步。不懈怠,才不会被时代所淘汰!