一、引出session问题以及token鉴权
session问题
session出现的问题:
session都是保存在内存中,认证用户增多,服务端开销明显增大。
扩展性,若是认证的记录保存在某台服务器内存中时,意味着用户的下次请求只能够在该服务器内存中进行认证。对于分布式应用,限制了负载局衡的能力,意味着限制了引用扩展能力。
CSRF跨站攻击:由于session认证是基于浏览器发送请求携带指定cookie值到服务器来进行用户识别的,一旦cookie被截获或者说在其他网站点击了恶意链接用户就会很容易受到跨站请求伪造的攻击,恶意者可以通过某个用户的cookie来间接做一些额外的事情。
token鉴权机制
token的鉴权机制:http协议也是无状态的,不需要在服务端去保留用户的认证信息或者会话信息。这也就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录,为应用的扩展提供了遍历。
鉴权流程:简单来说就是服务器根据前端传来的用户名与密码生成token并返回前端,前端之后的请求都会携带该cookie来进行执行操作认证。
用户使用用户名密码来请求服务器。
服务器进行验证用户的信息。
服务器通过验证生成token发送给用户一个token。
客户端存储token,并在每次请求时附送上这个token值。
服务端验证token值,并返回数据。
注意:这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *。
二、认识JWT(三部分详细构成)
2.1、JWT构成
JWT(JSON WEB TOKEN)是由三段信息构成的,将这三段信息文本用.链接一起就构成了Jwt字符串.
第一部分:头部(header)。
第二部分:载荷(payload),携带的信息。
第三部分:签证(signature)。
JWT字符串示例:JWT解析网站
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分:header
包含两部分信息:①声明类型,这里是jwt。②声明加密的算法 通常直接使用 HMAC SHA256。
完整的头部:
{ 'typ': 'JWT', 'alg': 'HS256' }
对头部进行加密:该加密是可以对称解密的
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
第二部分:playload
载荷就是存放有效信息的地方。携带的信息,这些有效信息包含三个部分:
标准中注册的声明
iss: jwt签发者 sub: jwt所面向的用户 aud: 接收jwt的一方 exp: jwt的过期时间,这个过期时间必须要大于签发时间 nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个playload载荷:
{ "sub": "1234567890", "name": "John Doe", "admin": true }
进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
第三部分:signature(签证)
签证信息由三部分组成:
header (base64后的)
payload (base64后的)
secret
第三部分组成:需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分
js加密示例:
// 将header与payload各自使用base64加密并用.来连接组合 var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload); // 接着将组合好之后的字符串进行加盐加密,使用HMACSHA256来进行加密 var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最终我们将header、playload以及signature各自的加密字符串使用.连接即可组成最终的JWT。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
2.2、客户端实际应用token
客户端在接收到token之后再次发送请求时,一般是在请求头里加入Authorization,并加上Bearer标注:
fetch('api/user/1', { headers: { 'Authorization': 'Bearer ' + token } })
服务端会验证token,如果验证通过就会返回相应的资源。
2.3、JWT的大致流程
三、手写JWT
<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.7.10</version> </dependency>
思路:头部、playload部分进行Base64编码,对于密钥部分是将playload与盐结合并使用MD5加密
import cn.hutool.crypto.digest.MD5; import com.alibaba.fastjson.JSONObject; import java.util.Base64; /** * @ClassName JWTUtils * @Author ChangLu * @Date 2021/9/20 13:58 * @Description TODO */ public class JWTUtils { private static String secret = "jidjiof"; public static void main(String[] args) { JSONObject header = new JSONObject(); header.put("alg", "HS256"); header.put("typ", "jwt"); JSONObject playLoad = new JSONObject(); playLoad.put("id", "123456"); //头部与主体部分进行base64编码 String base64Header = Base64.getEncoder().encodeToString(header.toJSONString().getBytes()); String base64PlayLoad = Base64.getEncoder().encodeToString(playLoad.toJSONString().getBytes()); //组合:其中第三部分是拿playLoad的json字符串与盐合并进行MD5加密 String token = base64Header + "." + base64PlayLoad + "." + MD5.create().digestHex(playLoad.toJSONString() + secret); System.out.println("token:" + token); //解密 String base64PlayLoadStr = new String(Base64.getDecoder().decode(token.split("\\.")[1]));//拿到playload编码前字符串 String jwtSecret = token.split("\\.")[2];//拿取到MD5加密后密钥 //判断是否有没有中途修改 System.out.println(MD5.create().digestHex(base64PlayLoadStr + secret).equals(jwtSecret)); } }
对于我们自己实现的话还是有许多的不足之处,例如设置有效时间、相同的内容会产生相同的token等等。
有效时间:可以另外在playload中设置描述有效时间的属性值。
相同的内容产生不同的token字符串:可以在属性中添加一个时间戳,此时我们每次生成的都会不一样。