• SpringBoot笔记:SpringBoot集成JWT实战


    JWT 简介

    概念

        JWT全称是:json web token。它将用户信息加密到 token 里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证 token 的正确性,只要正确即通过验证。

    JWT 的认证流程

    1、用户输入用户名和密码,发送给服务器,服务器验证账号密码成功
    2、服务器使用签名秘钥生成jwt,把用户id放到jwt中
    3、把令牌返给客户端
    4、下次请求的时候就把令牌放在请求头里带上
    5、服务器使用签名秘钥验证jwt是否有效
    6、有效后可以从jwt中获取到用户id

    优缺点

    优点
    1、 简洁,可以通过 URL POST 参数或者在 HTTP header 发送,因为数据量小,传输速度也很快;
    2、自包含,负载中可以包含用户所需要的信息,避免了多次查询数据库;
    3、跨平台,因为 Token 是以 JSON 加密的形式保存在客户端的,所以 JWT 是跨语言的,原则上任何 web 形式都支持;
    4、 存储在客户端,不需要再服务端保存会话信息,特别适用于分布式微服务;

    缺点
    1、无法作废已经发布的令牌
    2、不易应对数据过期

    JWT 消息构成

    一个 Token 分三部分,按顺序为

    1、头部(header)
    2、载荷(payload)
    3、签证(signature)

    三个部分之间用.分割。例如:

    eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJhdWQiOiIzOTZlNWZlNS0yZmQyLTQ3MjItYjQzOS0zYzY4NzA1OGMwZjAiLCJleHAiOjE2NjgxNzEyMTJ9.JUMFKdhzu1w_ecwHrOkqjKjosy3TOnaTrj1oFekG9HE
    
    • 1

    header

    JWT的头部承载两部分信息:
    1、声明类型,这里是JWT
    2、声明加密的算法,通常直接使用 HMAC SHA256

    JWT里验证和签名使用的算法列表如下:
    算法

    playload

    载荷就是存放有效信息的地方。基本上填两种类型的数据
    1、标准中注册的声明的数据;
    2、自定义数据;
    由这两部分内部做 base64 加密。

    标准中注册的声明(建议但不强制使用):
    iss: jwt签发者
    sub: jwt所面向的用户
    aud: 接收jwt的一方
    exp: jwt的过期时间,这个过期时间必须要大于签发时间
    nbf: 定义在什么时间之前,该jwt都是不可用的.
    iat: jwt的签发时间
    jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    自定义数据:存放我们想放在 token 中存放的 key-value 值。

    signature

    JWT的第三部分是一个签证信息,这个签证信息由三部分组成:
    1、base64 加密后的 header
    2、base64 加密后的 payload 连接组成的字符串
    3、然后通过 header 中声明的加密方式进行加盐 secret 组合加密
    然后就构成了JWT的第三部分。

    SpringBoot 集成 JWT 实战

    源代码地址:https://gitee.com/leo825/springboot-learning-parents.git

    项目整体结构:整体结构

    maven 依赖

    
    <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0modelVersion>
    	<parent>
    		<groupId>org.examplegroupId>
    		<artifactId>springboot-learning-parentsartifactId>
    		<version>1.0-SNAPSHOTversion>
    	parent>
    
    	<groupId>springboot-demogroupId>
    	<artifactId>springboot-h2artifactId>
    	<version>1.0-SNAPSHOTversion>
    	<name>springboot-jwtname>
    	<url>https://gitee.com/leo825/springboot-learning-parents.giturl>
    	<description>springboot集成jwt测试description>
    	<properties>
    		<start-class>com.demo.SpringbootJwtApplicationstart-class>
    		<project.build.sourceEncoding>UTF-8project.build.sourceEncoding>
    		<project.reporting.outputEncoding>UTF-8project.reporting.outputEncoding>
    		<java.version>1.8java.version>
    		<mybatisplus.version>3.5.1mybatisplus.version>
    		<freemaker.version>2.3.31freemaker.version>
    		<mysql.version>8.0.28mysql.version>
    	properties>
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.bootgroupId>
    			<artifactId>spring-boot-devtoolsartifactId>
    		dependency>
    
    		<dependency>
    			<groupId>org.springframework.bootgroupId>
    			<artifactId>spring-boot-starter-webartifactId>
    		dependency>
    		<dependency>
    			<groupId>org.springframework.bootgroupId>
    			<artifactId>spring-boot-starter-testartifactId>
    			<scope>testscope>
    		dependency>
    		<dependency>
    			<groupId>org.projectlombokgroupId>
    			<artifactId>lombokartifactId>
    		dependency>
    
    		
    		<dependency>
    			<groupId>com.baomidougroupId>
    			<artifactId>mybatis-plus-boot-starterartifactId>
    			<version>${mybatisplus.version}version>
    		dependency>
    		<dependency>
    			<groupId>com.baomidougroupId>
    			<artifactId>mybatis-plus-generatorartifactId>
    			<version>${mybatisplus.version}version>
    		dependency>
    
    		
    		<dependency>
    			<groupId>com.h2databasegroupId>
    			<artifactId>h2artifactId>
    		dependency>
    
    		
    		<dependency>
    			<groupId>com.auth0groupId>
    			<artifactId>java-jwtartifactId>
    			<version>3.8.1version>
    		dependency>
    		<dependency>
    			<groupId>com.alibabagroupId>
    			<artifactId>fastjsonartifactId>
    			<version>1.2.47version>
    		dependency>
    	dependencies>
    
    	<build>
    		<plugins>
    			<plugin>
    				<groupId>org.springframework.bootgroupId>
    				<artifactId>spring-boot-maven-pluginartifactId>
    			plugin>
    		plugins>
    	build>
    project>
    
    • 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

    JwtUtil

    package com.demo.util;
    
    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.exceptions.JWTDecodeException;
    import com.auth0.jwt.exceptions.JWTVerificationException;
    import com.auth0.jwt.interfaces.DecodedJWT;
    
    import java.util.Date;
    
    /**
     * jwt工具类
     */
    public class JwtUtil {
    
        /**
         * 过期时间5分钟
         */
        private static final long EXPIRE_TIME = 5 * 60 * 1000;
    
        /**
         * jwt 密钥
         */
        private static final String SECRET = "jwt_secret";
    
        /**
         * 生成签名,五分钟后过期
         *
         * @param userId
         * @return
         */
        public static String sign(String userId) {
            try {
                Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
                return JWT.create()
                        // 将 userId 保存到 token 里面
                        .withAudience(userId)
                        // 五分钟后token过期
                        .withExpiresAt(date)
                        // token 的密钥
                        .sign(algorithm);
            } catch (Exception e) {
                return null;
            }
        }
    
        /**
         * 根据 token 获取 userId
         *
         * @param token
         * @return
         */
        public static String getUserId(String token) {
            try {
                String userId = JWT.decode(token).getAudience().get(0);
                return userId;
            } catch (JWTDecodeException e) {
                return null;
            }
        }
    
        /**
         * 校验token
         *
         * @param token
         * @return
         */
        public static boolean checkSign(String token) {
            try {
                Algorithm algorithm = Algorithm.HMAC256(SECRET);
                JWTVerifier verifier = JWT.require(algorithm)
                        // .withClaim("username", username)
                        .build();
                DecodedJWT jwt = verifier.verify(token);
                return true;
            } catch (JWTVerificationException exception) {
                throw new RuntimeException("token 无效,请重新获取");
            }
        }
    }
    
    • 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

    JwtToken

    package com.demo.annotation;
    
    import java.lang.annotation.ElementType;
    import java.lang.annotation.Retention;
    import java.lang.annotation.RetentionPolicy;
    import java.lang.annotation.Target;
    
    /**
     * 加上该注解的接口需要登录才能访问
     */
    @Target({ElementType.METHOD, ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface JwtToken {
        boolean required() default true;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    JwtInterceptor

    package com.demo.interceptor;
    
    import com.demo.annotation.JwtToken;
    import com.demo.util.JwtUtil;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.web.method.HandlerMethod;
    import org.springframework.web.servlet.HandlerInterceptor;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.lang.reflect.Method;
    
    /**
     * jwt拦截器
     */
    @Slf4j
    public class JwtInterceptor implements HandlerInterceptor {
        @Override
        public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) {
            // 从 http 请求头中取出 token
            String token = httpServletRequest.getHeader("token");
            // 如果不是映射到方法直接通过
            if (!(object instanceof HandlerMethod)) {
                return true;
            }
            HandlerMethod handlerMethod = (HandlerMethod) object;
            Method method = handlerMethod.getMethod();
            //检查有没有需要用户权限的注解
            if (method.isAnnotationPresent(JwtToken.class)) {
                JwtToken jwtToken = method.getAnnotation(JwtToken.class);
                if (jwtToken.required()) {
                    // 执行认证
                    if (token == null) {
                        throw new RuntimeException("无token,请重新登录");
                    }
                    // 获取 token 中的 userId
                    String userId = JwtUtil.getUserId(token);
                    log.info("用户id: {}", userId);
                    // 验证 token
                    JwtUtil.checkSign(token);
                }
            }
            return true;
        }
    }
    
    • 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

    WebConfig

    package com.demo.config;
    
    import com.demo.interceptor.JwtInterceptor;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    /**
     * 注册拦截器
     */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        /**
         * 添加jwt拦截器
         *
         * @param registry
         */
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            registry.addInterceptor(jwtInterceptor())
                    // 拦截所有请求,通过判断是否有 @JwtToken 注解 决定是否需要登录
                    .addPathPatterns("/**");
        }
    
        /**
         * jwt拦截器
         *
         * @return
         */
        @Bean
        public JwtInterceptor jwtInterceptor() {
            return new JwtInterceptor();
        }
    }
    
    • 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

    JwtController

    package com.demo.controller;
    
    import com.alibaba.fastjson.JSONObject;
    import com.demo.annotation.JwtToken;
    import com.demo.util.JwtUtil;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.UUID;
    
    @RestController
    @RequestMapping("/jwt")
    public class JwtController {
        /**
         * 登录并获取token
         *
         * @param userName
         * @param passWord
         * @return
         */
        @PostMapping("/login")
        public Object login(String userName, String passWord) {
            JSONObject jsonObject = new JSONObject();
            // 检验用户是否存在(为了简单,这里假设用户存在,并制造一个uuid假设为用户id)
            String userId = UUID.randomUUID().toString();
            // 生成签名
            String token = JwtUtil.sign(userId);
            Map<String, String> userInfo = new HashMap<>();
            userInfo.put("userId", userId);
            userInfo.put("userName", userName);
            userInfo.put("passWord", passWord);
            jsonObject.put("token", token);
            jsonObject.put("user", userInfo);
            return jsonObject;
        }
    
        /**
         * 该接口需要带签名才能访问
         *
         * @return
         */
        @JwtToken
        @GetMapping("/getMessage")
        public String getMessage() {
            return "你已通过验证";
        }
    }
    
    • 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

    GlobalExceptionHandler

    package com.demo.common;
    
    import com.alibaba.fastjson.JSONObject;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.ResponseBody;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    /**
     * 全局异常处理
     */
    @RestControllerAdvice
    public class GlobalExceptionHandler {
        @ResponseBody
        @ExceptionHandler(Exception.class)
        public Object handleException(Exception e) {
            String msg = e.getMessage();
            if (msg == null || msg.equals("")) {
                msg = "服务器出错";
            }
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("code", 500);
            jsonObject.put("message", msg);
            return jsonObject;
        }
    }
    
    • 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

    SpringbootJwtApplication

    package com.demo;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    @SpringBootApplication
    @MapperScan(value = "com.demo.mapper")
    public class SpringbootJwtApplication {
    
    	public static void main(String[] args) {
    		SpringApplication.run(SpringbootJwtApplication.class, args);
    	}
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    application.yml

    #端口,项目上下文
    server:
      port: 8080
      servlet:
        context-path: /springboot-jwt
    
    spring:
      datasource:
        username: leo825
        password: 1WSX@357wj
        # 如果需要数据本地化,则改成 file 方式
        # jdbc:h2:file:D:/program/sqlite3/db/testDB;AUTO_SERVER=TRUE;DB_CLOSE_DELAY=-1
        url: jdbc:h2:mem:testDB;DB_CLOSE_DELAY=-1
        driver-class-name: org.h2.Driver
        # 初始化表
        schema: classpath:schema.sql
        # 初始化数据
        data: classpath:data.sql
        initialization-mode: always
        continue-on-error: true
        # 开启这个配置就可以通过 web 页面访问了,例如:http://localhost:8080/springboot-h2/h2-console
        h2:
          console:
            enabled: true
            settings:
              # 开启h2 console 跟踪 方便调试  默认 false
              trace: true
              # 允许console 远程访问 默认false
              web-allow-others: true
              # h2 访问路径上下文
              path: /h2-console
    
    
    # mybatis-plus 配置
    mybatis-plus:
      mapper-locations: classpath*:/mapper/**/*.xml
      #实体扫描,多个package用逗号或者分号分隔
      typeAliasesPackage: com.dmo.entity
      global-config:
        #数据库相关配置
        db-config:
          #主键类型  AUTO:"数据库ID自增", INPUT:"用户输入ID", ID_WORKER:"全局唯一ID (数字类型唯一ID)", UUID:"全局唯一ID UUID";
          id-type: AUTO
          #字段策略 IGNORED:"忽略判断",NOT_NULL:"非 NULL 判断"),NOT_EMPTY:"非空判断"
          field-strategy: NOT_NULL
          #驼峰下划线转换
          column-underline: true
          logic-delete-value: -1
          logic-not-delete-value: 0
        banner: false
      #原生配置
      configuration:
        # 打印sql
        log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
        map-underscore-to-camel-case: true
        cache-enabled: false
        call-setters-on-nulls: true
        jdbc-type-for-null: 'null'
    
    # 日志输出配置
    logging:
      level:
        root: INFO
        org:
          springframework:
            security: WARN
            web: ERROR
      file:
        path: ./logs
        name: './logs/springboot-jwt.log'
      pattern:
        file: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
        console: '%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50}:%L - %msg%n'
    
    • 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

    测试

    1、登录
    登录
    2、验证jwt
    登录成功
    3、验证jwt(无token失败)
    无token报错

  • 相关阅读:
    vue.js v-bind的基本使用语法
    elasticsearch 7.x的 常用命令
    mysql索引性能分析(sql执行频率、慢日志分析、sql效率分析工具 profile、explain)
    【JavaEE初阶】 Callable 接口
    问:为什么硬件测试如此重要
    Linux之(13)shell(5)sed进阶
    网上宠物用品商城的设计与实现(PHP+MySql)
    LeetCode 2316. 统计无向图中无法互相到达点对数::广度优先搜索(BFS)
    C#毕业设计——基于C#+asp.net+sqlserver的汽车修理厂物资流通管理系统设计与实现(毕业论文+程序源码)——物资流通管理系统
    【附源码】Python计算机毕业设计网上宠物店预约系统
  • 原文地址:https://blog.csdn.net/u011047968/article/details/127813137