JWT——讲解

1、简介

JWT全称叫json web token,通过数字签名的⽅式,以json对象为载体,在不同的服务终端之间 安全的传输信息。

  • JWT在前后端分离系统,或跨平台系统中,通过JSON形式作为WEB应⽤中的令牌,⽤于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中,还可以完成数据加密、签名等相关处理。
  • 前端应⽤在访问后端应⽤时,会携带令牌,后端应⽤在接到令牌后,会验证令牌的合法性。从⽽决定前端应⽤是否能继续访问。
  • JWT还可以系统之间进⾏信息传递,A系统通过令牌对B系统进⾏数据传输,在传输过程中,可以完成数据的加密,B系统拿到数据后,通过签名进⾏验证,从⽽判断信息是否有篡改。
  • JWT就是一个签名验证的框架,负责将用户信息进行编码加密,编码加密后再将加密后的数据返回给浏览器。浏览器下一次请求只需要带上这个令牌即可。服务器端就可以根据这个令牌来验证用户的身份信息。
  • JWT是一种客户端浏览器和服务器之间传递安全信息的一种声明规范

2、JWT的应用 

JWT最常⻅的场景就是授权认证,⼀旦⽤户登录,后续每个请求都将包含JWT,系统在每次处理⽤户请求之前,都要先进⾏JWT安全校验,通过之后再进⾏处理。

1:授权

这是JWT最常⻅⽅案,⼀旦⽤户登录,每个后续请求将包括JWT,从⽽允许⽤户访问该令牌允许的路由,服务和资源。

2:信息交换

JWT是在各⽅之间安全地传输信息的好⽅法,可以验证传输内容是否遭到篡改。

 3、jwt和session的区别:

1、jwt可以隐藏数据、安全系数更高(安全性更好)

2、jwt更适合分布式/微服务系统

3、session是将数据存储在应用服务器里的,当用户量较大的时候,会造成服务资源的浪费。

4、session是存在单台服务器上的,如果涉及到分布式应用,那么因为每一台服务器上存储的session不一致导致用户状态丢失等问题。

5、jwt所产生的验证字符串(token)是不会存储在服务器的。

专业术语

1、每个⽤户经过我们的应⽤认证之后,将认证信息保存在session中,由于session服务器中对象,随着认证⽤户的增多,服务器内存开销会明显增⼤。

2、⽤户认证之后,服务端使⽤session保存认证信息,那么要取到认证信息,只能访问同⼀台服务器,才能拿到授权的资源。这样在分布式应⽤上,就需要实现session共享机制,不⽅便集群应⽤。

3,JWT认证基于令牌,该令牌存储在客户端。但认证由服务器端进⾏,解决服务器内存占⽤问题。当⽤户提交⽤户名和密码时,在服务器端认证通过后,会⽣成token令牌。然后将令牌响应给客户端浏览器。

4,客户端浏览器会在本地存储令牌。在客户端再次请求服务器接⼝时,每次都会携带JWT,在服务器验证通过后,再继续访问服务器资源。

4、JWT优势

1、简洁明了,可以通过URL、POST参数或Http header发送,因为数据量⼩,传输速度快。用户只需要关心密钥安全性问题。

2、⾃包含,jwt的负载可以传递一些⽤户基本的信息,不需要在服务器端保存会话信息(不需要再将数据保存在服务器里),不占服务器内存,也避免了多次查询数据库,特别适⽤于分布式微服务。

3、因为(jwt)token是以json加密的形式保存在客户端的,所以JWT是可以跨语⾔使⽤(如:python、go),原则上任何WEB形式都⽀持。很多编程语言都可以使用jwt进行授权。

5、JWT认证流程

1、首先由客户端浏览器发送登录请求,服务器收到登录请求后进行用户身份的验证。(账户、密码的验证),验证失败,将失败信息回执给浏览器(用户名/密码错误),验证通过,会进行jwt加密授权。

2、jwt可以将用户信息存放在其内部结构里(负载中(payload)),将header(头部)和负载进行Base64编号拼接后进行签名(加密)签名时需要提供自定的一串密钥(密钥是jwt里重要的一环,密钥决定用户数据是否能够被篡改)。最后返回一个token令牌(形成JWT)。

3、后端服务器再将令牌(JWT字符串)作为登录成功的返回结果发送给客户端浏览器,客户端浏览器将返回结果存在localStorage对象里。

4、客户端浏览器下一次请求只需要将令牌(JWT)放在在header里的的Authorization位。

5、后端检查是否存在,如果验证令牌(JWT)有效,后端就可以使⽤JWT中包含的⽤户信息。

6、JWT组成结构

jwt是由三个部分组成,JWT其实就是⼀段字符串,由3部分组成,⽤ . (点)拼接

