美文网首页工具使用
SpringBoot 整合 JWT 实现 Token 认证

SpringBoot 整合 JWT 实现 Token 认证

作者: 大鱼炖海棠 | 来源:发表于2019-07-14 10:52 被阅读256次

    一、前言

    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

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

    相关文章

      网友评论

        本文标题:SpringBoot 整合 JWT 实现 Token 认证

        本文链接:https://www.haomeiwen.com/subject/wnrbkctx.html