SpringBoot 整合 JWT 实现 Token 认证

一、前言

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

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

  • 客户端使用用户名、密码进行认证
  • 服务端生成 Session 并存储,将 SessionID 通过 Cookie 返回给客户端
  • 客户端访问需要认证的接口时在 Cookie 中携带 SessionID
  • 服务端通过 SessionID 查找 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,由于 Session 经常需要快速查找,通常将其存储在内存或内存数据库中,当同时在线用户较多时会占用大量的服务器资源;
  • 在分布式架构下,当前访问的节点可能不是创建 Session 的节点,导致无法验证,因此需要考虑在多个节点间同步 Session 数据;
  • 由于客户端使用 Cookie 存储 SessionID,在跨域场景下需要进行兼容性处理,同时这种方式也难以防范 CSRF 攻击;
  • 不支持 Android,IOS,小程序等移动端;

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

  • 客户端使用用户名、密码进行认证
  • 服务端验证用户名密码,通过后生成 Token 返回给客户端
  • 客户端保存 Token,访问需要认证的接口时在 URL 参数或 HTTP Header 中加入 Token
  • 服务端通过解码 Token 进行鉴权,认证通过则返回给客户端需要的数据

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

  • 服务端不需要存储和用户鉴权有关的信息,鉴权信息会被加密到 Token 中,服务端只需要读取 Token 中包含的鉴权信息即可
  • 避免了共享 Session 导致的不易扩展问题
  • 不需要依赖 Cookie,有效避免 Cookie 带来的 CSRF 攻击问题
  • 使用 CORS 可以快速解决跨域问题
  • 支持 Android,IOS,小程序等不支持 Cookies 的移动端

二、什么是 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

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,686评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,668评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 158,160评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,736评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,847评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,043评论 1 291
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,129评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,872评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,318评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,645评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,777评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,470评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,126评论 3 317
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,861评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,095评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,589评论 2 362
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,687评论 2 351

推荐阅读更多精彩内容