JWT字符串示例

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M
jc5NzA5NDUsInVzZXIiOiJ7XCJpZFwiOjMsXCJuYW1lXCI6XCL
lkInlkIlcIixcInB3ZFwiOlwiMzMzXCJ9In0.NvGdUjFLJWj_
ZzhY9Qp--NkZgK1QGQtQjiCB7lEsFTg

1:标头:header——

  • 一般不需要手动改设置。

  • JWT会自动进行设置。

  • 头部主要分为两部分:typ:"JWT" 加密类型 alg:"hs256"签名算法。

Header{ //标头
‘typ’:’JWT’, 表示token类型
‘alg’:’HS256’ 表示签名算法
}

2:负载:payload——

负载一般设置的是用户基本数据。⽤于存储用户的主要信息,使⽤ Base64编码组成JWT结构的第⼆部分。由于该信息是可以被解析的,所以, 在信息中不要存放敏感信息

{name:"用户名",phoneNumber:"13878654547"};

Payload //有效负载
{
"userCode":"43435",
"name":"john",
"phone’:"13950497865"
}

3:签名:signature——

将编码过后的表头header和负载payload这两部分数据进行加密(加密需要使用到一个自定义的密钥。)

注意:

前⾯两部分都使⽤Base64进⾏编码,前端可以解开知道⾥⾯的信息, Signature需要使⽤编码后的header和payload以及我们提供的⼀密钥,然后使⽤header中指定的签名算法进⾏签名,以保证JWT没有被篡改过。
使⽤Signature签名可以防⽌内容被篡改。如果有⼈对头部及负载内容解码后进⾏修改,再进⾏编码,最后加上之前签名组成新的JWT。那么服务器会判断出新的头部和负载形成的签名和JWT附带的签名是不⼀样的。如果要对新的头部和负载进⾏签名,在不知道服务器加密时⽤的密钥的话,得出来的签名也是不⼀样的

7、JAVA里使用JWT

1、首先环境搭建,引入jwt的依赖jar包

<!--jwt的依赖-->
<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.13.0</version>
</dependency>

2、JWT创建、生成token

package com.lovo.utils;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import java.util.Calendar;
import java.util.HashMap;
import java.util.Map;
import static java.security.KeyRep.Type.SECRET;

/**
 * JWT工具类
 */
public class JwtUtils {
    //自定义一个密钥————签名时使用
    public static String signature = "lovo";

    /**
     * 获取生成token,使用jwt产生token 令牌
     * @param paramMap 参数对象(token携带的信息)
     * @return token字符串
     */
    public static String getToken(Map<String,String> paramMap){
        //创建一个日历对象
        Calendar calendar = Calendar.getInstance();
        //指定token过期时间为30分钟
        calendar.add(Calendar.MINUTE,30);
        //设置header————标头————jwt默认会设置标头
        //JWTCreator.Builder:初始化jwt构建对象
        JWTCreator.Builder builder = JWT.create();
        //设置负载payload
        //paramMap.keySet():得到map里的所有key值
        /*for (String key : paramMap.keySet()) {
            builder.withClaim(key,paramMap.get(key));
        }*/
        paramMap.forEach((k,v) -> builder.withClaim(k,v));
        // builder.withExpiresAt:设置token过期时间
        // 指定签名算法:sign(Algorithm.HMAC256(signature))
        String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(signature));
        return token;
    }

//编写代码测试生成的tokenn
    public static void main(String[] args) {
        Map<String,String> paramMap = new HashMap<>();
        paramMap.put("userName","faker");
        String token = JwtUtils.getToken(paramMap);
        System.out.println(token);
        //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImZha2VyIiwiZXhwIjoxNjY5MzY5MDM4fQ.SuIG1D4ti_bufG6uKjCvNxx-Gnzo4CUR-Fkl0GGps0M
    }
}

3、验证令牌(解码)

//自定义一个密钥————签名时使用
    public static String signature = "chw"; 
​
    /**
     * 验证令牌(解码)
     * @param token
     * @return
     */
    public static String verify(String token,String key){
        //1、创建(获取)验证对象(解码对象)
        //require():给验证对象提供自定义的密钥
        //build():创建验证对象
        String value = "";
        try{JWTVerifier jwtVerifier =
                JWT.require(Algorithm.HMAC256(signature)).build();
        //2、verify():验证token
        //得到解码对象
        DecodedJWT decodedJWT = jwtVerifier.verify(token);
        //3、从解码对象里获取负载里的数据
        //getClaim():从负载中获取数据
        value = decodedJWT.getClaim(key).asString();
        //捕获令牌失效的异常
        }catch (TokenExpiredException e){
            e.printStackTrace();
            return "令牌已经失效";
        }
        return value;
    }
