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

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,从而回避重放攻击。

自定义数据:存放我们想放在 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 依赖

<?xml version="1.0" encoding="UTF-8"?>
<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.0</modelVersion>
	<parent>
		<groupId>org.example</groupId>
		<artifactId>springboot-learning-parents</artifactId>
		<version>1.0-SNAPSHOT</version>
	</parent>

	<groupId>springboot-demo</groupId>
	<artifactId>springboot-h2</artifactId>
	<version>1.0-SNAPSHOT</version>
	<name>springboot-jwt</name>
	<url>https://gitee.com/leo825/springboot-learning-parents.git</url>
	<description>springboot集成jwt测试</description>
	<properties>
		<start-class>com.demo.SpringbootJwtApplication</start-class>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
		<java.version>1.8</java.version>
		<mybatisplus.version>3.5.1</mybatisplus.version>
		<freemaker.version>2.3.31</freemaker.version>
		<mysql.version>8.0.28</mysql.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>

		<!-- mybatis-plus 所需依赖  -->
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>${mybatisplus.version}</version>
		</dependency>
		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-generator</artifactId>
			<version>${mybatisplus.version}</version>
		</dependency>

		<!-- 使用h2内存数据库 -->
		<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
		</dependency>

		<!-- jwt依赖 -->
		<dependency>
			<groupId>com.auth0</groupId>
			<artifactId>java-jwt</artifactId>
			<version>3.8.1</version>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.47</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

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 无效,请重新获取");
        }
    }
}

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;
}

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;
    }
}

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();
    }
}

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 "你已通过验证";
    }
}

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;
    }
}

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);
	}
}

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、验证jwt
登录成功
3、验证jwt(无token失败)
无token报错

本图文内容来源于网友网络收集整理提供,作为学习参考使用,版权属于原作者。
THE END
分享
二维码
< <上一篇
下一篇>>