工具使用

SpringBoot 整合 JWT 实现 Token 认证

2019-07-14  本文已影响256人  大鱼炖海棠

一、前言

HTTP 是一个无状态的协议,因此服务器无法识别2次请求是否来自同一个客户端。但在 Web 应用中,用户的认证和鉴权又是非常重要的一环,实践中产生了多种可用的方案,基于 Session 的会话管理即是其中一种。

在 Web 应用发展的初期,大部分 Web 应用采用基于 Session 的会话管理方式,其逻辑如下:

Cookie

Cookie 是客户端保存用户信息的一种机制,用来记录用户的一些信息,也是实现 Session 的一种方式。Cookie 存储的数据量有限,且都是保存在客户端浏览器中。不同的浏览器有不同的存储大小,但一般不超过 4KB。因此使用 Cookie 实际上只能存储一小段的文本信息。
例如:登录网站,今输入用户名密码登录了,第二天再打开很多情况下就直接打开了。这个时候用到的一个机制就是 Cookie。

Session

Session 是另一种记录客户状态的机制,它是在服务端保存的一个数据结构(主要存储的的 SessionID 和 Session 内容,同时也包含了很多自定义的内容如:用户基础信息、权限信息、用户机构信息、固定变量等),这个数据可以保存在集群、数据库、文件中,用于跟踪用户的状态。

客户端浏览器访问服务器的时候,服务器把客户端信息以某种形式记录在服务器上。这就是 Session。客户端浏览器再次访问时只需要从该 Session 中查找该客户的状态就可以了。

用户第一次登录后,浏览器会将用户信息发送给服务器,服务器会为该用户创建一个 SessionId,并在响应内容(Cookie)中将该 SessionId 一并返回给浏览器,浏览器将这些数据保存在本地。当用户再次发送请求时,浏览器会自动的把上次请求存储的 Cookie 数据自动的携带给服务器。

服务器接收到请求信息后,会通过浏览器请求的数据中的 SessionId 判断当前是哪个用户,然后根据 SessionId 在 Session 库中获取用户的 Session 数据返回给浏览器。

例如:购物车,添加了商品之后客户端处可以知道添加了哪些商品,而服务器端如何判别呢,所以也需要存储一些信息就用到了 Session。

如果说 Cookie 机制是通过检查客户身上的“通行证”来确定客户身份的话,那么 Session 机制就是通过检查服务器上的“客户明细表”来确认客户身份。Session 相当于程序在服务器上建立的一份客户档案,客户来访的时候只需要查询客户档案表就可以了。

Session 生成后,只要用户继续访问,服务器就会更新 Session 的最后访问时间,并维护该 Session。为防止内存溢出,服务器会把长时间内没有活跃的 Session 从内存删除。这个时间就是 Session 的超时时间。如果超过了超时时间没访问过服务器,Session 就自动失效了。

基于 Session 的认证方式存在如下问题:

鉴于基于 Session 的会话管理方式存在上述多个缺点,无状态的基于 Token 的会话管理方式诞生了,所谓无状态,就是服务端不再存储信息,甚至是不再存储 Session,其处理逻辑如下:

基于 Token 的会话管理方式有效的解决了基于 Session 的会话管理方式带来的问题:

二、什么是 JWT

JWT,全称 JSON Web Token,是一个开放标准(RFC 7519),它以一种紧凑的、自包含的方式在各方之间安全的传输信息。其官方定义如下:

JWT 官方定义

三、JWT 原理

JWT 认证原理:服务器生成一个 JWT 后会将它以 Authorization : Bearer JWT 键值对的形式存放在 cookies 里面发送到客户端,客户端再次访问受 JWT 保护的资源时,服务器会获取到 cookies 中存放的 JWT 信息,服务端程序首先对 Header 进行反编码获取到加密算法,再通过存放在服务器上的密匙对 Header.Payload 这个字符串进行加密,然后比对 JWT 中的 Signature 和实际加密出来的结果是否一致,如果一致那么说明该 JWT 合法有效,认证通过,否则认证失败。

JWT格式:Header.Payload.Signature

Header

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

Header 是由上面这种格式的 Json 通过 Base64 编码生成的字符串,它描述了编码对象是一个 JWT 且使用 HMAC256 算法进行加密,当然也可以选用其他加密算法。

JWT 官方类库支持下列所有加密算法:

