Cookie、session、token、JWT

1. 认证与授权

认证(Authentication)和授权(Authorization)是比较容易混淆的概念。

  1. 认证:验证你身份的凭据(用户名/ID、密码等),通过这个凭据,系统就能判断出你是平台的用户。
  2. 授权:发生在认证之后,掌管我们访问系统的权限,也就是你有权访问什么,无权访问什么。

简而言之的话就是:

  1. 认证:你是谁。
  2. 授权:你有权限干什么。

HTTP是无状态的,客户端与服务端会话完成后,服务端不会保存会话信息。也就是说,服务端并不知道是谁连接了他。那服务器怎么知道是谁在登录?通过认证,证明你是你自己。

互联网中的认证:

  1. 用户名密码登录
  2. 邮箱发送登录链接登录
  3. 手机号接收验证码登录

互联网中的授权:

  1. APP询问授予权限
  2. 小程序授予权限
  3. 会员权限
  4. 封禁限制权限

实现认证和授权的前提就是需要一种证书标记访问者的身份,这就是凭证。
例如我们的居民身份证,能证明持有人的身份。
例如网站有游客模式和登录模式,游客模式可以游览,但是想发言的话只能登录,获得令牌,令牌就是一个凭证。

2. 什么是cookie?

cookie是name=value键值对。
cookie存储在客户端,请求后,服务器发送回浏览器,浏览器在下次向同一服务器发送请求时,作为参数携带,并发送到服务器上。
cookie是一个具体的东西,指的是浏览器实现的一种数据存储功能。
cookie是不可跨域的,每个cookie绑定单一的域名。

2.1 cookie和localStorage区别

cookielocalStorage
存储量4K5M
请求携带会被带到HTTP请求中不会带到 HTTP 请求中
生命周期浏览器关闭,cookie消失,也可设置过期时间持久化存储,除非清除浏览器缓存
爬虫可以被爬虫抓取不能被爬虫抓取
可操作性cookie可以存储数据,还可以设置一些操作属性只是存储数据
使用场景客户端与服务端的信息传递客户端的数据存储

3. 什么是session?

session是基于cookie实现的,session存储在服务器端,每个session都会有一个对应的sessionID。
sessionID会被存储到客户端的cookie的value,对应的name为JSESSIONID。
浏览器第二次访问服务器时,服务端从cookie中获取sessionID,再查找对应的session信息。如果能找到,就找到了用户信息,证明可以登录。
session的缺陷是:如果web服务器做了负载均衡,那么下次发起请求到另一台服务器时,session会丢失。
解决方式:单独使用一台服务器存储所有session,每台服务器都去这台服务器上找。

sessionID是连接cookie和session的一道桥梁。

4. cookie和session概念区别?

cookiesession
存储位置客户端服务端
存储数据类型只能是字符串可以是任意类型
存储数据量4k无限制
生命周期浏览器关闭,cookie消失,也可设置过期时间一般较短,半小时左右
安全性较不安全较安全

5. 什么是token?

token:访问资源接口(API)时所需要的资源凭证。
token代替的就是sessionID的位置。

token身份验证过程:

  1. 客户端通过用户名和密码发送请求。
  2. 服务端验证用户信息是否存在。
  3. 服务端将登录凭证(用户名、密码等)做数字签名得到token给客户端。
  4. 客户端储存token,用于再次发送请求放入请求头。
  5. 服务端验证token,进行解密和签名认证,并返回数据。

存储token的是cookie或localStorage,token被放到HTTP的header里。
用解析token的时间换取session的存储空间,减轻服务器的压力,减少频繁查询数据库。

6. session和token概念区别?

sessiontoken
存储位置存储在服务端存储在客户端
状态化有状态无状态
跨域可跨域不可跨域

如果你的用户数据可能需要和第三方共享,或者允许第三方调用 API 接口,用 token。
session对于非浏览器的客户端或手机移动端不适用,因为session依赖于cookie,而移动端没有cookie。
前后端分离系统中不适用session,后端部署复杂,前端发送的请求往往经过多个中间件到达后端,cookie中关于session的信息会转发多次。

7. 项目中实现token

7.1 使用JWT(Json Web Token)

JWT的优势是:JWT数据量小,传输速度很快。由于JWT是以JSON加密形式保存在客户端的,所以JWT可以跨语言。

JWT结构:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输时会将JWT的三部分分别进行Base64编码后连接形成最终的字符串,它的算法是这样的:

JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

了解一下其组成参数

Header

{
  "alg": "HS256",
  "typ": "JWT"
}

alg表示签名使用的算法,默认为HMAC SHA256,typ表示令牌的类型,JWT令牌统一写为JWT。

Payload

{
  "userName": "zhangsan",
  "dept": "safeAI",
  "userId": 3
}

放入我们自定义的私有字段,比如用户信息数据。

需要注意一点:默认情况下JWT是未加密的,因为只是采用Base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息。

Signature
对上面两部分数据签名,需要使用Base64编码后的header和payload数据,通过指定算法生成哈希,保证数据不会被篡改。
首先需要指定一个密钥(secret),该密钥仅仅保存在服务器中,不能向用户公开。然后使用HMAC SHA256算法(Payload里指定)根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)

