SpringBoot集成JWT令牌认证技术

1.什么是 JWT?

    JSON Web Token( JWT ) 是一个开放标准( RFC 7519 ),它定义了一种紧凑的、自包含的方式,用于在各方之间作为 JSON 对象安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。 JWT 可以使用秘密(使用 HMAC 算法)或公钥 / 私钥对使用 RSA 或 ECDSA 来签名。

    虽然 JWT 可以加密,但也提供保密各方之间,我们将重点放在签名令牌。签名的令牌可以验证包含在其中的声明的完整性,而加密的令牌隐藏这些声明以防止其他各方查阅变更。当令牌使用公钥 / 私钥对签名时,签名也证明只有持有私钥的方才是签名方。

    关于数字签名:数字签名(又称公钥数字签名)是只有信息的发送者才能产生的别人无法伪造的一段数字串,这段数字串同时也是对信息的发送者发送信息真实性的一个有效证明。它是一种类似写在纸上的普通的物理签名,但是使用了公钥加密领域的技术来实现的,用于鉴别数字信息的方法。一套数字签名通常定义两种互补的运算,一个用于签名,另一个用于验证。数字签名是非对称密钥加密技术数字摘要技术的应用。

2.JWT的结构

    JWT 是由三段信息构成的,将这三段信息文本用. 符号,链接一起就构成了 JWT 字符串。 就像这样

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
![avatar](img/1.jpg)

JWT 包含了三部分

  • 1.Header 头部 ( 标题包含了令牌的元数据,并且包含签名和 / 或加密算法的类型) ;

  • 2.Payload 负载 ( 类似于飞机上承载的物品 ) ;

  • 3.Signature 签名 / 签证;


    JWT TOKEN 结构图
  • 2.1 JWT 头

  JWT 头部分是一个描述 JWT 元数据的 JSON 对象,通常如下所示。

{
  "alg": "HS256",
  "typ": "JWT"
}

    在上面的代码中, alg 属性表示签名使用的算法,默认为 HMAC SHA256 (写为 HS256 ); typ 属性表示令牌的类型, JWT 令牌统一写为 JWT 。
    最后,使用 Base64 URL 算法将上述 JSON 对象转换为字符串保存。

  • 2.2 有效载荷

  有效载荷部分,是 JWT 的主体内容部分,也是一个 JSON 对象,包含需要传递的数据。 JWT 指定七个默认字段供选择。

  • iss :发行人
  • exp :到期时间
  • sub :主题
  • aud :用户
  • nbf :在此之前不可用
  • iat :发布时间
  • jti : JWT ID 用于标识该 JWT

除以上默认字段外,我们还可以自定义私有字段,如下例:

{
"loginName": "zs",
"userName": " 张三 ",
"admin": true
}

请注意,默认情况下 JWT 是未加密的,任何人都可以解读其内容,因此不要构建隐私信息字段,存放保密信息,以防止信息泄露。JSON 对象也使用 Base64 URL 算法转换为字符串保存。

  • 2.3 签名哈希

  签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改。
  首先,需要指定一个密码( secret )。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头(Header)中指定的签名算法(默认情况下为HMAC SHA256 )根据以下公式生成签名。

HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload) , secret)

  在计算出签名哈希后, JWT 头有效载荷签名哈希的三个部分组合成一个字符串,每个部分用 "." 分隔,就构成整个 JWT 对象。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9. eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9. TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

3.为什么使用 JWT ?

  随着技术的发展,分布式 web 应用的普及,通过 session 管理用户登录状态成本越来越高,因此慢慢发展成为 token 的方式做登录身份校验,然后通过 token 去取 redis 中的缓存的用户信息,随着之后 jwt 的出现,校验方式更加简单便捷化,无需通过 redis 缓存,而是直接根据 token 取出保存的用户信息,以及对 token 可用性校验,单点登录更为简单。

JWT 架构图

JWT 架构图

token的使用原理

token的使用原理

4.JWT 工作机制?

  在身份验证中,当用户使用其凭据成功登录时,将返回 JSON Web Token (即: JWT )。由于令牌是凭证,因此必须非常小心以防止出现安全问题。一般情况下,不应将令牌保留的时间超过要求。理论上超时时间越短越好。

  每当用户想要访问受保护的路由或资源时,用户代理应该使用 Bearer(持票人) 模式发送 JWT,通常在Authorization(授权) header中。标题内容应如下所示:

Authorization: Bearer <token>

  在某些情况下,这可以作为无状态授权机制。服务器的受保护路由将检查 Authorization header 中的有效 JWT ,如果有效,则允许用户访问受保护资源。如果 JWT 包含必要的数据,则可以减少查询数据库或缓存信息。 如果在 Authorization header 中发送令牌,则跨域资源共享( CORS )将不会成为问题,因为它不使用 cookie 。
  注意:使用签名令牌,虽然他们无法更改,但是令牌中包含的所有信息都会向用户或其他方公开。这意味着不应该在令牌中放置敏感信息

5.如何使用 JWT?

  • 5.1 创建 springboot 工程

  在 IDEA 中构建一个 springboot 的 web 工程,在对应的 pom.xml 文件中引入以下内容:

<?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.springframework.boot</groupId>-->
        <!--<artifactId>spring-boot-starter-parent</artifactId>-->
        <!--<version>2.2.1.RELEASE</version>-->
        <!--<relativePath/> &lt;!&ndash; lookup parent from repository &ndash;&gt;-->
    <!--</parent>-->
    <groupId>com.sccl</groupId>
    <artifactId>spring-boot-jwt</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>spring-boot-jwt</name>
    <description>Spring Boot For Json Web Token</description>

    <properties>
        <java.version>1.8</java.version>
    </properties>
    <!--项目不继承springboot starter 父工程,采用依赖管理-->
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>2.2.1.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <!-- 导入 redis 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!-- 导入 jwt 工具包 -->
        <dependency>
            <groupId>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.1</version>
        </dependency>
        <!-- 导入 web 启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 导入热部署启动器 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>
        <!-- 导入 lombox 包 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- 导入 google 提供的 Java 库 -->
        <dependency>
            <groupId>com.google.guava</groupId>
            <artifactId>guava</artifactId>
            <version>18.0</version>
        </dependency>
        <!--解决@ConfigurationProperties报红,引入此依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--测试依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  • 5.2 编写 application.yml 配置文件
server:
  port: 8099
  servlet:
    context-path: /boots
#jwt 设置
com:
  sccl:
    springbootjwt:
      secret: wangbin # 私钥
      issuer: www.sccl.com #发布者
      subject: userLoginToken #主题
      audience: App # 签发给谁?
      hour: 1 #令牌过期时间
      minute: 30 #令牌刷新时间

