JWT 实现登录认证功能及Cookie、Session 和 Token的区别

发布时间:2023年12月24日

前言

认证功能是项目基础建设之一,要实现认证功能,很容易就会想到 JWT 或者 Session,但是两者有啥区别?各自的优缺点?

一、JSON Web Token

1.1 JWT 介绍

JWT 是 JSON Web Token 的缩写,是一种轻量级的身份验证和授权的标准。通过数字签名的方式,以 JSON 对象为载体,在不同服务终端之间进行安全的数据传输。

JWT 通常用于Web 应用程序中的身份认证和授权,它通过在用户登录后生成一个包含用户信息和过期时间等数据的 JSON 格式令牌,并将其使用密钥进行加密,然后在之后的请求中将该令牌发送给服务器,服务器可以根据令牌中的信息验证用户身份并进行授权操作。

1.2 使用 JWT 的原因

JWT 相较于传统的基于会话(Session)的认证机制,具有以下优势:

  1. 无需服务器存储状态: 传统的基于会话的认证机制需要服务器在会话中存储用户的状态信息,包括用户的登录状态、权限等。而使用 JWT,服务器无需存储任何会话状态信息,所有的认证和授权信息都包含在 JWT 中,使得系统可以更容易地进行水平扩展。
  2. 跨域支持: 由于 JWT 包含了完整的认证和授权信息,因此可以轻松地在多个域之间进行传递和使用,实现跨域授权。
  3. 适应微服务架构: 在微服务架构中,很多服务是独立部署并且可以横向扩展的,这就需要保证认证和授权的无状态性。使用 JWT 可以满足这种需求,每次请求携带 JWT 即可实现认证和授权。
  4. 自包含: JWT 包含了认证和授权信息,以及其他自定义的声明,这些信息都被编码在 JWT 中,在服务端解码后使用。JWT 的自包含性减少了对服务端资源的依赖,并提供了统一的安全机制。
  5. 扩展性: JWT 可以被扩展和定制,可以按照需求添加自定义的声明和数据,灵活性更高。

总结来说,使用 JWT 相较于传统的基于会话的认证机制,可以减少服务器存储开销和管理复杂性,实现跨域支持和水平扩展,并且更适应无状态和微服务架构。

1.3 JWT 组成

JWT 由三部分组成分别是:头部(Header),载荷(Payload), 签名 (Signature)。

  1. 头部通常用于描述JWT的元数据,如算法类型,token类型等信息,采用Base64编码。
    typ:token 的类型,这里固定为 JWT
    alg:使用的 hash 算法,例如 HMAC、SHA256 或者 RSA
    JWT 官方规定了 7 个字段,可供使用:
    1.iss (Issuer):签发者
    2.sub (Subject):主题
    3.aud (Audience):接收者
    4.exp (Expiration time):过期时间
    5.nbf (Not Before):生效时间
    6.iat (Issued At):签发时间
    7.jti (JWT ID):编号

  2. 载荷是 JWT 实际要传输的内容,通过 Base64 编码进行传输。载荷中可以包含各种声明,如用户 ID、角色、权限等信息。

  3. 签名用于验证消息的完整性和真实性,可以使用 HMAC 算法或者 RSA 签名算法生成,,由头部和载荷一起进行签名,防止 token 被篡改。

1.4 JWT 实现原理

JWT 本质是将秘钥存放在服务器端,并通过某种加密手段进行加密和验证的机制。加密签名 = 某加密算法(header + payload + 服务器端私钥),因为服务端私钥别人不能获取,所以 JWT 能保证自身其安全性。

1.5 创建 JWT

1.3.1 引入依赖

<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

1.3.2 编写 JwtUtil 工具类

public class JwtUtil {
    //token有效期为一个小时
    public static final Long JWT_TTL = 60 * 60 *1000L;
    //设置秘钥明文
    public static final String JWT_KEY = "JWT_KEY";

    /**
     * 获取token
     * @return
     */
    public static String getUUID(){
        return UUID.randomUUID().toString().replaceAll("-", "").toLowerCase();
    }
    
    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @return
     */
    public static String createJWT(String subject) {
        JwtBuilder builder = getJwtBuilder(subject, null, getUUID());
        return builder.compact();
    }