JWS Algorithm Description
HS256 HMAC256 HMAC with SHA-256
HS384 HMAC384 HMAC with SHA-384
HS512 HMAC512 HMAC with SHA-512
RS256 RSA256 RSASSA-PKCS1-v1_5 with SHA-256
RS384 RSA384 RSASSA-PKCS1-v1_5 with SHA-384
RS512 RSA512 RSASSA-PKCS1-v1_5 with SHA-512
ES256 ECDSA256 ECDSA with curve P-256 and SHA-256
ES384 ECDSA384 ECDSA with curve P-384 and SHA-384
ES512 ECDSA512 ECDSA with curve P-521 and SHA-512

Claim => Payload

Claim 也是一个 Json。Claim 中存放的内容是 JWT 自身的标准属性,所有的标准属性都是可选的,可自行添加的,比如 JWT 的签发者、JWT 的接收者、JWT 的有效时间等;同时 Claim 中也可以存放一些自定义的属性,这个自定义的属性可以是在用户认证中用于标明用户身份的属性,如用户对应的数据库记录 ID(为了安全起见,不可以将用户名及密码这类敏感的信息存放在 Claim 中)。Claim 经 Base64转码之后生成的一串字符串称作Payload。 Claim 的内容可以是:

{
    loginUser: 'muyao',
    userId: '10000000',
    exp: 1544602234
}

Signature

将 Header 和 Claim 这两个 Json 分别使用 Base64 方式进行编码,生成字符串 Header 和 Payload,然后将Header 和 Payload 以 Header.Payload 的格式拼接在一起形成一个字符串,再使用 Header 中定义好的加密算法和一个密匙(这个密匙存放在服务器上,用于进行验证)对这个字符串进行加密,获得一个新的字符串,这个字符串就是 Signature。

四、SpringBoot 整合 JWT 实现 Token 认证

1. pom.xml 添加 maven 依赖

<properties>
    <jwt.version>3.8.1</jwt.version>
</properties>

<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>${jwt.version}</version>
</dependency>

2. 实现签名方法和认证方法

package com.muyao;

import java.sql.Date;
import java.util.HashMap;
import java.util.Map;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;

public class JwtUtils {
    
    /** 过期时间,缺省15分钟 */
    private long EXPIRE_TIME = 15 * 60 * 1000;
    
    /** token 私钥,缺省 galaxy-all */
    private String TOKEN_SECRET = "Galaxy-All";

    /** header */
    private Map<String, Object> header = new HashMap<>();

    /** 签名算法实例 */
    private Algorithm algorithm;

    /** token 认证器 */
    private JWTVerifier verifier;

    public JwtUtils() {
        JwtInit();
    }

    public JwtUtils(long expireTime, String tokenSecret) {
        this.EXPIRE_TIME = expireTime;
        this.TOKEN_SECRET = tokenSecret;
        JwtInit();
    }

    // 签名算法和认证器初始化
    private void JwtInit() {
        this.algorithm = Algorithm.HMAC256(this.TOKEN_SECRET);
        this.verifier = JWT.require(this.algorithm).build();
        this.header.put("typ", "JWT");
        this.header.put("alg", "HS256");
    }

    /**
     * 签名方法:采用 HMAC256算法,附带 claims 信息生成签名
     *
     * @param claims
     * @return
     */
    public String sign(Map<String, String> claims) throws Exception {
        // 计算 token 过期时间
        Date date = new Date(System.currentTimeMillis() + this.EXPIRE_TIME);

        try {
            JWTCreator.Builder jwt = JWT.create().withHeader(this.header).withExpiresAt(date);
            for (Map.Entry<String, String> entry : claims.entrySet()) {
                jwt.withClaim(entry.getKey(), entry.getValue());
            }

            return jwt.sign(this.algorithm);
        } catch (JWTCreationException exception) {
            exception.printStackTrace();
            throw new Exception(String.format("生成签名异常【%s】!", exception.getMessage()));
        }
    }
    
    /**
     * 认证方法类
     * @param token
     * @return
     */
    public boolean verify(String token) {
        try {
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (JWTVerificationException exception) {
            return false;
        }
    }
}

五、JWT 认证方式存在的问题

  1. token 不能撤销:JWT 没有过期或者失效时,客户端重置密码,JWT 依然可以使用;
  2. 不支持 refresh token,JWT 过期后需要执行登录授权的完整流程;
  3. 无法知道用户签发了几个 JWT

续篇将针对上述问题给出解决方案。

上一篇下一篇

猜你喜欢

热点阅读