#redis配置
spring:
  redis:
    database: 0 #Redis 数据库索引(默认为 0 ) (0-15,共16个数据库)
    host: 127.0.0.1 # Redis 服务器地址
    port: 6379 # Redis 服务器连接端口
    password:  # Redis 服务器连接密码(默认为空)
    jedis:
      pool:
        max-active: 200 # 连接池最大连接数(使用负值表示没有限制)
    timeout: 5000ms # 连接超时时间(毫秒)
  • 5.3 构建项目包结构

项目包结构

包结构说明: annotation 注解包, bean 实体包, configure 配置包, exception 异常包, interceptor 拦截器包, jwt JWT 工具包, message 返回消息包, sysmag 系统管理包, usermag 用户管理包, util 工具包

  • 5.4 代码说明

annotation 包
  FieldMarker 该注解的作用:主要在于说明 UserBean 中那些字段是 JWT 的负载字段

package com.sccl.springbootjwt.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Create by wangbin
 * 2019-12-03-19:36
 *  主要在于说明 UserBean 中那些字段是 JWT 的负载字段
 *  功能描述 标记 JWT  字段 用在 JavaBean 身上
 *  用来描述 对象与 Map  进行转换时 属性是否需要忽略
 */
@Retention(RetentionPolicy.RUNTIME) //生命周期运行时
@Target({ElementType.METHOD,ElementType.FIELD,ElementType.PARAMETER}) //作用范围方法,字段,参数
public @interface FieldMarker {
    /**
     *  功能描述 属性的名称
     *  开发时间 2019-12-03-19:36
     */
    String value();
}

bean 包
  UserBean 是一个 JavaBean ,代表用户资源,此类中某些属性可以作为 JWT 负载中的一部分

package com.sccl.springbootjwt.bean;

import com.sccl.springbootjwt.annotation.FieldMarker;
import lombok.Data;

import java.io.Serializable;

/** UserBean  是一个 JavaBean ,
 * 代表用户资源,此类中某些属性可以作为 JWT 负载中的一部分
 * Create by wangbin
 * 2019-12-03-19:41
 */
@Data
public class UserBean implements Serializable {
    private Long id;
    /**
     *  功能描述 用户名
     *  将 userName 作为 token 令牌中的一部分
     *  一定不要把过于敏感的数据,作为令牌中的一部分,切记
     *  开发时间 2019-12-03-19:41
     */
    @FieldMarker(value = "userName")
    private String userName;
    /**
     *  功能描述 登录名
     *  开发时间 2019-12-03-19:41
     */
    private String loginName;
    /**
     *  功能描述 密码
     *  开发时间 2019-12-03-19:41
     */
    private String password;
    /**
     *  功能描述 年龄
     *  开发时间 2019-12-03-19:41
     */
    private Integer age;
}

configure 包
  RedisConfig 类,通过 @Configuration 标记为一个 spring 配置类, 使用 @Bean 向容器中装配一个 RedisTemplate 的模板实例

package com.sccl.springbootjwt.configure;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

/** redis的配置类
 * 通过 @Configuration 标记为一个 spring 配置类,
 * 使用 @Bean 向容器中装配一个 RedisTemplate 的模板实例
 * Create by wangbin
 * 2019-12-03-19:47
 */
@Configuration
public class RedieConfig {
    @Bean
    @SuppressWarnings("all")
    public RedisTemplate<String,Object> redisTemplate(RedisConnectionFactory factory){
        RedisTemplate<String,Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(factory);
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om  = new ObjectMapper();
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);

        /* 制定 key  与 value  的 序列化规则 */
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key 采用 String 的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash 的 key 也采用 String 的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value 序列化方式采用 jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash 的 value 序列化方式采用 jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