    /**
     * 生成jtw
     * @param subject token中要存放的数据(json格式)
     * @param ttlMillis token超时时间
     * @return
     */
    public static String createJWT(String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, getUUID());// 设置过期时间
        return builder.compact();
    }

    /**
     * 获取 JwtBuilder 对象
     * @param subject
     * @param ttlMillis
     * @param uuid
     * @return
     */
    private static JwtBuilder getJwtBuilder(String subject, Long ttlMillis, String uuid) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        if(ttlMillis == null){
            ttlMillis = JwtUtil.JWT_TTL;
        }
        long expMillis = nowMillis + ttlMillis;
        Date expDate = new Date(expMillis);
        return Jwts.builder()
                .setId(uuid)           //唯一的ID
                .setSubject(subject)   // 主题  可以是JSON数据
                .setIssuer("zhangSan")  // 签发者
                .setIssuedAt(now)      // 签发时间
                .signWith(signatureAlgorithm, secretKey) //使用HS256对称加密算法签名, 第二个参数为秘钥
                .setExpiration(expDate);
    }

    /**
     * 创建token
     * @param id
     * @param subject
     * @param ttlMillis
     * @return
     */
    public static String createJWT(String id, String subject, Long ttlMillis) {
        JwtBuilder builder = getJwtBuilder(subject, ttlMillis, id);
        return builder.compact();
    }

    /**
     * 解析
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public static Claims parseJWT(String jwt) throws Exception {
        SecretKey secretKey = generalKey();
        return Jwts.parser()
                .setSigningKey(secretKey)
                .parseClaimsJws(jwt)
                .getBody();
    }

    /**
     * 生成加密后的秘钥 secretKey
     * @return
     */
    public static SecretKey generalKey() {
        byte[] encodedKey = Base64.getDecoder().decode(JwtUtil.JWT_KEY);
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
        return key;
    }
}

1.3.3 测试加密

@Test
public void encrypt() {
    String jwt = JwtUtil.createJWT("zhangSan");
    System.out.println(jwt);
}

加密后数据

eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2OWNkZjM2OWRiOWM0YmMxOGExNGZjYzYxNGIzMGYwYyIsInN1YiI6Imh1YW5nQngiLCJpc3MiOiJodWFuZ0JYIiwiaWF0IjoxNzAzMTY5NDI3LCJleHAiOjE3MDMxNzMwMjd9.rpFpaXm2ih7cqYL43JucRBZnWBx-Wu-OdyJS9NxpNUI

1.3.4 测试解密

@Test
public void decrypt() throws Exception {
    Claims claims = JwtUtil.parseJWT("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2OWNkZjM2OWRiOWM0YmMxOGExNGZjYzYxNGIzMGYwYyIsInN1YiI6Imh1YW5nQngiLCJpc3MiOiJodWFuZ0JYIiwiaWF0IjoxNzAzMTY5NDI3LCJleHAiOjE3MDMxNzMwMjd9.rpFpaXm2ih7cqYL43JucRBZnWBx-Wu-OdyJS9NxpNUI");
    String subject = claims.getSubject();
    System.out.println(subject);
}

解密后数据

zhangSan

1.6 登录功能

@RestController
@PostMapping("/test")
    public Result<String> login(@RequestBody UserLoginRequest userLoginRequest) {
        AssertUtil.isNotNull(userLoginRequest, ResultCodeEnum.PARAMETER_ERROR);
        AssertUtil.isNotNull(userLoginRequest.getAccountId(), ResultCodeEnum.PARAMETER_ERROR, "accountId不能为空!");
        AssertUtil.isNotNull(userLoginRequest.getUserName(), ResultCodeEnum.PARAMETER_ERROR, "userName不能为空!");
        AssertUtil.isNotNull(userLoginRequest.getPassword(), ResultCodeEnum.PARAMETER_ERROR, "password不能为空!");

        //根据账号id获取账号信息
        TAccount account = accountService.findById(userLoginRequest.getAccountId());
        //校验用户名和密码
        AssertUtil.isTrue(userLoginRequest.getUserName().equals(account.getName()), ResultCodeEnum.USERNAME_NO_EXISTS);
        AssertUtil.isTrue(userLoginRequest.getPassword().equals(account.getPassword()), ResultCodeEnum.PASSWORD_ERROR);
        //如果账号密码都正确,则生成token
        String token = JwtUtil.createJWT(userLoginRequest.getUserName());
        //缓存到redis
        stringRedisTemplate.opsForValue().set(RedisPrefix.TOKEN + userLoginRequest.getUserName(), token, 1, TimeUnit.HOURS);
        return Result.success(token);
    }
}

返回结果

{
    "code": "200",
    "msg": "成功",
    "data": "eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIxYTljNjRkMDRhNWE0MjQxOThhZjhjZWRjMThlYzZmYyIsInN1YiI6InpoYW5nU2FuIiwiaXNzIjoiaHVhbmdCWCIsImlhdCI6MTcwMzQwMTI3MiwiZXhwIjoxNzAzNDA0ODcyfQ.OAK0Fi05pmwF2ZTaGWYyON_VmGFv03G7Lk5jujhJAzA"
}

二、Cookie、Session 和 Token

2.1 三者的区别

Cookie、Session 和 Token 通常都是用来保存用户登录信息的技术,但三者有很大的区别,简单来说 Cookie 适用于简单的状态管理,Session 适用于需要保护用户敏感信息的场景,而 Token 适用于状态无关的身份验证和授权。

Token 状态无关性解析:在传统的基于会话的认证方式中,服务器需要在后端保存用户的会话状态,通过 Session ID 进行会话的管理。而 Token 机制不需要在服务器上保存任何关于用户的状态信息,只需要在登录成功时,服务器端通过某种算法生成一个唯一的 Token 值,之后再将此 Token 发送给客户端存储(存储在 localStorage 或 sessionStorage 中),注意此时服务端是不存储这个 Token 值的,服务器端只进行效验而不保存此 Token,这就叫“状态无关性”。这样就可以减轻服务器存储和管理会话状态的负担,所以它比较适用于大型系统和分布式系统。

