• 【JavaWeb】关于JWT做认证授权的十万个理由(JSON Web Token)


    前言: 基于Token与基于Session的身份认证

    基于Session

    最为传统的做法,客户端储存 cookie 一般是 Session id ,服务器存储 Session

    • Session 是每次用户认证通过以后 ,服务器需要内存中创建一条记录保存用户信息(存储在HttpSession中,相当于一个Map(也可以放redis中)),,然后通过Cookie的形式给客户端返回一个jsessionid,然后每次访问的时候都需要从HttpSession中根据jsessionid来获取,通过这个逻辑来判断是否是认证的状态。
      • 随着认证通过的用户越来越多,服务器的在这里的开销就会越来越大
      • 不同域名之前切换的时候,请求可能会被禁止,即跨域问题
        在这里插入图片描述
        在这里插入图片描述

    存在的问题:

    • 每个用户都需要做一次记录,而Session一般情况下都会存在内存中,增大了服务器的开销
    • 集群环境下Session需要同步,或者分布式Session来处理
    • 因为是基于Cookie来传输的,如果Cookie被获取,用户容易受到CSRF攻击。
    • 前后端分离项目中会更加的麻烦

    基于token

    • JWT与Session的差异相同点是,他们都是存储用户信息。然而Session是在服务器端的,而JWT是在客户端
    • JWT方式将用户状态分散到了客户端中,可以明显减轻请服务器的内存压力,服务端只需要用算法解析客户端的token就可以得到信息
      在这里插入图片描述
      在这里插入图片描述

    认证的流程:

    • 用户通过账号密码提交到后端服务后,如果认证成功就会生成一个对应的Token信息
    • 之后用户请求资源都会携带这个Token值,后端获取到后校验通过放行,校验不通过拒绝

    jwt的优势:

    • 简单:可以通过URL,POST参数或者HTTP header发送,因为数据量小,传输速度快。
    • 自包含:负载中包含了所有用户所需的信息,避免多次查询数据
    • 跨语言:以JSON形式保存在客户端。
    • 不需要服务端保存信息,适合分布式环境

    一.JWT概念

    JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案

    二.分布式系统认证授权问题

    1、客户端向服务端发送用户名和密码。
    
    2、服务端验证通过后,在当前对话(session)里面保存相关数据,比如用户角色、登录时间等等。
    
    3、服务端向客户端返回一个 session_id,写入用户的 Cookie4、客户端随后的每一次请求,都会通过 Cookie,将 session_id 传回服务端。
    
    5、服务端收到 session_id,找到前期保存的数据,由此得知客户端的身份。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    该模式在单机部署下问题,如果是服务器集群,就要求 session 数据共享,每台服务器都能够读取 session。

    • 假设:A 网站和 B 网站是同一家公司的关联服务。现在要求,用户只要在其中一个网站登录,再访问另一个网站就会自动登录,请问怎么实现?

      • session 数据持久化,写入数据库或别的持久层。各种服务收到请求后,都向持久层请求数据。这种方案的优点是架构清晰,缺点是工程量比较大。另外,持久层万一挂了,就会单点失败。
      • 服务器索性不保存 session 数据了,所有数据都保存在客户端,每次请求都发回服务器JWT 就是这种方案的一个代表

    三.JWT 的原理

    JWT 的原理是,服务器认证以后,生成一个 JSON 对象,发回给用户,就像下面这样。

    
    {
      "username": "张三",
      "role": "管理员",
      "expired": "2019年8月1日0点0分"
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 客户端与服务端通信的时候,都要透传这个 JSON 对象。服务器完全只靠这个对象认定用户身份。为了防止用户篡改数据,服务器在生成这个对象的时候,会加上签名(详见后文)。

      • 服务器就不保存任何 session 数据了,也就是说,服务器变成无状态了,从而比较容易实现扩展

    四.JWT数据结构

    JWT是长的字符串,中间用点(.)分隔成三个部分

    • 注意,JWT 内部是没有换行的,这里只是为了便于展示,将它写成了几行。如下所示:
      在这里插入图片描述

    JWT 的三个部分依次如下。

    • Header(头部)
    • Payload(负载)
    • Signature(签名)

    写成一行,就是下面的样子。

    • Header.Payload.Signature
      在这里插入图片描述

    1 Header

    Header 部分是一个 JSON 对象, 常由两部分组成:令牌的类型【JWT】和所使用的签名算法【如HMAC、SHA256或者RSA】

    {
        "alg": "HS256",//签名或摘要算法
        "typ": "JWT"//token类型
    }
    
    • 1
    • 2
    • 3
    • 4
    • alg表示签名的算法(algorithm),默认是 HMAC SHA256(写成 HS256
    • typ表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT
      • Header部分的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串然后以"点"(.)拼接

    使用Base64URL 加密上面JSON对象得到第一部分

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
    
    • 1

    2. Payload

    Payload 部分也是一个JSON对象,用来存放实际需要传递的数据。通常是用户信息,比如用户名,用户角色,过期时间等JWT 规定了7个官方字段,供选用。

    iss (issuer):签发人
    sub (subject):主题,jwt所面向的用户
    aud (audience):用户,接收jwt的一方
    exp (expiration time):失效时间戳
    nbf (Not Before):生效时间时间戳(在此之前不可用)
    iat (Issued At):签发时间
    jti (JWT ID):JWT ID用于标识该JWT
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    
    {
        "iss": "token-server",//签发者
        "exp ": "Mon Nov 13 15:28:41 CST 2017",//过期时间
        "sub ": "wangjie",//用户名
        "aud": "web-server-1"//接收方,
        "nbf": "Mon Nov 13 15:40:12 CST 2017",//这个时间之前token不可用
        "jat": "Mon Nov 13 15:20:41 CST 2017",//签发时间
        "jti": "0023",//令牌id标识
        "claim": {“auth”:”ROLE_ADMIN”}//访问主张
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 注意,JWT 默认是不加密的,任何人都可以读到,所以不要把重要信息放在Payload。
      • Payload部分的的 JSON 对象使用 Base64URL 算法(详见后文)转成字符串然后以"点"(.)拼接

    使用Base64URL 加密上面JSON对象得到第二部分

    eyJuYW1lIjoiemhhbmdzYW4iLCJjb2RlIjoiMjEyMzQxMCJ9
    
    • 1

    3. Signature

    Signature 部分是对前两部分(Header/Payload)的签名,防止数据篡改。

    • 首先,需要指定一个密钥(secret)。这个密钥只有服务端才知道,不能泄露给用户。然后,使用 Header 里面指定的签名算法(默认是 HMAC SHA256),签名公式如下:
    HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload) , secret)
    
    • 1

    算出签名以后,把 base64UrlEncode(header)、base64UrlEncode(payload)、Signature 3个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

    //base64Url编码后的header和payload使用"点"(.)`拼接
    String encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
    
    //密钥
    Strubg secet="aaa";
    
    //通过header中声明的加密方式进行加盐secret组合加密
    String signature = HMACSHA256(encodedString, '密钥');
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    加密之后,得到第三步部分signature签名信息。

    xHtGAnyhgrD_FcIu3xzunVQjzThByjBF2GF5iA2ezOY
    
    • 1

    将这三部分用"点"(.)连接成一个完整的字符串,就构成了最终的JWT生成公式:

    base64UrlEncode(header) + "." + base64UrlEncode(payload) + "." + 
    HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload) , secret)
    
    • 1
    • 2

    执行结果

    eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
    
    • 1

    注意:secret用来进行jwt的签发和jwt的验证,所以,在任何场景都不应该流露出去

    4.Base64URL

    前面提到,Header 和 Payload 序列化算法是 Base64URL。这个算法跟 Base64 算法基本类似,但有一些小的不同。

    • JWT 作为一个令牌(token),有些场合可能会放到 URL(比如 api.example.com/?token=xxx)。Base64 有三个字符+、/和=,在 URL 里面有特殊含义, 在Base64URL 算法会被替换掉:=被省略+替换成-/替换成_

    5.JWT生成的token的安全性分析

    • 从JWT生成的token组成上来看,要想避免token被伪造,主要就得看签名signature 部分了
      • signature 部分由3部分组成,其中头部header载荷payload的base64编码,几乎是透明的,毫无安全性可言,token安全性就落在了加入的盐secet上面了,为了避免生成token所用的盐secet与解析token时加入的盐secet是一样的,我们需要对盐secet采用非对称加密,以达到生成token与校验token方所用的盐不一致的安全效果!

    注意:加盐的意思就是让味道改变,也就是让通过加盐来提高token的复杂度,让token更加安全,这个盐你可以任意指定,全凭自己和项目需求

    五.JWT 的使用方式

    客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage

    • 此后,客户端每次与服务器交互,都要带上这个 JWT。你可以把它放在 Cookie 里面自动发送,但是这样不能跨域所以更好的做法是放在 HTTP 请求头信息Authorization字段里面。

      Authorization: Bearer <token>
      
      • 1
      • 另一种做法是,跨域的时候,JWT 就放在 POST 请求的数据体里面。

    六.JWT的特点

    • JWT 默认是不加密,不能将重要数据写入 JWT。因此生成原始 Token 以后,可以加密算法再加密一次。

    • JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。

    • JWT 包含了认证信息,泄露后任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。

    • 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。

    七.JWT开源框架

    1.java-jwt

    https://github.com/auth0/java-jwt

    <dependency>
      <groupId>com.auth0groupId>
      <artifactId>java-jwtartifactId>
      <version>4.2.1version>
    dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5

    产生加密Token

    		HashMap<String, Object> map = new HashMap<>();
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.SECOND,60);
    
            String token = JWT.create()
                    .withHeader(map) //head
                    .withClaim("userId", 21)  //payload
                    .withClaim("username", "xizi")
                    .withAudience("user1") //设置接受方信息,一般时登录用户
                    .withExpiresAt(instance.getTime())  //过期时间 20秒之后
                    .sign(Algorithm.HMAC256("111111"));//Signature,使用HMAC算法,111111作为密钥加密
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    解密Token获取负载信息并验证Token是否有效

            //创建验证对象
            Verification verification = JWT.require(Algorithm.HMAC256("111111"));//签名
           
            JWTVerifier jwtVerifier = verification.build();
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            
            Claim userId = decodedJWT .getClaim("userId");
            Claim username = decodedJWT .getClaim("username");
            System.out.println("过期时间:"+decodedJWT .getExpiresAt());
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    常见的异常

    • SignatureVerificationException: 签名不一致异常

    • TokenExpiredException: 令牌过期异常

    • AlgorithmMi smatchException: 算法不匹配异常

    • InvalidClaimException: 失效的payload异常

    工具类

    public class JWTUtils {
        //自定义签名
        private static final  String SIGN="XIZI";
    
        /**
         * 生成token  三部分组成header.payload.sign
         *
         */
        public  static String getToken(Map<String,String> map){
    
            Calendar instance = Calendar.getInstance();
            instance.add(Calendar.DATE,7);  //七天  过期时间
    
            //创建jwt builder
            JWTCreator.Builder builder = JWT.create();
            //payload 遍历map中的键值对
            map.forEach((k,v)->{
                builder.withClaim(k,v );
            });
            //指定过期时间 设置编码方式
            String token = builder.withExpiresAt(instance.getTime())
                    .sign(Algorithm.HMAC256(SIGN));
            return  token;
        }
    
        /**
         * 验证token  并且放回DecodedJWT  获取token信息方法
         */
        public  static DecodedJWT verify(String token){
           //没有抛出异常 验证通过
           return JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
    
        }
    
        /**
         * 获取token信息方法
         */
        public static DecodedJWT getTokenInfo(String token){
            DecodedJWT verify = JWT.require(Algorithm.HMAC256(SIGN)).build().verify(token);
            return verify;
        }
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    2.jjwt

    引入依赖
    https://github.com/jwtk/jjwt#install-jdk-maven

    <dependency>
        <groupId>io.jsonwebtokengroupId>
        <artifactId>jjwt-apiartifactId>
        <version>0.11.2version>
    dependency>
    <dependency>
        <groupId>io.jsonwebtokengroupId>
        <artifactId>jjwt-implartifactId>
        <version>0.11.2version>
        <scope>runtimescope>
    dependency>
    <dependency>
        <groupId>io.jsonwebtokengroupId>
        <artifactId>jjwt-jacksonartifactId> 
        <version>0.11.2version>
        <scope>runtimescope>
    dependency>
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    或者

       
            <dependency>
                <groupId>io.jsonwebtokengroupId>
                <artifactId>jjwtartifactId>
                <version>0.9.0version>
            dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import javax.crypto.SecretKey;
    import javax.crypto.spec.SecretKeySpec;
    import com.lss.jwt_test.rest.TestController;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.JwtBuilder;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import org.apache.tomcat.util.codec.binary.Base64;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    
    public class JWTUtils {
        /**
         * token 过期时间, 单位: 秒. 这个值表示 30 天
         */
        private static final long TOKEN_EXPIRED_TIME = 30 * 24 * 60 * 60;
    
        public static final String jwtId = "tokenId";
    
        /**
         * jwt 加密解密密钥(可自行填写)
         */
        private static final String JWT_SECRET = "1234567890";
    
        /**
         * 创建JWT
         */
        public static String createJWT(Map<String, Object> claims, Long time) {
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; //指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
            Date now = new Date(System.currentTimeMillis());
    
            SecretKey secretKey = generalKey();
            long nowMillis = System.currentTimeMillis();//生成JWT的时间
            //下面就是在为payload添加各种标准声明和私有声明了
            JwtBuilder builder = Jwts.builder() //这里其实就是new一个JwtBuilder,设置jwt的body
                    .setClaims(claims)          //如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                    .setId(jwtId)                  //设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                    .setIssuedAt(now)           //iat: jwt的签发时间
                    .signWith(signatureAlgorithm, secretKey);//设置签名使用的签名算法和签名使用的秘钥
            if (time >= 0) {
                long expMillis = nowMillis + time;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp);     //设置过期时间
            }
            return builder.compact();
        }
    
    
        /**
         * 验证jwt
         */
        public static Claims verifyJwt(String token) {
            //签名秘钥,和生成的签名的秘钥一模一样
            SecretKey key = generalKey();
            Claims claims;
            try {
                claims = Jwts.parser()  //得到DefaultJwtParser
                        .setSigningKey(key)         //设置签名的秘钥
                        .parseClaimsJws(token).getBody();
            } catch (Exception e) {
                claims = null;
            }//设置需要解析的jwt
            return claims;
    
        }
    
        /**
         * 由字符串生成加密key
         *
         * @return
         */
        public static SecretKey generalKey() {
            String stringKey = JWT_SECRET;
            byte[] encodedKey = Base64.decodeBase64(stringKey);
            SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
            return key;
        }
    
        /**
         * 根据userId和openid生成token
         */
        public static String generateToken(String openId, Integer userId) {
            Map<String, Object> map = new HashMap<>();
            map.put("userId", userId);
            map.put("openId", openId);
            map.put("sub", openId);
            return createJWT(map, TOKEN_EXPIRED_TIME);
        }
    
        public static void main(String[] args) {
            // 生成token
            String s = generateToken("111", 20);
            System.out.println(s);
    
            // 验证
            String token = "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMTEiLCJvcGVuSWQiOiIxMTEiLCJleHAiOjE1OTI1NTc3ODMsInVzZXJJZCI6MjAsImlhdCI6MTU5MjU1NTE5MSwianRpIjoidG9rZW5JZCJ9.X7JHnx3Y5wtb-n3pT9tft2I4hENJdeRxW2QWaI4jv2E";
            Claims claims = verifyJwt(token);
            String subject = claims.getSubject();
            String userId = (String)claims.get("userId");
            String openId = (String)claims.get("openId");
            String sub = (String)claims.get("sub");
            System.out.println("subject:" + subject);
            System.out.println("userId:" + userId);
            System.out.println("openId:" + openId);
            System.out.println("sub:" + sub);
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111

    JJWT使用详解

  • 相关阅读:
    微博情绪分类
    [0CTF 2016]piapiapia
    STM32学习历程(day6)
    windbg的时间旅行实现对 C# 程序的终极调试
    Windows server 由于没有远程桌面授权服务器可以提供许可证,远程会话连接已断开。
    Java 第三阶段增强分析需求,代码实现能力【正则表达式】
    【八股总结】至今为止遇到的八股(上半)
    9.13号作业
    基于LeNet实现手写体数字识别实验
    36 【节流和防抖】
  • 原文地址:https://blog.csdn.net/qq877728715/article/details/127650669