  WebMvcConfig 类,通过 @Configuration 标记为一个 spring 配置类, 使用 @Bean 向容器中装配一个 LoginInterceptor 的模板实例,该拦截器主要用来做 JWT 令牌认证

package com.sccl.springbootjwt.configure;

import com.sccl.springbootjwt.interceptor.LoginInterceptor;
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 标记为一个 spring 配置类,
 *  使用 @Bean 向容器中装配一个 LoginInterceptor 的模板实例,拦截器主要用
 来做 JWT 令牌认证
 * Create by wangbin
 * 2019-12-04-9:57
 * 功能描述 springmvc 补充配置
 */
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
    /**
     * 方法说明:向容器中添加拦截器
     * @author wangbin
     * @date 2019/12/4 10:02
     */
    @Bean
    public LoginInterceptor loginInterceptor(){
        return new LoginInterceptor();
    }
    /**
     * 方法说明:拦截器添加拦截规则
     * @author wangbin
     * @date 2019/12/4 10:03
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor()) //注册拦截器
                .addPathPatterns("/**")  //拦截的路径模式
                .excludePathPatterns("/sys/login") //排除哪些路径
                .excludePathPatterns("/sys/logout")
                .excludePathPatterns("/static/**");
    }
}

exception 异常处理包
  异常,代表后端系统可能存在的问题。可能是持久层的,也可能是业务层的,当然也可能是表现的,还有可能是其他组件的。但不管是哪个层的,都要注意,不要抛给前端(特别是:不要让用户看到,体验感很重要!!!)
  ExceptionHandle 全局异常处理类,通过 @RestControllerAdvice 使用 AOP 编程原理将通知定义在表现层,只要表现层有异常都可以被该处理类监听到,从而可以从容的在此处对 " 异常 " 进行加工处理!!!

package com.sccl.springbootjwt.exception;

import com.sccl.springbootjwt.exception.customer.CustomerException;
import com.sccl.springbootjwt.message.ReturnMessage;
import com.sccl.springbootjwt.message.ReturnMessageUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletResponse;

/** 全局异常处理类
 * 异常,代表后端系统可能存在的问题。可能是持久层的,也可能是业务层的,
 * 当然也可能是表现的,还有可能是其他组件的。但不管是哪个层
 * 的,都要注意,不要抛给前端(特别是:不要让用户看到,体验感很重要!!!)
 * ExceptionHandle  全局异常处理类,通过 @RestControllerAdvice 使用 AOP 编程原理
 * 将通知定义在表现层,只要表现层有异常都可以被该处理类监听到,
 * 从而可以从容的在此处对 " 异常 " 进行加工处理!!!
 * Create by wangbin
 * 2019-12-04-10:08
 */
@RestControllerAdvice
public class ExceptionHandle {
    private static final Logger logger = LoggerFactory.getLogger(ExceptionHandle.class);
    @ExceptionHandler(value = Exception.class)
    public ReturnMessage<Object> handle(HttpServletResponse response, Exception exception){
        response.setCharacterEncoding("utf-8");
        if(exception instanceof CustomerException){
            CustomerException customerException = (CustomerException)exception;
            return ReturnMessageUtil.error(customerException.getCode(),customerException.getMessage());
        }else{
            logger.error(" 系统异常 {}",exception);
            return ReturnMessageUtil.error(-1, " 未知异常 : "+exception.getMessage());
        }
    }
}

customer 自定义异常包

CustomerException 自定义异常

package com.sccl.springbootjwt.exception.customer;

import com.sccl.springbootjwt.message.CodeEnum;
import lombok.Data;

/** 自定义用户异常
 * Create by wangbin
 * 2019-12-04-10:19
 */
@Data
public class CustomerException extends RuntimeException{
    //状态码
    private Integer code;
    public CustomerException(CodeEnum codeEnum){
        super(codeEnum.getMsg());
        this.code = codeEnum.getCode();
    }
}

TokenIllegalException 令牌非法异常

package com.sccl.springbootjwt.exception.customer;

/** 自定义 令牌非法异常
 * Create by wangbin
 * 2019-12-04-10:23
 */
public class TokenIllegalException extends RuntimeException {
    /**
     * Constructs a new runtime exception with {@code null} as its
     * detail message. The cause is not initialized, and may subsequently be
     * initialized by a call to {@link #initCause}.
     */
    public TokenIllegalException(){

    }
    /**
     * Constructs a new runtime exception with the specified detail message.
     * The cause is not initialized, and may subsequently be initialized by a
     * call to {@link #initCause}.
     *
     * @param message the detail message. The detail message is saved for
     * later retrieval by the {@link #getMessage()} method.
     */
    public TokenIllegalException(String message){
        super(message);
    }
}

TokenMisMatchException 令牌数据不匹配异常

package com.sccl.springbootjwt.exception.customer;

/** 令牌数据不匹配异常
 *  token 与 redis 中数据不匹配异常
 * Create by wangbin
 * 2019-12-04-10:25
 */
public class TokenMisMatchException extends RuntimeException {
    /**
     * Constructs a new runtime exception with {@code null} as its
     * detail message. The cause is not initialized, and may subsequently be
     * initialized by a call to {@link #initCause}.
     */
    public TokenMisMatchException(){

    }
    /**
     * Constructs a new runtime exception with the specified detail message.
     * The cause is not initialized, and may subsequently be initialized by a
     * call to {@link #initCause}.
     *
     * @param message the detail message. The detail message is saved for
     * later retrieval by the {@link #getMessage()} method.
     */
    public TokenMisMatchException(String message){
        super(message);
    }
}

interceptor 拦截器包
LoginInterceptor 拦截器类,这是一个主要的类,主要作用:判断用户的登录状态,判断用户的令牌是否合法,判断用户令牌是否已达刷新标准(此处:刷新令牌的手段,采用的是后端根据时间自动完成刷新 " 令牌交换 " ,当然也可以前端调用接口来完成刷新)

package com.sccl.springbootjwt.interceptor;

import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.sccl.springbootjwt.exception.customer.CustomerException;
import com.sccl.springbootjwt.exception.customer.TokenIllegalException;
import com.sccl.springbootjwt.exception.customer.TokenMisMatchException;
import com.sccl.springbootjwt.jwt.JwtProvide;
import com.sccl.springbootjwt.message.CodeEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import com.google.common.base.Objects;

/** 登录拦截器
 * 功能描述 状态拦截器
 * 专门用来判断用户是否登录的拦截器
 * 类似于 WEB 开发中的登录过滤器的功能
 * =====================================================
 * LoginInterceptor 拦截器类,这是一个主要的类,
 * 主要作用:判断用户的登录状态,判断用户的令牌是否合法,
 * 判断用户令牌是否已达刷新标准
 * (此处:刷新令牌的手段,采用的是后端根据时间自动完成刷新 " 令牌交换 " ,
 * 当然也可以前端调用接口来完成刷新)
 * Create by wangbin
 * 2019-12-04-10:27
 */
public class LoginInterceptor implements HandlerInterceptor {
    Logger log = LoggerFactory.getLogger(LoginInterceptor.class);
    @Resource
    private JwtProvide jwtProvide;

    //该方法将在Controller处理之前进行调用
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
        log.info("Token Checkout processing");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Type","text/html;charset=UTF-8");
        /* 从 HTTP 请求头中,获得令牌信息 */
        String token = request.getHeader("Authorization");
        if(StringUtils.isEmpty(token)){
            throw new CustomerException(CodeEnum.TOKENISEMPTY);
        }
        /* 验证令牌 */
        try{
            jwtProvide.verifyToken(token);
            /* 判断令牌是否需要刷新 */
            String newToken = jwtProvide.refreshToken(token);
            if(!Objects.equal(token,newToken)){
                response.setHeader("Authorization",newToken);
            }
        }catch (AlgorithmMismatchException e){
            log.error("Token Checkout processing AlgorithmMismatchException  异常! "+e.getLocalizedMessage());
            throw new CustomerException(CodeEnum.ILLEGALTOKEN);
        }catch (TokenExpiredException e){
            log.info("token 已经过期 ");
            throw new CustomerException(CodeEnum.EXPIRETOKEN);
        }catch (SignatureVerificationException e){
            log.error("Token Checkout processing SignatureVerificationException  异常! "+e.getLocalizedMessage());
            throw new CustomerException(CodeEnum.ILLEGALTOKEN);
        }catch (TokenIllegalException e){
            log.error("Token Checkout processing TokenIllegalException  异常! "+e.getLocalizedMessage());
            throw new CustomerException(CodeEnum.ILLEGALTOKEN);
        }catch (TokenMisMatchException e){
            log.error("Token Checkout processing TokenMisMatchException  异常! "+e.getLocalizedMessage());
            throw new CustomerException(CodeEnum.ILLEGALTOKEN);
        }catch (Exception e) {
            log.error("Token Checkout processing  未知异常! "+e.getLocalizedMessage());
            throw e;
        }
        return true;
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) {

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {

    }
}

jwt 包
  此包主要用于:产生 JWT 令牌,以及校验 JWT 令牌,可以理解为 JWT 的工具包。

  JwtProvide 类,使用 @ConfigurationProperties 完成 application.yml 中定义的配置信息,自动绑定到类中的属性身上

package com.sccl.springbootjwt.jwt;

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.JWTCreator.Builder;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.sccl.springbootjwt.bean.UserBean;
import com.sccl.springbootjwt.exception.customer.CustomerException;
import com.sccl.springbootjwt.exception.customer.TokenIllegalException;
import com.sccl.springbootjwt.exception.customer.TokenMisMatchException;
import com.sccl.springbootjwt.message.CodeEnum;
import com.sccl.springbootjwt.util.Object2MapUtil;
import com.sccl.springbootjwt.util.RedisProvide;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import com.google.common.base.Objects;
import javax.annotation.Resource;
import java.util.*;

/**
 * Create by wangbin
 * 2019-12-04-10:48
 */
@Data
@Component
@ConfigurationProperties(prefix = "com.sccl.springbootjwt")
public class JwtProvide {
    @Resource
    private RedisProvide redisProvide;