具体来说,Cookie、Session 和 Token 的区别主要有以下几点区别:

  1. 存储位置不同: Cookie 存储在客户端,即浏览器中的文本文件,通过在 HTTP 头中传递给服务器来进行通信;Session 是服务器端的存储方式,通常存储在服务器的内存或数据库中;Token 也是存储在客户端,但是通常以加密的方式存储在客户端的 localStorage 或 sessionStorage 中。
  2. 数据安全性不同: Cookie 存储在客户端,可能会被窃取或篡改,因此对敏感信息的存储需要进行加密处理;Session 存储在服务器端,通过一个 Session ID 在客户端和服务器之间进行关联,可以避免敏感数据直接暴露;Token 通常使用加密算法生成,有效期较短且单向不可逆,可以提供较高的安全性。
  3. 跨域支持不同: 为了防止安全事故,因此 Cookie 是不支持跨域传输的,也就是不同域名下的 Cookie 是不能相互访问的;而 Session 机制通常是通过 Cookie 来保存 Session ID 的,因此 Session ID 默认情况下也是不支持跨域的;但 Token 可以轻松实现跨域,因为 Token 是存储在客户端的 localStorage 或者作为请求头的一部分发送到服务器的,所以不同的域名 Token 信息传输通常是不受影响的。
  4. 状态管理不同: Cookie 是应用程序通过在客户端存储临时数据,用于实现状态管理的一种机制;Session 是服务器端记录用户状态的方式,服务器会为每个会话分配一个唯一的 Session ID,并将其与用户状态相关联;Token 是一种用于认证和授权的一种机制,通常表示用户的身份信息和权限信息。

2.2 Cookie 和 Session 有什么关系

准确来说 Cookie 的实现和 Session 是没有任何关系的,但 Session 的实现需要借助于 Cookie。

Session 机制的实现流程如下:

  1. 会话创建: 通常情况下,当用户登录成功后,服务器会为该用户创建一个新的会话。在创建会话过程中,服务器会为该会话生成一个唯一的标识符,通常称为 Session ID。
  2. Session ID 传递: 服务器将生成的 Session ID 通过响应的方式发送给客户端,使用 SetCookie 命令,将用户的 Session ID 保存在 Cookie 中,通常是一个名为 JSESSIONID 的 Cookie。
  3. Session 数据存储: 在服务器端,Session 数据会被存储在一个能够关联 Session ID 的数据结构中(例如内存、数据库或者文件存储等)。常用的方式是将 Session ID 作为键,与对应的 Session 用户身份数据进行关联。
  4. Session ID 验证与检索: 当用户发送一个新的请求时,客户端会将之前存储的 Session ID 携带在请求的 Cookie 或请求头中发送给服务器。服务器会根据 Session ID 找到对应的 Session 数据,从而获得用户的状态信息。
  5. Session 数据使用: 服务器在获取到 Session 数据后,可以根据具体需求读取、修改或删除其中保存的状态信息。服务器可以通过 Session 来管理用户的登录状态、购物车内容、用户配置等。
  6. Session 过期与销毁: Session 有一个有效期限,一般通过设置一个固定的时间,或者在一定时间内没有用户活动时会将 Session 标记为过期。当 Session 过期时,服务器会销毁对应的 Session 数据,释放内存或其他资源。

所以默认情况下,Session 是借助 Cookie 来完成身份标识的传递的,这样服务器端才能根据 Session ID 和保存的会话信息进行关联,用于找到某个具体登录的用户,所以说:默认情况下,Session 机制是依赖 Cookie 实现的。

2.3 禁用 Cookie 之后 Session 还能用吗?

通过上文我们知道,默认情况下 Session 机制是依赖 Cookie 实现的,那么是不是禁用了 Cookie 之后,Session 机制也就无法使用了呢?其实不然。

除了默认情况下,我们可以使用 Cookie 来传递 Session ID 之外,我们可以通过一些特殊的手段来自行传递 Session ID,以此来摆脱禁用 Cookie 之后 Session 无法使用的情况,例如以下两种实现手段:

  1. URL Rewriting: 可以在每个请求的 URL 中附加 Session ID 参数。服务器在接收到请求时,解析 URL 中的 Session ID,并与对应的 Session 数据进行关联。这种方式适用于没有禁用地址栏中的参数传递的情况。
  2. 隐藏表单字段: 可以将 Session ID 作为隐藏表单字段的方式传递给服务器。当用户提交表单时,Session ID 将随着表单数据一起发送给服务器,服务器据此建立与当前会话的关联。

通过以上手段都可以将 Session ID 传递到服务器端(虽然麻烦点),然后在服务器端,我们再对以上传递的 Session ID 进行获取和映射,这样就手动完成了传递和匹配登录用户的工作了,Session 机制也得已继续使用了。

文章来源:https://blog.csdn.net/m0_49692893/article/details/128327086
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。