​
    public static void main(String[] args) {
        Map<String,String> paramMap = new HashMap<>();
        paramMap.put("userName","faker");
        paramMap.put("phoneNum","13534788685");
//        String token = JwtUtils.createToken(paramMap);
        String tokenStr = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwaG9uZU51bSI6IjEzNTM0Nzg4Njg1IiwidXNlck5hbWUiOiJmYWtlciIsImV4cCI6MTY2OTYwNTA4N30.VMqDuRnjyBfteGcCVkPtm6plxZ7KmbmnjTGsZa7Ne-M";
//        System.out.println(token);
        //如果没有对应的key值,则返回null
        //如果token过期,则抛出TokenExpiredException异常
        String value = JwtUtils.verify(tokenStr, "userName");
        System.out.println(value);
    }

4、JWT的发送和接收

首先书写控制器

@RestController
@RequestMapping("/users")
public class UserController {
​
    @RequestMapping("/login")
    public String userLogin(String userName, String passWord, HttpServletResponse response){
        //验证用户的数据(账号和密码)
        if ("faker".equals(userName)&&"123456".equals(passWord)){
            //验证通过,给用户授权
            Map<String,String> paramMap = new HashMap<>();
            paramMap.put("userName",userName);
            paramMap.put("passWord",passWord);
            //获取token令牌
            String token = JwtUtils.createToken(paramMap);
            //在应⽤控制器Controller中,通过响应头发送给客户端。
            response.addHeader("token",token);
            /*所以同名的响应头可以有多个,每调用一次 addHeader() 方法添加一个响应头,
            而调用 setHeader() 方法后会覆盖且只保留一个同名的响应头。*/
            return "success";
        }
        return "error";
    }
}

书写HTML界面验证效果

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
    <script src="js/axios.min.js"></script>
</head>
<body>
<form>
  账号:<input type="text" id="userName"><br>
  密码:<input type="password" id="passWord"><br>
  <input type="button" value="登录" onclick="userLogin()">
</form>
​
</body>
​
<script>
  function userLogin(){
      //post请求传递单个参数的方式
      let dataParam = new URLSearchParams();
      dataParam.append("userName",document.getElementById("userName").value);
      dataParam.append("passWord",document.getElementById("passWord").value);
      axios.post('/users/login',dataParam).then(response=>{
          //请求成功获取token令牌
          if ("success" == response.data){
              let token = response.headers.token;
              //把令牌保存在localStorage对象里
              localStorage.setItem("token",token);
              location.href="pet.html";
          }else{
              alert("用户名或密码有误!")
          }
      });
      //sessionStorage和localStorage区别和联系
      //两者都是客户端保存数据所使用的对象
      //但是sessionStorage保存的数据会失效。一旦浏览器关闭
      //sessionSrorage里保存的数据将被清空,它是会话级别的存储
      //localStorage它会长久的存在与浏览器里。不会因为浏览器窗口关闭而清空,
      //只有清楚浏览器缓存后才会清空。
      //这两个对象一般都是存储一些字符串信息。常常和json字符串一起使用。
      //具体选用哪个对象作为token令牌存储,根据业务需求来定
  }
</script>
</html>

新增一个跳转界面

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>宠物界面</title>
    <script src="js/axios.min.js"></script>
</head>
<body>
<img src="img/pic1.jpg" style="width: 100px;height: 100px">
<a href="JavaScript:void(0)">购买</a>
<img src="img/pic2.jpg" style="width: 100px;height: 100px">
<a href="JavaScript:void(0)">购买</a>
<img src="img/pic3.jpg" style="width: 100px;height: 100px">
<a href="JavaScript:void(0)">购买</a>
​
</body>
<script>
    window.onload = function (){
        let config = {
            headers: {
                'token': localStorage.getItem("token")
            }
        }
        axios.post('/pets/petList',null,config).then(response=>{
            if (response.data == "success"){
                //填充用户数据
                alert("页面数据加载成功!")
            }else {
                alert("会话超时!请重新登录!");
                location.href = "login.html";
            }
        });
    }
</script>
​
<style>
    a {
        position: absolute;
        margin-top: 100px;
        margin-left: -70px;
    }
</style>
</html>

控制器

@RestController
@RequestMapping("/pets")
public class PetController {
​
    @RequestMapping("/petList")
    public String petList(HttpServletRequest request){
        //从请求头里获取到令牌token
        String token = request.getHeader("token");
        //验证令牌token
        Object value = JwtUtils.verify(token,"userName");
        if (value instanceof Boolean){
            return "error";
        }
        String userName = value.toString();
        if (userName != null && !"".equals(userName)) {
            return "success";
        }else {
            return "error";
        }
    }
}

8、Token的工具类

封装了一个Token的工具类,大家有需要可以使用。

传入token的是一个登录用户对象的JSON字符串,因为JSON支持多门语言,利于复用。

/**
 * Token工具类
 */