    /**
     *  功能描述 私钥
     *  开发时间 2019-12-04-10:48
     */
    private String secret;
    /**
     *  功能描述 发布者
     *  开发时间 2019-12-04-10:48
     */
    private String issuer;
    /**
     *  功能描述 主题
     *  开发时间 2019-12-04-10:48
     */
    private String subject;
    /**
     *  功能描述 签名的观众 也可以理解谁接受签名的
     *  开发时间 2019-12-04-10:48
     */
    private String audience;
    /**
     *  功能描述 自定义签名
     *  开发时间 2019-12-04-10:48
     */
    private Map<String, String> claims;
    /**
     *  功能描述 过期时间
     *  开发时间 2019-12-04-10:48
     */
    private Integer hour;
    /**
     *  功能描述 刷新时间
     *  开发时间 2019-12-04-10:48
     */
    private Integer minute;

    /**
     *  创建 默认xx小时后过期的 Token
     * @param claims
     * @return
     */
    public String createToken(Map<String, Object> claims) {
        return createToken(claims,hour,minute);
    }
    /**
     *  创建 hour 小时后过期的 Token
     *
     * @param claims
     * @param hour 有效时间
     * @param minute  刷新时间
     * @return
     */
    public String createToken(Map<String, Object> claims, int hour, int minute) {
        Payload createPayload = this.createPayload(hour, minute);
        createPayload.setClaims(claims);
        Algorithm hmac256 = Algorithm.HMAC256(this.getSecret());
        return createToken(createPayload, hmac256);
    }
    /**
     *  根据负载和算法创建 Token
     *
     * @param payload
     * @param algorithm
     * @return
     */
    public String createToken(Payload payload, Algorithm algorithm) {
        Builder headBuilder = createHeaderBuilder(algorithm);
        Builder publicClaimbuilder = addPublicClaimBuilder(headBuilder, payload);
        Builder privateClaimbuilder = addPrivateClaimbuilder(publicClaimbuilder, payload);
        String token = privateClaimbuilder.sign(algorithm);
        return token;
    }
    /**
     *  创建自定小时后过期的负载
     *
     * @param hour 有效时间
     * @param minute  刷新时间
     * @return
     */
    public Payload createPayload(int hour, int minute) {
        Payload payload = new Payload();
        payload.setIssuer(this.getIssuer());
        payload.setSubject(this.getSubject());
        payload.setAudiences(this.getAudience());
        this.setIssuedAtAndExpiresAt(new Date(), hour, minute, payload);
        return payload;
    }
    /**
     *  添加私有声明
     *
     * @param builder
     * @param payload
     * @return
     */
    private Builder addPrivateClaimbuilder(Builder builder, Payload payload) {
        Map<String, Object> claims = payload.getClaims();
        if (!CollectionUtils.isEmpty(claims)) {
            claims.forEach((k, v) -> {
                builder.withClaim(k, (String) v);
            });
        }
        builder.withClaim("refreshAt", payload.getRefreshAt());
        return builder;
    }
    /**
     *  添加公共声明
     *
     * @param builder
     * @param payload
     * @return
     */
    private Builder addPublicClaimBuilder(Builder builder, Payload payload) {
        // 生成签名者
        if (!StringUtils.isEmpty(payload.getIssuer())) {
            builder.withIssuer(payload.getIssuer());
        }
        // 生成签名主题
        if (!StringUtils.isEmpty(payload.getSubject())) {
            builder.withSubject(payload.getSubject());
        }
        // 生成签名的时间
        if (payload.getIssuedAt() != null) {
            builder.withIssuedAt(payload.getIssuedAt());
        }
        // 签名过期的时间
        if (payload.getExpiresAt() != null) {
            builder.withExpiresAt(payload.getExpiresAt());
        }
        //  签名领取签名的观众 也可以理解谁接受签名的
        if (CollectionUtils.isEmpty(payload.getAudience())) {
            payload.getAudience().forEach((s) -> {
                builder.withAudience(s);
            });
        }
        return builder;
    }
    /**
     *  创建 JWT  头部信息
     *
     * @param algorithm
     * @return
     */
    private Builder createHeaderBuilder(Algorithm algorithm) {
        Builder builder = JWT.create().withHeader(buildJWTHeader(algorithm));
        return builder;
    }
    /**
     *  校验 Token
     *
     * @param token
     * @return
     */
    public Payload verifyToken(String token) {
        DecodedJWT jwt = null;
        Payload payload = null;
        try {
            jwt = getDecodedJWT(token);
            payload = getPublicClaim(jwt);
            payload = getPrivateClaim(jwt, payload);
            Map<String, Object> claims = payload.getClaims();
            UserBean userBean = Object2MapUtil.map2Bean(claims, UserBean.class);
            /* 判断解析出来的对象,是否与 redis 中的对象在属性上是否一致,如果不一致则需要抛出异常 */
            UserBean user = (UserBean) redisProvide.get(token);
            if (user == null) {
                throw new TokenIllegalException(token);
            }
            if (!Objects.equal(user.getUserName(), userBean.getUserName())) {
                throw new TokenMisMatchException(token);
            }
        } catch (AlgorithmMismatchException e) {
            // 算法不一致,将抛出异常
            throw e;
        } catch (TokenExpiredException e) {
            // 令牌失效,将抛出异常
            throw e;
        } catch (Exception e) {
            // 其他异常
            throw e;
        }
        return payload;
    }
    /**
     *  功能描述 :  刷新新的 Token
     *
     * @param token  老的令牌信息
     * @return  开发时间 2019/11/26 0026  下午 1:02
     */
    public String refreshToken(String token) {
        DecodedJWT jwt = null;
        Payload payload = null;
        try {
            jwt = getDecodedJWT(token);
            payload = getPublicClaim(jwt);
            payload = getPrivateClaim(jwt, payload);
            Map<String, Object> claims = payload.getClaims();
            Claim claim = (Claim) claims.get("refreshAt");
            //  刷新时间
            Date refreshAt = claim.asDate();
            //  过期时间
            Date expiresAt = payload.getExpiresAt();
            Date currentAt = new Date();
            // 当前时间未超过过期时间,但是又超过了刷新时间,那么就刷新
            if (currentAt.before(expiresAt) && refreshAt.before(currentAt)) {
                UserBean userBean = (UserBean) redisProvide.get(token);
                Map<String, Object> userInfo = null;
                try {
                    userInfo = Object2MapUtil.bean2Map(userBean);
                } catch (IllegalAccessException e) {
                    throw new CustomerException(CodeEnum.DATAPARSEERROR);
                }
                redisProvide.del(token);
                token = createToken(userInfo, hour, minute);
                redisProvide.set(token, userBean);
            }
        } catch (AlgorithmMismatchException e) {
            // 算法不一致,将抛出异常
            throw e;
        } catch (TokenExpiredException e) {
            // 令牌失效,将抛出异常
            throw e;
        } catch (Exception e) {
            // 其他异常
            throw e;
        }
        return token;
    }
    /**
     *  获取 JWT  私有声明
     *
     * @param jwt
     * @param payload
     * @return
     */
    private Payload getPrivateClaim(DecodedJWT jwt, Payload payload) {
        Map<String, Object> claims = new HashMap<String, Object>();
        jwt.getClaims().forEach((k, v) -> {
            claims.put(k, v);
        });
        payload.setClaims(claims);
        return payload;
    }
    /**
     *  获取 JWT  公共声明
     *
     * @param jwt
     * @return
     */
    private Payload getPublicClaim(DecodedJWT jwt) {
        Payload payload = new Payload();
        payload.setIssuer(jwt.getIssuer());
        payload.setSubject(jwt.getSubject());
        payload.setAudience(jwt.getAudience());
        payload.setIssuedAt(jwt.getIssuedAt());
        payload.setExpiresAt(jwt.getExpiresAt());
        return payload;
    }
    /**
     *  获取 DecodedJWT
     *
     * @param token
     * @return
     */
    private DecodedJWT getDecodedJWT(String token) {
        JWTVerifier verifier = JWT.require(Algorithm.HMAC256(this.getSecret())).build();
        DecodedJWT jwt = verifier.verify(token);
        return jwt;
    }
    /**
     *  构建 JWT 头部 Map 信息
     *
     * @param algorithm
     * @return
     */
    private Map<String, Object> buildJWTHeader(Algorithm algorithm) {
        Map<String, Object> map = new HashMap<String, Object>();
        map.put("alg", algorithm.getName());
        map.put("typ", "JWT");
        return map;
    }
    /**
     *  根据发布时间设置过期时间
     *  根据发布时间设置刷新时间
     *
     * @param issuedAt
     * @param hour
     * @param payload
     */
    public void setIssuedAtAndExpiresAt(Date issuedAt, Integer hour, Integer minute, Payload payload) {
        payload.setIssuedAt(issuedAt);
        payload.setExpiresAt(getAfterDateByHour(issuedAt, hour));
        payload.setRefreshAt(getAfterDateByMinute(issuedAt, minute));
    }
    /**
     *  返回一定时间后的日期
     *
     * @param date  开始计时的时间
     * @param hour  增加的小时
     * @return
     */
    public Date getAfterDateByHour(Date date, int hour) {
        if (date == null) {
            date = new Date();
        }
        Date afterDate = getAfterDate(date, 0, 0, 0, hour, 0, 0);
        return afterDate;
    }
    /**
     *  返回一定时间后的日期
     *
     * @param date 开始计时的时间
     * @param minute  增加的分钟
     * @return
     */
    public Date getAfterDateByMinute(Date date, int minute) {
        if (date == null) {
            date = new Date();
        }
        Date afterDate = getAfterDate(date, 0, 0, 0, 0, minute, 0);
        return afterDate;
    }
    /**
     *  返回一定时间后的日期
     *
     * @param date 开始计时的时间
     * @param year 增加的年
     * @param month 增加的月
     * @param day 增加的日
     * @param hour 增加的小时
     * @param minute  增加的分钟
     * @param second  增加的秒
     * @return
     */
    public Date getAfterDate(Date date, int year, int month, int day, int hour, int minute, int second) {
        if (date == null) {
            date = new Date();
        }
        Calendar cal = new GregorianCalendar();
        cal.setTime(date);
        if (year != 0) {
            cal.add(Calendar.YEAR, year);
        }
        if (month != 0) {
            cal.add(Calendar.MONTH, month);
        }
        if (day != 0) {
            cal.add(Calendar.DATE, day);
        }
        if (hour != 0) {
            cal.add(Calendar.HOUR_OF_DAY, hour);
        }
        if (minute != 0) {
            cal.add(Calendar.MINUTE, minute);
        }
        if (second != 0) {
            cal.add(Calendar.SECOND, second);
        }
        return cal.getTime();
    }
}

Payload 负载类,此类中主要描述 JWT 中 负载 部分的相关信息

package com.sccl.springbootjwt.jwt;

import lombok.Data;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Map;

/** 功能描述 JWT  负载
 *  Payload  负载类,此类中主要描述 JWT 中 负载 部分的相关信息
 * Create by wangbin
 * 2019-12-04-10:58
 */
@Data
public class Payload implements Serializable {
    /**
     *  功能描述 发布者
     *  开发时间 2019-12-04-10:58
     */
    private String issuer;
    /**
     *  功能描述 主题
     *  开发时间 2019-12-04-10:58
     */
    private String subject;
    /**
     *  功能描述 签名的观众 也可以理解谁接受签名的
     *  开发时间 2019-12-04-10:58
     */
    private List<String> audience;
    /**
     *  功能描述 发布时间
     *  开发时间 2019-12-04-10:58
     */
    private Date issuedAt;
    /**
     *  功能描述 过期时间
     *  开发时间 2019-12-04-10:58
     */
    private Date expiresAt;
    /**
     *  功能描述 开始使用时间
     *  开发时间 2019-12-04-10:58
     */
    private Date notBefore;
    /**
     *  功能描述 刷新时间
     *  开发时间 2019-12-04-10:58
     */
    private Date refreshAt;
    /**
     *  功能描述 自定义签名
     *  开发时间 2019-12-04-10:58
     */
    private Map<String,Object> claims;

