• 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字符串示例

    1. eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2M
    2. jc5NzA5NDUsInVzZXIiOiJ7XCJpZFwiOjMsXCJuYW1lXCI6XCL
    3. lkInlkIlcIixcInB3ZFwiOlwiMzMzXCJ9In0.NvGdUjFLJWj_
    4. ZzhY9Qp--NkZgK1QGQtQjiCB7lEsFTg

    1:标头:header——

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

    • JWT会自动进行设置。

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

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

    2:负载:payload——

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

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

    1. Payload //有效负载
    2. {
    3. "userCode":"43435",
    4. "name":"john",
    5. "phone’:"13950497865"
    6. }

    3:签名:signature——

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

    注意:

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

    7、JAVA里使用JWT

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

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

    2、JWT创建、生成token

    1. package com.lovo.utils;
    2. import com.auth0.jwt.JWT;
    3. import com.auth0.jwt.JWTCreator;
    4. import com.auth0.jwt.algorithms.Algorithm;
    5. import java.util.Calendar;
    6. import java.util.HashMap;
    7. import java.util.Map;
    8. import static java.security.KeyRep.Type.SECRET;
    9. /**
    10. * JWT工具类
    11. */
    12. public class JwtUtils {
    13. //自定义一个密钥————签名时使用
    14. public static String signature = "lovo";
    15. /**
    16. * 获取生成token,使用jwt产生token 令牌
    17. * @param paramMap 参数对象(token携带的信息)
    18. * @return token字符串
    19. */
    20. public static String getToken(Map paramMap){
    21. //创建一个日历对象
    22. Calendar calendar = Calendar.getInstance();
    23. //指定token过期时间为30分钟
    24. calendar.add(Calendar.MINUTE,30);
    25. //设置header————标头————jwt默认会设置标头
    26. //JWTCreator.Builder:初始化jwt构建对象
    27. JWTCreator.Builder builder = JWT.create();
    28. //设置负载payload
    29. //paramMap.keySet():得到map里的所有key值
    30. /*for (String key : paramMap.keySet()) {
    31. builder.withClaim(key,paramMap.get(key));
    32. }*/
    33. paramMap.forEach((k,v) -> builder.withClaim(k,v));
    34. // builder.withExpiresAt:设置token过期时间
    35. // 指定签名算法:sign(Algorithm.HMAC256(signature))
    36. String token = builder.withExpiresAt(calendar.getTime()).sign(Algorithm.HMAC256(signature));
    37. return token;
    38. }
    39. //编写代码测试生成的tokenn
    40. public static void main(String[] args) {
    41. Map paramMap = new HashMap<>();
    42. paramMap.put("userName","faker");
    43. String token = JwtUtils.getToken(paramMap);
    44. System.out.println(token);
    45. //eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyTmFtZSI6ImZha2VyIiwiZXhwIjoxNjY5MzY5MDM4fQ.SuIG1D4ti_bufG6uKjCvNxx-Gnzo4CUR-Fkl0GGps0M
    46. }
    47. }

    3、验证令牌(解码)

    1. //自定义一个密钥————签名时使用
    2. public static String signature = "chw";
    3. /**
    4.     * 验证令牌(解码)
    5.     * @param token
    6.     * @return
    7.     */
    8.    public static String verify(String token,String key){
    9.        //1、创建(获取)验证对象(解码对象)
    10.        //require():给验证对象提供自定义的密钥
    11.        //build():创建验证对象
    12.        String value = "";
    13.        try{JWTVerifier jwtVerifier =
    14.                JWT.require(Algorithm.HMAC256(signature)).build();
    15.        //2、verify():验证token
    16.        //得到解码对象
    17.        DecodedJWT decodedJWT = jwtVerifier.verify(token);
    18.        //3、从解码对象里获取负载里的数据
    19.        //getClaim():从负载中获取数据
    20.        value = decodedJWT.getClaim(key).asString();
    21.        //捕获令牌失效的异常
    22.       }catch (TokenExpiredException e){
    23.            e.printStackTrace();
    24.            return "令牌已经失效";
    25.       }
    26.        return value;
    27.   }
    28.    public static void main(String[] args) {
    29.        Map paramMap = new HashMap<>();
    30.        paramMap.put("userName","faker");
    31.        paramMap.put("phoneNum","13534788685");
    32. //       String token = JwtUtils.createToken(paramMap);
    33.        String tokenStr = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJwaG9uZU51bSI6IjEzNTM0Nzg4Njg1IiwidXNlck5hbWUiOiJmYWtlciIsImV4cCI6MTY2OTYwNTA4N30.VMqDuRnjyBfteGcCVkPtm6plxZ7KmbmnjTGsZa7Ne-M";
    34. //       System.out.println(token);
    35.        //如果没有对应的key值,则返回null
    36.        //如果token过期,则抛出TokenExpiredException异常
    37.        String value = JwtUtils.verify(tokenStr, "userName");
    38.        System.out.println(value);
    39.   }

    4、JWT的发送和接收

    首先书写控制器

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

    书写HTML界面验证效果

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

    新增一个跳转界面

    1. html>
    2. <html lang="en" xmlns:th="http://www.thymeleaf.org">
    3. <head>
    4.    <meta charset="UTF-8">
    5.    <title>宠物界面title>
    6.    <script src="js/axios.min.js">script>
    7. head>
    8. <body>
    9. <img src="img/pic1.jpg" style="width: 100px;height: 100px">
    10. <a href="JavaScript:void(0)">购买a>
    11. <img src="img/pic2.jpg" style="width: 100px;height: 100px">
    12. <a href="JavaScript:void(0)">购买a>
    13. <img src="img/pic3.jpg" style="width: 100px;height: 100px">
    14. <a href="JavaScript:void(0)">购买a>
    15. body>
    16. <script>
    17.    window.onload = function (){
    18.        let config = {
    19.            headers: {
    20.                'token': localStorage.getItem("token")
    21.           }
    22.       }
    23.        axios.post('/pets/petList',null,config).then(response=>{
    24.            if (response.data == "success"){
    25.                //填充用户数据
    26.                alert("页面数据加载成功!")
    27.           }else {
    28.                alert("会话超时!请重新登录!");
    29.                location.href = "login.html";
    30.           }
    31.       });
    32.   }
    33. script>
    34. <style>
    35.    a {
    36.        position: absolute;
    37.        margin-top: 100px;
    38.        margin-left: -70px;
    39.   }
    40. style>
    41. html>

    控制器

    1. @RestController
    2. @RequestMapping("/pets")
    3. public class PetController {
    4.    @RequestMapping("/petList")
    5.    public String petList(HttpServletRequest request){
    6.        //从请求头里获取到令牌token
    7.        String token = request.getHeader("token");
    8.        //验证令牌token
    9.        Object value = JwtUtils.verify(token,"userName");
    10.        if (value instanceof Boolean){
    11.            return "error";
    12.       }
    13.        String userName = value.toString();
    14.        if (userName != null && !"".equals(userName)) {
    15.            return "success";
    16.       }else {
    17.            return "error";
    18.       }
    19.   }
    20. }

    8、Token的工具类

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

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

    1. /**
    2. * Token工具类
    3. */
    4. public class TokenUtils {
    5. /*public static void main(String[] args) {
    6. UUID uuid = UUID.randomUUID();
    7. System.out.println(uuid);
    8. }*/
    9. //设置过期时间,60分钟
    10. private static final long EXPIRE_TIME=1000*60*60;
    11. //设置一个自定义的密钥————签名时使用
    12. private static final String TOKEN_SECRET="23ff6755-3e65-4c8a-9896-2e79157eaba7";
    13. /**
    14. * 生成token(通过用户对象的json字符串)
    15. * @param userJson
    16. * @return token字符串
    17. */
    18. public static String createToken(String userJson) {
    19. //过期时间
    20. Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
    21. //私钥及加密算法
    22. Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
    23. //设置头信息
    24. HashMap header = new HashMap<>(2);
    25. header.put("typ", "JWT");
    26. header.put("alg", "HS256");
    27. //附带username和userID生成签名
    28. //设置header————标头————jwt默认会设置标头
    29. //JWTCreator.Builder:初始化jwt构建对象
    30. JWTCreator.Builder builder = JWT.create();
    31. String token= builder.withHeader(header)
    32. //设置负载payload
    33. .withClaim("info",userJson)
    34. //过期时间
    35. .withExpiresAt(date)
    36. //密钥和加密算法
    37. .sign(algorithm);
    38. return token;
    39. }
    40. //解密
    41. public static String verifierToken(String token){
    42. String value = "";
    43. try {
    44. //私钥及加密算法
    45. Algorithm algorithm = Algorithm.HMAC256(TOKEN_SECRET);
    46. //1、创建(获取)验证对象(解码对象)
    47. //require():给验证对象提供自定义的密钥
    48. //build():创建验证对象
    49. JWTVerifier verifier = JWT.require(algorithm).build();
    50. //2、verify():验证token
    51. //得到解码对象
    52. DecodedJWT jwt = verifier.verify(token);
    53. //3、从解码对象里获取负载里的数据
    54. //getClaim():从负载中获取数据(取token中的信息)
    55. value=jwt.getClaim("info").asString();
    56. //捕获令牌失效的异常
    57. }catch (Exception e){
    58. e.printStackTrace();
    59. return "令牌已经失效";
    60. }
    61. return value;
    62. }
    63. }

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

    1. public class SnowIdUtils {
    2. /**
    3. * 私有的 静态内部类
    4. */
    5. private static class SnowFlake {
    6. /**
    7. * 内部类对象(单例模式)
    8. */
    9. private static final SnowFlake SNOW_FLAKE = new SnowFlake();
    10. /**
    11. * 起始的时间戳
    12. */
    13. private final long START_TIMESTAMP = 1557489395327L;
    14. /**
    15. * 序列号占用位数
    16. */
    17. private final long SEQUENCE_BIT = 12;
    18. /**
    19. * 机器标识占用位数
    20. */
    21. private final long MACHINE_BIT = 10;
    22. /**
    23. * 时间戳位移位数
    24. */
    25. private final long TIMESTAMP_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    26. /**
    27. * 最大序列号 (4095)
    28. */
    29. private final long MAX_SEQUENCE = ~(-1L << SEQUENCE_BIT);
    30. /**
    31. * 最大机器编号 (1023)
    32. */
    33. private final long MAX_MACHINE_ID = ~(-1L << MACHINE_BIT);
    34. /**
    35. * 生成id机器标识部分
    36. */
    37. private long machineIdPart;
    38. /**
    39. * 序列号
    40. */
    41. private long sequence = 0L;
    42. /**
    43. * 上一次时间戳
    44. */
    45. private long lastStamp = -1L;
    46. /**
    47. * 构造函数初始化机器编码
    48. */
    49. private SnowFlake() {
    50. //模拟这里获得本机机器编码
    51. long localIp = 4321;
    52. //localIp & MAX_MACHINE_ID最大不会超过1023,在左位移12位
    53. machineIdPart = (localIp & MAX_MACHINE_ID) << SEQUENCE_BIT;
    54. }
    55. /**
    56. * 获取雪花ID
    57. */
    58. public synchronized long nextId() {
    59. long currentStamp = timeGen();
    60. //避免机器时钟回拨
    61. while (currentStamp < lastStamp) {
    62. // //服务器时钟被调整了,ID生成器停止服务.
    63. throw new RuntimeException(String.format("时钟已经回拨. Refusing to generate id for %d milliseconds", lastStamp - currentStamp));
    64. }
    65. if (currentStamp == lastStamp) {
    66. // 每次+1
    67. sequence = (sequence + 1) & MAX_SEQUENCE;
    68. // 毫秒内序列溢出
    69. if (sequence == 0) {
    70. // 阻塞到下一个毫秒,获得新的时间戳
    71. currentStamp = getNextMill();
    72. }
    73. } else {
    74. //不同毫秒内,序列号置0
    75. sequence = 0L;
    76. }
    77. lastStamp = currentStamp;
    78. //时间戳部分+机器标识部分+序列号部分
    79. return (currentStamp - START_TIMESTAMP) << TIMESTAMP_LEFT | machineIdPart | sequence;
    80. }
    81. /**
    82. * 阻塞到下一个毫秒,直到获得新的时间戳
    83. */
    84. private long getNextMill() {
    85. long mill = timeGen();
    86. //
    87. while (mill <= lastStamp) {
    88. mill = timeGen();
    89. }
    90. return mill;
    91. }
    92. /**
    93. * 返回以毫秒为单位的当前时间
    94. */
    95. protected long timeGen() {
    96. return System.currentTimeMillis();
    97. }
    98. }
    99. /**
    100. * 获取long类型雪花ID
    101. */
    102. public static long uniqueLong() {
    103. return SnowFlake.SNOW_FLAKE.nextId();
    104. }
    105. /**
    106. * 获取String类型雪花ID
    107. */
    108. public static String uniqueLongHex() {
    109. return String.format("%016x", uniqueLong());
    110. }
    111. public static void main(String[] args) {
    112. for(int i=0;i<10;i++) {
    113. System.out.println(SnowIdUtils.uniqueLong());
    114. }
    115. }
    116. }

  • 相关阅读:
    【ATT&CK】基于ATT&CK识别网络钓鱼攻防战法
    internship:术语了解及着手写接口
    Redis集群3.2.11离线安装详细版本(使用Ruby)
    Qt多线程之QThreadData::current()理解
    Web框架Gin
    typescript---配置类相关内容(十五)
    Halcon xld镜像操作
    van-uploader上传图片报错Invalid handler for event “load“(在uniapp编译)
    硅雪崩光电二极管(Si-APDs)行业发展现状及前景预测
    【PG】postgreSQL参数解释二 WRITE-AHEAD LOG部分
  • 原文地址:https://blog.csdn.net/weixin_62993347/article/details/128064106