public class TokenUtils {
    /*public static void main(String[] args) {
        UUID uuid = UUID.randomUUID();
        System.out.println(uuid);
    }*/
    //设置过期时间,60分钟
    private  static final  long EXPIRE_TIME=1000*60*60;
    //设置一个自定义的密钥————签名时使用
    private static  final String TOKEN_SECRET="23ff6755-3e65-4c8a-9896-2e79157eaba7";
    /**
     * 生成token(通过用户对象的json字符串)
     * @param userJson
     * @return token字符串
     */
    public static String createToken(String userJson) {
        //过期时间
        Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
        //私钥及加密算法
        Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
        //设置头信息
        HashMap<String, Object> header = new HashMap<>(2);
        header.put("typ", "JWT");
        header.put("alg", "HS256");
        //附带username和userID生成签名
        //设置header————标头————jwt默认会设置标头
        //JWTCreator.Builder:初始化jwt构建对象
        JWTCreator.Builder builder = JWT.create();
        String token= builder.withHeader(header)
                //设置负载payload
                .withClaim("info",userJson)
                //过期时间
                .withExpiresAt(date)
                //密钥和加密算法
                .sign(algorithm);
        return token;
    }

    //解密
    public static String verifierToken(String token){
        String value = "";
        try {
            //私钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
            //1、创建(获取)验证对象(解码对象)
            //require():给验证对象提供自定义的密钥
            //build():创建验证对象
            JWTVerifier verifier = JWT.require(algorithm).build();
            //2、verify():验证token
            //得到解码对象
            DecodedJWT jwt = verifier.verify(token);
            //3、从解码对象里获取负载里的数据
            //getClaim():从负载中获取数据(取token中的信息)
            value=jwt.getClaim("info").asString();
            //捕获令牌失效的异常
        }catch (Exception e){
            e.printStackTrace();
            return "令牌已经失效";
        }
        return value;
    }
}

 扩展——雪花算法工具类(生成雪花ID)

public class SnowIdUtils {
    /**
     * 私有的 静态内部类
     */
    private static class SnowFlake {

        /**
         * 内部类对象(单例模式)
         */
        private static final SnowFlake SNOW_FLAKE = new SnowFlake();
        /**
         * 起始的时间戳
         */
        private final long START_TIMESTAMP = 1557489395327L;
        /**
         * 序列号占用位数
         */
        private final long SEQUENCE_BIT = 12;
        /**
         * 机器标识占用位数
         */
        private final long MACHINE_BIT = 10;
        /**
         * 时间戳位移位数
         */
        private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
        /**
         * 最大序列号  (4095)
         */
        private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
        /**
         * 最大机器编号 (1023)
         */
        private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
        /**
         * 生成id机器标识部分
         */
        private long machineIdPart;
        /**
         * 序列号
         */
        private long sequence = 0L;
        /**
         * 上一次时间戳
         */
        private long lastStamp = -1L;

        /**
         * 构造函数初始化机器编码
         */
        private SnowFlake() {
            //模拟这里获得本机机器编码
            long localIp = 4321;
            //localIp & MAX_MACHINE_ID最大不会超过1023,在左位移12位
            machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
        }
        /**
         * 获取雪花ID
         */
        public synchronized long nextId() {
            long currentStamp = timeGen();
            //避免机器时钟回拨
            while (currentStamp < lastStamp) {
                // //服务器时钟被调整了,ID生成器停止服务.
                throw new RuntimeException(String.format("时钟已经回拨.  Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
            }
            if (currentStamp == lastStamp) {
                // 每次+1
                sequence = (sequence + 1) & MAX_SEQUENCE;
                // 毫秒内序列溢出
                if (sequence == 0) {
                    // 阻塞到下一个毫秒,获得新的时间戳
                    currentStamp = getNextMill();
                }
            } else {
                //不同毫秒内,序列号置0
                sequence = 0L;
            }
            lastStamp = currentStamp;
            //时间戳部分+机器标识部分+序列号部分
            return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
        }
        /**
         * 阻塞到下一个毫秒,直到获得新的时间戳
         */
        private long getNextMill() {
            long mill = timeGen();
            //
            while (mill <= lastStamp) {
                mill = timeGen();
            }
            return mill;
        }
        /**
         * 返回以毫秒为单位的当前时间
         */
        protected long timeGen() {
            return System.currentTimeMillis();
        }
    }

    /**
     * 获取long类型雪花ID
     */
    public static long uniqueLong() {
        return SnowFlake.SNOW_FLAKE.nextId();
    }
    /**
     * 获取String类型雪花ID
     */
    public static String uniqueLongHex() {
        return String.format("%016x", uniqueLong());
    }

    public static void main(String[] args) {
        for(int i=0;i<10;i++) {
            System.out.println(SnowIdUtils.uniqueLong());
        }
    }
}

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值