    //类型后面三个点(String…),是从Java 5开始,Java语言对方法参数支持一种新写法,
    // 叫可变长度参数列表,其语法就是类型后跟…,表示此处接受的参数为0到多个Object类型的对象,
    // 或者是一个Object[]。 例如我们有一个方法叫做test(String…strings),那么你还可以写方法test(),
    // 但你不能写test(String[] strings),这样会出编译错误,系统提示出现重复的方法。
    //参考:https://blog.csdn.net/u012260238/article/details/79365405
    //参考:https://blog.csdn.net/qq_38230811/article/details/78008751
    //================================================================
    //设置签名的观众
    public void setAudiences(String... audienceStr) {
        List<String> audiences = new ArrayList<String>();
        for (String string : audienceStr) {
            audiences.add(string);
        }
        this.audience = audiences;
    }
}

message 包
CodeEnum 枚举类,主要作用:定义向前端输出的提示信息(替代:向前端抛出异常)

package com.sccl.springbootjwt.message;

/** CodeEnum  枚举类,
 * 主要作用:定义向前端输出的提示信息(替代:向前端抛出异常)
 * Create by wangbin
 * 2019-12-04-11:14
 * 功能描述 认证码说明
 */
public enum CodeEnum {
    /**
     *  用户名或者密码错误
     */
    LOGINNAMEANDPWDERROR(100000," 用户名或者密码错误! "),
    /**
     *  非法 token
     */
    ILLEGALTOKEN(110000," 非法 token ! "),
    /**
     * token 已经过期
     */
    EXPIRETOKEN(110001,"token 已经过期! "),
    /**
     * Token  不能为空
     */
    TOKENISEMPTY(110002,"Token  不能为空! "),
    /**
     *  数据解析错误!
     */
    DATAPARSEERROR(110003," 数据解析错误! ");