计算出签名哈希后,JWT头、有效载荷和签名哈希三个部分组成一个字符串,每个部分用.分隔,构成整个JWT对象。例如:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
.eyJkZXB0IjoiaWt1buWQjuaPtOS8miIsInVzZXJOYW1lIjoi5byg5LiJIiwiZXhwIjoxNjY1NjMwMjc1LCJ1c2VySWQiOiIzIn0
.Oy82soyC8JGNFUzlZsZEC17Srxb6nokeBQHlonlxxkE

服务端收到JWT后,怎么处理呢?
Header和Payload可以直接用Base64解码出原文,可以从Header获取哈希签名算法,从Payload获取有效数据。
Signature使用的是SHA256这种不可逆的算法,无法解码成原码,它的作用是检验token有没有被篡改。
服务端获取header的加密算法后,利用该算法加上密钥secretKey对header、payload进行加密,比较加密后的数据和客户端发来的是否一致。secretKey对于MD5的摘要算法,代表的是盐值。

7.2 在Java中使用JWT

我们给JWT的加密与解密方法封装成工具类

先导入JWT的依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.19.0</version>
</dependency>

写一个JWT的工具类,比如JWTHelper
然后就可以写数字签名生成token的静态方法了。

我们想把什么参数写进token,就传参进去,这就是JWT加密。

public static String sign(String userName, String dept, Integer userId, String secret, long time) {
    Date date = new Date(System.currentTimeMillis() + time);
    Algorithm algorithm = Algorithm.HMAC256(secret);
    String userIdString = null;
    if (userId != null) {
        userIdString = userId.toString();
    }
    return JWT.create().withClaim("userName", userName)
            .withClaim("dept", dept)
            .withClaim("userId", userIdString)
            .withExpiresAt(date).sign(algorithm);
}

比如这里,我们就把用户名、部门、用户id的信息,外加时间戳和数字签名通过JWT生成token。

下面是JWT解密。

我们创建一个UserInfoDTO类接收拿到的个人信息。

@Data
public class UserInfoDTO {

    private String userName;
    private String deptName;
    private Integer userId;
    
}
public static UserInfoDTO getInfo(String token) {
    try {
        DecodedJWT jwt = JWT.decode(token);
        UserInfoDTO userInfoDTO = new UserInfoDTO();
        userInfoDTO.setUserName(jwt.getClaim("userName").asString());
        userInfoDTO.setDeptName(jwt.getClaim("deptName").asString());
        String userId = jwt.getClaim("userId").asString();
        if (StringUtils.isNotBlank(userId)) {
            userInfoDTO.setUserId(Integer.valueOf(userId));
        }
        return userInfoDTO;
    } catch (JWTDecodeException e) {
        return null;
    }
}

当然,我们可以检验一下数字签名,看看token是否被人篡改了。

public static DecodedJWT decode(String token){
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(CommonContext.TOKEN_SECRET)).build();
    DecodedJWT decodedJWT = jwtVerifier.verify(token);
    return decodedJWT;
}

我们写一个测试方法

@Test
public void test02() {

//        加密
    String token = JWTHelper.sign("张三", "ikun后援会", 3, CommonContext.TOKEN_SECRET, 30000);
    System.out.println(token);

//        解密
    UserInfoDTO userInfoDTO = JWTHelper.getInfo(token);
    System.out.println(userInfoDTO.toString());

//        校验
    DecodedJWT decode = JWTHelper.decode(token);
    System.out.println(token.equals(decode.getToken()));
}

得到的结果是

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJkZXB0TmFtZSI6ImlrdW7lkI7mj7TkvJoiLCJ1c2VyTmFtZSI6IuW8oOS4iSIsImV4cCI6MTY2NTY1MDY2NCwidXNlcklkIjoiMyJ9.ORNDmXHKhF7qQZaLuwlfVQ_kwgE5sivWd3d-5tibmYo
UserInfoDTO(userName=张三, deptName=ikun后援会, userId=3)
true

所以我们在之后,可以先判断一下token有无被篡改,如果无,再返回具体信息。

7.3 实际开发中使用JWT流程

在实际开发中,可以使用如下流程做登录:

  1. 在登录验证通过后,使用UUID算法生成一个随机token,并将这个token作为key的一部分,用户信息作为value存入Redis,并设置过期时间,这个过期时间就是登录失效的时间。

  2. 将第1步中生成的随机token作为JWT的payload生成JWT字符串,并使用一个密钥进行签名,确保JWT的安全性。最后,将JWT字符串返回给前端。

  3. 前端之后每次请求都在请求头中的Authorization字段中携带JWT字符串。

  4. 后端定义一个拦截器,每次收到前端请求时,都先从请求头中的Authorization字段中取出JWT字符串并进行验证,验证通过后解析出payload中的随机token,然后再用这个随机token得到key,从Redis中获取用户信息,如果能获取到就说明用户已经登录。如果解析出的随机token不存在于Redis中,则返回401 Unauthorized状态码并提示用户未登录。

如何保证JWT的安全性?

因为JWT是在请求头中传递的,所以为了避免网络劫持,推荐使用HTTPS来传输,更加安全。
JWT的哈希签名的密钥是存放在服务端的,所以只要服务器不被攻破,理论上JWT是安全的。因此要保证服务器的安全。
为了防止暴力穷举攻击,建议定期更换服务端的哈希签名密钥(相当于盐值),周期建议设置为3-6个月,并在实施前通知所有参与方。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值