    private Integer code;
    private String msg;
    private CodeEnum(Integer code,String msg){
        this.code = code;
        this.msg = msg;
    }
    public Integer getCode() {
        return code;
    }
    public void setCode(Integer code) {
        this.code = code;
    }
    public String getMsg() {
        return msg;
    }
    public void setMsg(String msg) {
        this.msg = msg;
    }
}

ReturnMessage 返回消息类

package com.sccl.springbootjwt.message;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/** 返回消息类
 * Create by wangbin
 * 2019-12-04-11:18
 * 功能描述 返回消息类
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class ReturnMessage<T> {
    /**
     *  功能描述 错误码
     *  开发时间 2019/11/25 0025  下午 5:16
     */
    private Integer code;
    /**
     *  功能描述 提示信息
     *  开发时间 2019/11/25 0025  下午 5:16
     */
    private String message;
    /**
     *  功能描述 返回具体内容
     *  开发时间 2019/11/25 0025  下午 5:16
     */
    private T data;
}

ReturnMessageUtil 返回消息工具类,主要针对 ReturnMessage 进行封装

package com.sccl.springbootjwt.message;

/** 返回消息工具类,
 *  主要针对 ReturnMessage 进行封装
 * Create by wangbin
 * 2019-12-04-11:21
 */
public class ReturnMessageUtil {
    /**
     *  无异常 请求成功并有具体内容返回
     * @param object
     * @return
     */
    public static ReturnMessage<Object> sucess(Object object) {
        ReturnMessage<Object> message = new ReturnMessage<Object>(0,"sucess",object);
        return message;
    }
    /**
     *  无异常 请求成功并无具体内容返回
     * @return
     */
    public static ReturnMessage<Object> sucess() {
        ReturnMessage<Object> message = new ReturnMessage<Object>(0,"sucess",null);
        return message;
    }
    /**
     *  有自定义错误异常信息
     * @param code
     * @param msg
     * @return
     */
    public static ReturnMessage<Object> error(Integer code,String msg) {
        ReturnMessage<Object> message = new ReturnMessage<Object>(code,msg,null);
        return message;
    }
    public static ReturnMessage<Object> error(CodeEnum codeEnum) {
        ReturnMessage<Object> message = new ReturnMessage<Object>(codeEnum.getCode(),codeEnum.getMsg(),null);
        return message;
    }
}

sysmag 包

  • controller 包
    该包的主要作用:编写 Controller 完成用户登录或者退出

LoginController

package com.sccl.springbootjwt.sysmag.controller;

import com.google.common.base.Objects;
import com.sccl.springbootjwt.bean.UserBean;
import com.sccl.springbootjwt.exception.customer.CustomerException;
import com.sccl.springbootjwt.jwt.JwtProvide;
import com.sccl.springbootjwt.message.CodeEnum;
import com.sccl.springbootjwt.message.ReturnMessage;
import com.sccl.springbootjwt.message.ReturnMessageUtil;
import com.sccl.springbootjwt.util.Object2MapUtil;
import com.sccl.springbootjwt.util.RedisProvide;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import javax.servlet.http.HttpSession;
import java.util.Map;

/** 此Controller完成用户登录或者退出
 * Create by wangbin
 * 2019-12-04-11:25
 */
@RestController
@RequestMapping("/sys")
public class LoginController {
    Logger log = LoggerFactory.getLogger(this.getClass());
    @Resource
    private RedisProvide redisProvide;
    @Resource
    private JwtProvide jwtProvide;
    @RequestMapping("/login")
    public ReturnMessage<Object> login(String loginName, String password, HttpSession session){
        /* 从数据库中根据登录名和密码查询是否存在用户数据 */
        UserBean userBean = valid(loginName,password);
        if(userBean == null){
            return ReturnMessageUtil.error(CodeEnum.LOGINNAMEANDPWDERROR);
        }
        /* 将 javabean 实体转换为 map 数据,方便向 jwt 中追加数据 */
        Map<String,Object> userInfo = null;
        try{
            userInfo = Object2MapUtil.bean2Map(userBean);
        }catch (IllegalAccessException e){
            log.error("login",e);
            throw new CustomerException(CodeEnum.DATAPARSEERROR);
        }
        /* 转换成功之后,使用 JWT 令牌提供类 */
        String token = jwtProvide.createToken(userInfo);
        /* 最好在这里,再将 token 存入到 redis 中做个备份,方便做校验 */
        redisProvide.set(token,userBean,1);
        log.info("Authorization:"+token);
        return ReturnMessageUtil.sucess(token);
    }
    @GetMapping("/logout")
    public ReturnMessage<?> logout(String token){
        /* 从 redis 中清理令牌信息 */
        redisProvide.del(token);
        return ReturnMessageUtil.sucess();
    }
    /**
     *  功能描述 :  模拟从数据库中查询用户信息
     * @param loginName  登录名
     * @param password  密码
     * @return  用户信息
     *  开发时间 2019-12-04-11:25
     */
    private UserBean valid(String loginName,String password){
        if(Objects.equal("admin", loginName) && Objects.equal("123456", password) ) {
            UserBean userBean = new UserBean();
            userBean.setId(2L);
            userBean.setUserName(" 张三 ");
            userBean.setLoginName("zs");
            userBean.setPassword("123456");
            userBean.setAge(20);
            return userBean;
        }
        return null;
    }
}

usermag 包

  • controller 包
    该包的主要作用:编写 Controller 完成用户资源的 CRUD

UserController

package com.sccl.springbootjwt.usermag.controller;

import com.sccl.springbootjwt.message.ReturnMessage;
import com.sccl.springbootjwt.message.ReturnMessageUtil;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/** 编写此 Controller 完成用户资源的 CRUD
 * Create by wangbin
 * 2019-12-04-14:28
 */
@RequestMapping("/users")
@RestController
public class UserController {
    @PostMapping(value = "/{id}")
    public ReturnMessage<Object> addUserBean(){
        return ReturnMessageUtil.sucess();
    }
    @PutMapping(value = "/{id}")
    public ReturnMessage<Object> updateUserBean(){
        return ReturnMessageUtil.sucess();
    }
}

util 工具包
Object2MapUtil 工具类,完成对象与 Map 数据直接的相互转换

package com.sccl.springbootjwt.util;

import com.auth0.jwt.interfaces.Claim;
import com.sccl.springbootjwt.annotation.FieldMarker;

import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.Map;

/** Object2MapUtil 工具类,完成对象与 Map 数据直接的相互转换
 * Create by wangbin
 * 2019-12-04-14:30
 */
public class Object2MapUtil {
    /** =======================================================================
     * 返回值前面的<T>是声明这个是一个泛型方法,返回的类型的Map<String,Object>,
     *  方法的参数是泛型T
     *  方法返回值前的<T>的左右是告诉编译器,
     *  当前的方法的值传入类型可以和类初始化的泛型类不同,
     *  也就是该方法的泛型类可以自定义,不需要跟类初始化的泛型类相同
     *  关于泛型的讲解请参考:
     * https://blog.csdn.net/huyashangding/article/details/90265492
     * =============================================================
     * 泛型方法,转换 bean 为 map
     * @param source  要转换的 bean
     * @param <T> bean 类型
     * @return  转换结果
     */
    public static <T> Map<String,Object> bean2Map(T source) throws IllegalAccessException{
        Map<String,Object> result = new HashMap<>();
        Class<?> sourceClass = source.getClass();
        // 拿到所有的字段 , 不包括继承的字段
        Field[] sourceField = sourceClass.getDeclaredFields();
        for (Field field:sourceField){
            // Field设置可访问 , 不然拿不到 private
            field.setAccessible(true);
            // 配置 @FieldMarker 注解的属性 , 作为 JWT  负载数据
            //字段上获取FieldMarker注解
            FieldMarker fm = field.getAnnotation(FieldMarker.class);
            if(fm != null){
                result.put(field.getName(),field.get(source));
            }
        }
        return result;
    }
    /**
     * map 转 bean
     * @param source map 属性
     * @param instance  要转换成的实例的类型
     * @return  该 bean
     */
    public static <T> T map2Bean(Map<String,Object> source,Class<T> instance){
        try {
            T object = instance.newInstance();//创建实例
            Field[] fields = object.getClass().getDeclaredFields();//获取所有的字段 , 不包括继承的字段
            for (Field field : fields) {
                field.setAccessible(true);
                FieldMarker fm = field.getAnnotation(FieldMarker.class);
                if (fm != null){
                    Claim value = (Claim)source.get(field.getName());
                    field.set(object,value.asString());
                }
            }
            return object;
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }
}

RedisProvide 类,针对 RedisTemplate 的封装类,为了降低RedisTemplate 模板操作的复杂度

package com.sccl.springbootjwt.util;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

/** RedisProvide  类,
 * 针对 RedisTemplate 的封装类,
 * 为了降低 RedisTemplate 模板操作的复杂度
 * Create by wangbin
 * 2019-12-04-14:57
 */
@Component
public class RedisProvide {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // =============================common============================
    /**
     *  指定缓存失效时间
     *
     * @param key 键
     * @param time  时间 ( 秒 )
     * @return
     */
    public boolean expire(String key,long time){
        try{
            if(time>0){
                redisTemplate.expire(key,time, TimeUnit.SECONDS);
            }
            return true;
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     *  根据 key 获取过期时间
     *
     * @param key  键 不能为 null
     * @return  时间 ( 秒 )  返回 0 代表为永久有效
     */
    public long getExpire(String key){
        return redisTemplate.getExpire(key,TimeUnit.SECONDS);
    }
    /**
     *  判断 key 是否存在
     *
     * @param key  键
     * @return true  存在 false 不存在
     */
    public boolean hasKey(String key){
        try{
            return redisTemplate.hasKey(key);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  删除缓存
     *
     * @param key  可以传一个值 或多个
     */
    @SuppressWarnings("unchecked")
    public void del(String... key){
        if(key !=null && key.length>0){
            if(key.length == 1){
                redisTemplate.delete(key[0]);
            }else{
                redisTemplate.delete(CollectionUtils.arrayToList(key));
            }
        }
    }
    // ============================String=============================
    /**
     *  普通缓存获取
     *
     * @param key  键
     * @return  值
     */
    public Object get(String key){
        return key == null ? null: redisTemplate.opsForValue().get(key);
    }

    /**
     *  普通缓存放入
     *
     * @param key 键
     * @param value  值
     * @return true 成功 false 失败
     */
    public boolean set(String key,Object value){
        try{
            redisTemplate.opsForValue().set(key,value);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  普通缓存放入并设置时间
     *
     * @param key 键
     * @param value  值
     * @param time 时间 ( 秒 ) time 要大于 0  ,
     * 如果 time 小于等于 0  将设置无限期
     * @return true 成功 false  失败
     */
    public boolean set(String key,Object value,long time){
        try {
            if(time>0){
                redisTemplate.opsForValue().set(key,value,time,TimeUnit.HOURS);
            }else {
                set(key,value);
            }
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  递增
     *
     * @param key 键
     * @param delta  要增加几 ( 大于 0)
     * @return
     */
    public long incr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException(" 递增因子必须大于 0");
        }
        return redisTemplate.opsForValue().increment(key, delta);
    }

    /**
     *  递减
     *
     * @param key 键
     * @param delta  要减少几 ( 小于 0)
     * @return
     */
    public long decr(String key, long delta) {
        if (delta < 0) {
            throw new RuntimeException(" 递减因子必须大于 0");
        }
        return redisTemplate.opsForValue().increment(key, -delta);
    }

    // ================================Map=================================
    /**
     * HashGet
     *
     * @param key 键 不能为 null
     * @param item  项 不能为 null
     * @return  值
     */
    public Object hget(String key, String item) {
        return redisTemplate.opsForHash().get(key, item);
    }
    /**
     *  获取 hashKey 对应的所有键值
     *
     * @param key  键
     * @return  对应的多个键值
     */
    public Map<Object, Object> hmget(String key) {
        return redisTemplate.opsForHash().entries(key);
    }
    /**
     * HashSet
     *
     * @param key  键
     * @param map  对应多个键值
     * @return true  成功 false  失败
     */
    public boolean hmset(String key, Map<String, Object> map) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     * HashSet  并设置时间
     *
     * @param key 键
     * @param map 对应多个键值
     * @param time  时间 ( 秒 )
     * @return true 成功 false 失败
     */
    public boolean hmset(String key, Map<String, Object> map, long time) {
        try {
            redisTemplate.opsForHash().putAll(key, map);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  向一张 hash 表中放入数据 , 如果不存在将创建
     *
     * @param key 键
     * @param item 项
     * @param value  值
     * @return true  成功 false 失败
     */
    public boolean hset(String key, String item, Object value) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  向一张 hash 表中放入数据 , 如果不存在将创建
     *
     * @param key 键
     * @param item 项
     * @param value  值
     * @param time 时间 ( 秒 )  注意 : 如果已存在的 hash 表有时间 , 这里将会替换原有的时间
     * @return true  成功 false 失败
     */
    public boolean hset(String key, String item, Object value, long time) {
        try {
            redisTemplate.opsForHash().put(key, item, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  删除 hash 表中的值
     *
     * @param key 键 不能为 null
     * @param item  项 可以使多个 不能为 null
     */
    public void hdel(String key, Object... item) {
        redisTemplate.opsForHash().delete(key, item);
    }
    /**
     *  判断 hash 表中是否有该项的值
     *
     * @param key 键 不能为 null
     * @param item  项 不能为 null
     * @return true  存在 false 不存在
     */
    public boolean hHasKey(String key, String item) {
        return redisTemplate.opsForHash().hasKey(key, item);
    }
    /**
     * hash 递增 如果不存在 , 就会创建一个 并把新增后的值返回
     *
     * @param key 键
     * @param item  项
     * @param by 要增加几 ( 大于 0)
     * @return
     */
    public double hincr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, by);
    }
    /**
     * hash 递减
     *
     * @param key 键
     * @param item  项
     * @param by 要减少记 ( 小于 0)
     * @return
     */
    public double hdecr(String key, String item, double by) {
        return redisTemplate.opsForHash().increment(key, item, -by);
    }
    // ============================set=============================
    /**
     *  根据 key 获取 Set 中的所有值
     *
     * @param key  键
     * @return
     */
    public Set<Object> sGet(String key) {
        try {
            return redisTemplate.opsForSet().members(key);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     *  根据 value 从一个 set 中查询 , 是否存在
     *
     * @param key 键
     * @param value  值
     * @return true  存在 false 不存在
     */
    public boolean sHasKey(String key, Object value) {
        try {
            return redisTemplate.opsForSet().isMember(key, value);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  将数据放入 set 缓存
     *
     * @param key 键
     * @param values  值 可以是多个
     * @return  成功个数
     */
    public long sSet(String key, Object... values) {
        try {
            return redisTemplate.opsForSet().add(key, values);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     *  将 set 数据放入缓存
     *
     * @param key 键
     * @param time 时间 ( 秒 )
     * @param values  值 可以是多个
     * @return  成功个数
     */
    public long sSetAndTime(String key, long time, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().add(key, values);
            if (time > 0) {
                expire(key, time);
            }
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     *  获取 set 缓存的长度
     *
     * @param key  键
     * @return
     */
    public long sGetSetSize(String key) {
        try {
            return redisTemplate.opsForSet().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     *  移除值为 value 的
     *
     * @param key 键
     * @param values  值 可以是多个
     * @return  移除的个数
     */
    public long setRemove(String key, Object... values) {
        try {
            Long count = redisTemplate.opsForSet().remove(key, values);
            return count;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    // ===============================list=================================
    /**
     *  获取 list 缓存的内容
     *
     * @param key 键
     * @param start  开始
     * @param end 结束 0  到 -1 代表所有值
     * @return
     */
    public List<Object> lGet(String key, long start, long end) {
        try {
            return redisTemplate.opsForList().range(key, start, end);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     *  获取 list 缓存的长度
     *
     * @param key  键
     * @return
     */
    public long lGetListSize(String key) {
        try {
            return redisTemplate.opsForList().size(key);
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
    /**
     *  通过索引 获取 list 中的值
     *
     * @param key 键
     * @param index  索引 index>=0 时, 0  表头, 1  第二个元素,依次类推; index<0 时, -1 ,表尾, -2 倒数第二个元素,依次类推
     * @return
     */
    public Object lGetIndex(String key, long index) {
        try {
            return redisTemplate.opsForList().index(key, index);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }
    /**
     *  将 list 放入缓存
     *
     * @param key 键
     * @param value  值
     * @return
     */
    public boolean lSet(String key, Object value) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  将 list 放入缓存
     *
     * @param key 键
     * @param value  值
     * @param time 时间 ( 秒 )
     * @return
     */
    public boolean lSet(String key, Object value, long time) {
        try {
            redisTemplate.opsForList().rightPush(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  将 list 放入缓存
     *
     * @param key 键
     * @param value  值
     * @return
     */
    public boolean lSet(String key, List<Object> value) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  将 list 放入缓存
     *
     * @param key 键
     * @param value  值
     * @param time 时间 ( 秒 )
     * @return
     */
    public boolean lSet(String key, List<Object> value, long time) {
        try {
            redisTemplate.opsForList().rightPushAll(key, value);
            if (time > 0) {
                expire(key, time);
            }
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  根据索引修改 list 中的某条数据
     *
     * @param key 键
     * @param index  索引
     * @param value  值
     * @return
     */
    public boolean lUpdateIndex(String key, long index, Object value) {
        try {
            redisTemplate.opsForList().set(key, index, value);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
    /**
     *  移除 N 个值为 value
     *
     * @param key 键
     * @param count  移除多少个
     * @param value  值
     * @return  移除的个数
     */
    public long lRemove(String key, long count, Object value) {
        try {
            Long remove = redisTemplate.opsForList().remove(key, count, value);
            return remove;
        } catch (Exception e) {
            e.printStackTrace();
            return 0;
        }
    }
}

启动类
SpringBootJwtApplication 启动类, @SpringBootApplication 表示这是一个启动类,也是 spring 应用的加载类

package com.sccl.springbootjwt;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringBootJwtApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringBootJwtApplication.class, args);
    }
}

6.项目演示

  • 6.1 启动项目,使用 Postman 访问 未登录时的效果:
未登录时,没有生成token
  • 6.2 登录系统,后端使用 JWT 分配 Token 信息给前端:
用户身份验证成功后,返回token
  • 6.3 携带 Token ,再次访问后端时的效果:
请求header中携带 Token进行访问
  • 6.4 修改 Token 后,再次访问后端时的效果:
修改了之前返回的token信息再次访问

本Demo整合相对简单,接下来会抽时间,写一下springboot+shiro+jwt的整合demo,附:Demo地址