一、JWT介绍

官方网址
image.png

1. 什么是JWT

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA. ——摘自官网


JSON Web令牌(JWT)是一种开放标准(RFC 7519),它定义了一种紧凑且自包含的方式,用于在各方之间安全地将信息作为JSON对象传输。由于此信息是经过数字签名的,因此可以被验证和信任。可以使用秘密(使用HMAC算法)或使用RSAECDSA的公用/专用密钥对对JWT进行签名

通俗解释: JWT简称JSON Web Token,也就是通过JSON形式作为Web应用中的令牌,用于在各方之间安全地将信息作为JSON对象传输。在数据传输过程中还可以完成数据加密、签名等相关处理。

2. JWT能做什么

1. 授权

这是使用JWT的最常见方案。一旦用户登录,每个后续请求将包括JWT,从而允允许的路由,服务和资源。单点登录是当今广泛使用JWT的一项功能,因为它的开销很小并且可以在不同的域中轻松使用。

2. 信息交换

JSON Web Token是在各方之间安全地传输信息的好方法。因为可以对JWT进行签名(例如,使用公钥/私钥对),所以您可以确保发件人是他们所说的人。此外,由于签名是使用标头和有效负载计算的,因此您还可以验证内容是否遭到篡改

注意
注意:jwt跟session不一样,jwt存储在客户端,session存储在服务器端,服务器断电后session就没了,而jwt因为存储在客户端,所以就不会被影响,只要jwt不过期,就可以继续使用。

3. 为什么是JWT

1. 基于传统的Session认证

认证方式

我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了用户名和密码来进行用户认证,那么下一次请求时,用户还要再一次进行用户认证才行,因为根据http协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为cookie,以便下次请求时发送给我们的应用,这样我们的应用就能识别请求来自哪个用户了,这就是传统的基于session认证。

JWT - 图2

暴露的问题

  • 每个用户经过我们的应用认证之后,我们的应用都要在服务端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中,而随着认证用户的增多,服务端的开销会明显增大
  • 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应的限制了负载均衡器的能力。这也意味着限制了应用的扩展能力。
  • 因为是基于cookie来进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击
  • 在前后端分离系统中就更加痛苦,如下图所示:也就是说前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次。如果用session 每次携带sessionid 到服务器,服务器还要查询用户信息。同时如果用户很多。这些信息存储在服务器内存中,给服务器增加负担。还有就是CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的, cookie如果被截获,用户就会很容易受到跨站请求伪造的攻击。还有就是sessionid就是一个特征值,表达的信息不够丰富。不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制。不方便集群应用。

    1. ![](https://cdn.nlark.com/yuque/0/2021/png/12376550/1619496146137-a2488a20-e027-4cdd-b133-9f67c4b40179.png#align=left&display=inline&height=120&margin=%5Bobject%20Object%5D&originHeight=480&originWidth=2446&size=0&status=done&style=none&width=612)

2. 基于JWT认证

JWT - 图3

认证流程
  • 首先,前端通过Web表单将自己的用户名和密码发送到后端的接口。这一过程一般是一个HTTP POST请求。建议的方式是通过SSL加密的传输(https协议),从而避免敏感信息被嗅探。
  • 后端核对用户名和密码成功后,将用户的id等其他信息作为JWT Payload(负载),将其与头部分别进行Base64编码拼接后签名,形成一个JWT(Token)。形成的JWT就是一个形同lll.zzz.xxx的字符串。 (token head.payload.singurater)
  • 后端将JWT字符串作为登录成功的返回结果返回给前端。前端可以将返回的结果保存在localStorage或sessionStorage上,退出登录时前端删除保存的JWT即可。
  • 前端在每次请求时将JWT放入HTTP Header中的Authorization位。(解决XSS和XSRF问题) HEADER
  • 后端检查是否存在,如存在验证JWT的有效性。例如,检查签名是否正确;检查Token是否过期;检查Token的接收方是否是自己(可选)。
  • 验证通过后后端使用JWT中包含的用户信息进行其他逻辑操作,返回相应结果


JWT优势

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

4. JWT的结构

JWT的Token是一个由 [header].[payload].[singnature] 组成的字符串

令牌组成

  1. 标头(Header)
  2. 有效载荷(Payload)
  3. 签名(Signature)

因此,JWT通常如下所示:xxxxx.yyyyy.zzzzz Header.Payload.Signature

1. Header

标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256或RSA。它会使用 Base64 编码组成 JWT 结构的第一部分

注意:Base64是一种编码,也就是说,它是可以被翻译回原来的样子来的。它并不是一种加密过程

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

2. Payload

令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的声明。同样的,它会使用 Base64 编码组成 JWT 结构的第二部分

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

3. Singnature

前面两部分都是使用 Base64 进行编码的,即前端可以解开知道里面的信息。Signature 需要使用编码后的 header 和 payload 以及服务器提供的一个密钥,然后使用 header 中指定的签名算法(HS256)进行签名。签名的作用是保证 JWT 没有被篡改过
如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret);

签名目的
最后一步签名的过程,实际上是对头部(Header)以及负载内容(Payload)进行签名,防止内容被窜改。如果有人对头部以及负载的内容解码之后进行修改,再进行编码,最后加上之前的签名组合形成新的JWT的话,那么服务器端会判断出新的头部和负载形成的签名和JWT附带上的签名是不一样的。如果要对新的头部和负载进行签名,在不知道服务器加密时用的密钥的话,得出来的签名也是不一样的
JWT - 图4

信息安全问题

在这里大家一定会问一个问题:Base64是一种编码,是可逆的,那么我的信息不就被暴露了吗?
是的。所以,在JWT中,不应该在负载里面加入任何敏感的数据。在上面的例子中,我们传输的是用户的User ID。这个值实际上不是什么敏感内容,一般情况下被知道也是安全的。但是像密码这样的内容就不能被放在JWT中了。如果将用户的密码放在了JWT中,那么怀有恶意的第三方通过Base64解码就能很快地知道你的密码了。因此JWT适合用于向Web应用传递一些非敏感信息。JWT还经常用于设计用户认证和授权系统,甚至实现Web应用的单点登录

结构优点

输出是三个由点分隔的Base64-URL字符串,可以在HTML和HTTP环境中轻松传递这些字符串,与基于XML的标准(例如SAML)相比,它更紧凑。

  • 简洁(Compact),可以通过URL, POST 参数或者在 HTTP header 发送,因为数据量小,传输速度快
  • 自包含(Self-contained),负载中包含了所有用户所需要的信息,避免了多次查询数据库

JWT - 图5

二、第一个JWT程序

1. 创建Maven项目

以SpringBoot项目为例

2. 引入JWT依赖

<dependency>
    <groupId>com.auth0</groupId>
    <artifactId>java-jwt</artifactId>
    <version>3.4.0</version>
</dependency>

3. 生成Token

public void createJwtToken() {
    //设置超时时间:100秒后Token失效
    Calendar instance = Calendar.getInstance();
    instance.add(Calendar.SECOND, 100);

    String token = JWT.create()
        //设置Token信息
        .withClaim("username", "KYN")
        .withClaim("age", 18)
        //设置Token到期时间
        .withExpiresAt(instance.getTime())
        //设置算法以及密钥
        .sign(Algorithm.HMAC256("@GMF2003"));

    //打印Token查看
    System.out.println(token);
}

image.png

4. 根据令牌和算法密钥解析数据

public void resolverToken(){
    //指定算法与密钥获取获取JWT校验器对象
    JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("@GMF2003")).build();
    //根据指定Token获取解析对象
    DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2MTk1MTI5NDUsImFnZSI6MTgsInVzZXJuYW1lIjoiS1lOIn0.XtwQJm5kk9-L5S3zvrOP7kEg-aKO95Dnb5mvJp0eAjs");
    //存与取的类型需一致
    System.out.println("username = "+verify.getClaim("username").asString());
    System.out.println("age = "+verify.getClaim("age").asInt());
    System.out.println("过期时间 = "+verify.getExpiresAt());
}

5. 可能会出现的异常

image.png

1. 令牌过期异常—TokenExpiredException

image.png

2. 算法不匹配异常—AlgorithmMismatchException

image.png
_

3. 签名不匹配异常—SignatureVerificationException

image.png

4. 失效的payload异常—InvalidClaimException

_

三、封装工具类

JwtUtil.java

import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.util.Calendar;
import java.util.Map;

/**
 * @author GMF
 * @className: JwtUtil
 * @description JWT封装的工具类(用于获取Token、校验Token、获取Token解析器)
 * @date 2021/4/27
 * @time 17:27
 */
@Component
public class JwtUtil {
    @Value("${jwt.secretKey}")
    public String secretKey;

    /**
     * @param map
     * @return java.lang.String
     * @description 根据传递的数据按照指定算法与密钥生成Token并返回
     */
    public String getToken(Map<String, String> map) {
        //设置令牌过期时间(4天后过期)
        Calendar instance = Calendar.getInstance();
        instance.add(Calendar.DATE,4);

        JWTCreator.Builder builder = JWT.create();
        //遍历Map集合设置Token信息
        map.forEach((key,val) -> {
            builder.withClaim(key,val);
        });

        //设置令牌过期时间
        builder.withExpiresAt(instance.getTime());

        //生成Token令牌后返回
        return builder.sign(Algorithm.HMAC256(secretKey));
    }

    /**
     * @param token
     * @return void
     * @description 校验Token,在调用端捕获异常作为是否校验成功的依据
     */
    public void verifyToken(String token){
        JWT.require(Algorithm.HMAC256(secretKey)).build().verify(token);
    }

    /**
     * @param token 
     * @return com.auth0.jwt.interfaces.DecodedJWT
     * @description 获取解析Token后装载所有数据的对象
     */
    public DecodedJWT getTokenInfo(String token){
        return JWT.require(Algorithm.HMAC256(token)).build().verify(token);
    }

    /**
     * @param token
     * @param key
     * @return java.lang.String
     * @description 解析Token,根据指定键获取数据
     */
    public String getTokenInfo(String token, String key){
        return JWT.require(Algorithm.HMAC256(token)).build().verify(token).getClaim(key).asString();
    }
}

四、整合SpringBoot

1. 引入依赖

<dependencies>
        <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>com.auth0</groupId>
            <artifactId>java-jwt</artifactId>
            <version>3.4.0</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.1.4</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.23</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.2.3</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.18</version>
        </dependency>
    </dependencies>

2. 数据库

CREATE TABLE `user`(
   `id` INT NOT NULL AUTO_INCREMENT,
   `username` VARCHAR(20) NOT NULL,
   `password` VARCHAR(30) NOT NULL,
   PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

INSERT INTO jwt VALUES(NULL,'KYN','321')

3. application.yaml

jwt:
  secretKey: '@GMF2003'
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/test?serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

4. mapper、controller、service

UserMapper.java

@Mapper
public interface UserMapper {
    @Select("select * from user where username = #{username} and password = #{password}")
    User selectUserById(User user);
}

UserController.java

@RestController
public class UserController {
    @Autowired
    UserService userService;

    @Autowired
    JwtUtil jwtUtil;

    @GetMapping("/user/login")
    public Map<String,Object> login(User user){
        return userService.login(user);
    }

    @PostMapping("/list/selectAll")
    public String selectUsers(String token){
        //经过拦截器校验Token,成功返回数据
        return "All Users";
    }
}

UserService.java

@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    JwtUtil jwtUtil;

    @Override
    public Map<String, Object> login(User user){
        Map<String, Object> map = new HashMap<>();
        User loginUser = userMapper.selectUserById(user);
        if (loginUser != null){
            map.put("state",true);
            map.put("message","登陆成功");
            map.put("token",jwtUtil.getToken(null));
        }else {
            map.put("state",false);
            map.put("message","用户名或密码错误");
        }

        return map;
    }
}

5. Interceptor拦截器校验Token

LoginInterceptor.java

/**
 * @className: LoginInterceptor
 * @description 使用JWT Token判断是否登陆
 * @author GMF
 * @date 2021/4/27
 * @time 22:10
*/
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Autowired
    JwtUtil jwtUtil;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        String token = request.getHeader("token");
        Map<String, Object> map = new HashMap<>();
        try {
            jwtUtil.verifyToken(token);
            return true;
        } catch (TokenExpiredException e) {
            map.put("state", false);
            map.put("msg", "Token已经过期!!!");
        } catch (SignatureVerificationException e){
            map.put("state", false);
            map.put("msg", "签名错误!!!");
        } catch (AlgorithmMismatchException e){
            map.put("state", false);
            map.put("msg", "加密算法不匹配!!!");
        } catch (Exception e) {
            e.printStackTrace();
            map.put("state", false);
            map.put("msg", "无效token~~");
        }

        String json = new ObjectMapper().writeValueAsString(map);
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(json);
        return false;
    }
}

MyConfig.java

@Configuration
public class MyConfig {
    @Autowired
    LoginInterceptor loginInterceptor;

    @Bean
    WebMvcConfigurer webMvcConfigurer(){
        return new WebMvcConfigurer() {
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                InterceptorRegistration registration = registry.addInterceptor(loginInterceptor);
                registration.addPathPatterns("/**");
                registration.excludePathPatterns("/user/login");
            }
        };
    }
}

5. 测试结果

登陆成功返回令牌
image.png

带上 token 验证令牌访问接口
image.png

不带 token 访问
image.png

五、JJWT

1. JJWT介绍

1. 概述

JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。

  • JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
  • JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。
  • JJWT还添加了一些不属于规范的便利扩展,比如JWT压缩和索赔强制。

2. JJWT 规范兼容

  • 创建和解析明文压缩JWTs
  • 创建、解析和验证所有标准JWS算法的数字签名压缩JWTs(又称JWSs):
  • HS256:使用SHA-256的HMAC
  • HS384:使用SHA-384的HMAC
  • HS512:使用SHA-512的HMAC
  • RS256:使用SHA-256的RSASSA-PKCS-v1_5
  • RS384:使用SHA-384的RSASSA-PKCS-v1_5
  • RS512:使用SHA-512的RSASSA-PKCS-v1_5
  • PS256:使用SHA-256的RSASSA-PSS和使用SHA-256的MGF1
  • PS384:使用SHA-384的RSASSA-PSS和使用SHA-384的MGF1
  • PS512:使用SHA-512的RSASSA-PSS和使用SHA-512的MGF1
  • ES256:使用P-256和SHA-256的ECDSA
  • ES384:使用P-384和SHA-384的ECDSA
  • ES512:使用P-521和SHA-512的ECDSA

下面我们根据 https://github.com/jwtk/jjwt 上的demo,来介绍下 JJWT 的用法。

2. 使用步骤

1. 引入依赖

jjwt 提供了 Maven 和 Gradle 两种构建方式,Maven 配置如下即可使用 JJWT

<dependency>     
    <groupId>io.jsonwebtoken</groupId>     
    <artifactId>jjwt</artifactId>     
    <version>0.9.0</version>
</dependency>

注意:JJWT依赖于Jackson 2.x. 如果您已经在您的应用程序中使用了旧版本的 Jackson 请升级相关配置

2. 签发JWT

public static String createJWT() {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        SecretKey secretKey = generalKey();
        JwtBuilder builder = Jwts.builder()
                .setId(id)                                      // JWT_ID
                .setAudience("")                                // 接受者
                .setClaims(null)                                // 自定义属性
                .setSubject("")                                 // 主题
                .setIssuer("")                                  // 签发者
                .setIssuedAt(new Date())                        // 签发时间
                .setNotBefore(new Date())                       // 失效时间
                .setExpiration(long)                                // 过期时间
                .signWith(signatureAlgorithm, secretKey);           // 签名算法以及密匙
        return builder.compact();
}

3. 验证JWT

public static Claims parseJWT(String jwt) throws Exception {
    SecretKey secretKey = generalKey();
    return Jwts.parser()
        .setSigningKey(secretKey)
        .parseClaimsJws(jwt)
        .getBody();
}

4. 完整示例

import com.google.gson.Gson;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtil {

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public SecretKey generalKey() {
        String stringKey = Constant.JWT_SECRET;

        // 本地的密码解码
        byte[] encodedKey = Base64.decodeBase64(stringKey);

        // 根据给定的字节数组使用AES加密算法构造一个密钥
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    /**
     * 创建jwt
     * @param id
     * @param issuer
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

        // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
        Map<String, Object> claims = new HashMap<>();
        claims.put("uid", "123456");
        claims.put("user_name", "admin");
        claims.put("nick_name", "X-rapido");

        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
        // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        SecretKey key = generalKey();

        // 下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
                .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(id)                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           // iat: jwt的签发时间
                .setIssuer(issuer)          // issuer:jwt签发人
                .setSubject(subject)        // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥

        // 设置过期时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public Claims parseJWT(String jwt) throws Exception {
        SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //设置签名的秘钥
                .parseClaimsJws(jwt).getBody();     //设置需要解析的jwt
        return claims;
    }

    public static void main(String[] args) {

        User user = new User("GMF", "bulingbuling", "123");
        String subject = new Gson().toJson(user);

        try {
            JwtUtil util = new JwtUtil();
            String jwt = util.createJWT(Constant.JWT_ID, "Anson", subject, Constant.JWT_TTL);
            System.out.println("JWT:" + jwt);

            System.out.println("\n解密\n");

            Claims c = util.parseJWT(jwt);
            System.out.println(c.getId());
            System.out.println(c.getIssuedAt());
            System.out.println(c.getSubject());
            System.out.println(c.getIssuer());
            System.out.println(c.get("uid", String.class));

        } catch (Exception e) {
            e.printStackTrace();
        }

    }
}

5. 常量类 Constant.java

import java.util.UUID;

public class Constant {
    public static final String JWT_ID = UUID.randomUUID().toString();

    /**
     * 加密密文
     */
    public static final String JWT_SECRET = "woyebuzhidaoxiediansha";
    public static final int JWT_TTL = 60*60*1000;  //millisecond
}

6. 输出示例

JWT:eyJhbGciOiJIUzI1NiJ9.eyJ1aWQiOiIxMjM0NTYiLCJzdWIiOiJ7XCJuaWNrbmFtZVwiOlwidGluZ2ZlbmdcIixcIndlY2hhdFwiOlwiYnVsaW5nYnVsaW5nXCIsXCJxcVwiOlwiMTA1Njg1NjE5MVwifSIsInVzZXJfbmFtZSI6ImFkbWluIiwibmlja19uYW1lIjoiWC1yYXBpZG8iLCJpc3MiOiJBbnNvbiIsImV4cCI6MTUyMjMxNDEyNCwiaWF0IjoxNTIyMzEwNTI0LCJqdGkiOiJhNGQ5MjA0Zi1kYjM3LTRhZGYtODE0NS1iZGNmMDAzMzFmZjYifQ.B5wdY3_W4MZLj9uBHSYalG6vmYwdpdTXg0otdwTmU4U

解密

a4d9204f-db37-4adf-8145-bdcf00331ff6
Thu Mar 29 16:02:04 CST 2018
{"nickname":"GMF","wechat":"bulingbuling","qq":"123"}
Anson
123456

六、JWT+SpringSecurity实现登陆/权限认证

1. 代码执行流程图

JWT - 图14

登陆

JWT - 图15

认证

JWT - 图16

2. 环境搭建

1. 引入依赖

<dependencies>
    <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>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <optional>true</optional>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>com.baomidou</groupId>
      <artifactId>mybatis-plus-boot-starter</artifactId>
      <version>3.4.2</version>
    </dependency>

    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>8.0.23</version>
    </dependency>

    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid-spring-boot-starter</artifactId>
      <version>1.2.4</version>
    </dependency>

    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- jwt -->
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-api</artifactId>
      <version>0.10.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-impl</artifactId>
      <version>0.10.6</version>
    </dependency>
    <dependency>
      <groupId>io.jsonwebtoken</groupId>
      <artifactId>jjwt-jackson</artifactId>
      <version>0.10.6</version>
    </dependency>
</dependencies>

2. 搭建数据库

-- 用户表
DROP TABLE `user`
CREATE TABLE `user`(
   `id` INT NOT NULL AUTO_INCREMENT,
   `username` VARCHAR(20) NOT NULL,
   `password` VARCHAR(30) NOT NULL,
   PRIMARY KEY (`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
INSERT INTO `user` VALUES(NULL,'GMF','237327')

-- 权限表
DROP TABLE `role`;
CREATE TABLE role(
   `id` INT NOT NULL AUTO_INCREMENT, 
   `name` VARCHAR(10) NOT NULL,  -- 权限名称
   `url` VARCHAR(50) NOT NULL,     -- 权限设定访问的URL
   PRIMARY KEY(`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO role VALUES(NULL,'admin','/auth/vip');
INSERT INTO role VALUES(NULL,'vip','/auth/admin');
SELECT * FROM role


-- 用户权限中间表
drop table `role_user`
CREATE TABLE `role_user`(
    `id` INT AUTO_INCREMENT,
    `user_id` INT NOT NULL,
    `role_id` INT NOT NULL,
    PRIMARY KEY(`id`)
) ENGINE=INNODB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

INSERT INTO role_user VALUES(NULL,1,1);
INSERT INTO role_user VALUES(NULL,1,2);
SELECT * FROM role_user

3. 实体类

AuthorityAdmin.java

import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;
import org.springframework.security.core.GrantedAuthority;

/**
 * @className: AuthorityAdmin
 * @description 权限相关信息实体类
 * @author GMF
 * @date 2021/4/29
 * @time 22:12
*/
@Data
@TableName("role")
public class AuthorityAdmin implements GrantedAuthority {
    private Integer id;

    //权限名称
    private String name;

    //权限对应能够访问的URL
    private String url;

    @JsonIgnore
    @Override
    public String getAuthority() {
        return name;
    }
}

User.java

@Data
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Integer id;
    private String username;
    private String password;
}

SecurityUser.java

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

/**
 * SecurityUser 权限判断类 (用户信息子类)
 */
@Data
public class SecurityUser extends User implements UserDetails {

    private static final long serialVersionUID = 1L;

    //权限列表
    private Collection<? extends GrantedAuthority> authorities;

    //账户是否未过期,过期无法验证,在springSecurity 验证中自动调用
    boolean isAccountNonExpired;

    //指定用户是否解锁,锁定的用户无法进行身份验证,在springSecurity 验证中自动调用
    boolean isAccountNonLocked;

    //指示是否已过期的用户的凭据(密码),过期的凭据防止认证,在springSecurity 验证中自动调用
    boolean isCredentialsNonExpired;

    //是否可用 ,禁用的用户不能身份验证,在springSecurity 验证中自动调用
    boolean isEnabled;

}

4. application.yaml

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/springsecurity?serverTimezone=UTC
    username: root
    password: root
    driver-class-name: com.mysql.cj.jdbc.Driver
    type: com.alibaba.druid.pool.DruidDataSource

mybatis:
  mapper-locations: classpath:mapper/*.xml
  configuration:
    map-underscore-to-camel-case: true

5. 工具类

JJwtUtil.java

import com.gmf.entity.SecurityUser;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import java.io.IOException;
import java.security.Key;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;

/**
 * @className: JJwtUtil
 * @description TODO  jwt 工具类
 * @author GMF
*/
public class JJwtUtil {

    // 主题
    private static final String SUBJECT = "GMF";

    // jwt的token有效期,
    //private static final long EXPIRITION = 1000L * 60 * 60 * 24 * 7;//7天
    private static final long EXPIRITION = 1000L * 60 * 30;   // 半小时

    // 加密key(黑客没有该值无法篡改token内容)
    private static final String APPSECRET_KEY = "sadsc21414adgarmi1m24i12h4hausdgjihhosafdFWQEWQDSAFSAGVG";

    // 用户url权限列表key
    private static final String AUTH_CLAIMS = "auth";

    //经过BASE64编码后的SecretKey
    private static final Key key;

    static {
        byte[] keyBytes = Decoders.BASE64.decode(APPSECRET_KEY);
        key = Keys.hmacShaKeyFor(keyBytes);
    }

    /**
     * @param user
     * @return java.lang.String
     * @description TODO  生成token
     */
    public static String generateToken(SecurityUser user) {
        String token = Jwts
                .builder()
                // 主题
                .setSubject(SUBJECT)
                // 添加jwt自定义值
                .claim(AUTH_CLAIMS, user.getAuthorities())
                .claim("username", user.getUsername())
                .claim("userId", user.getId())
                .setIssuedAt(new Date())
                // 过期时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRITION))
                // 加密方式,加密key
                .signWith(key,SignatureAlgorithm.HS256).compact();
        return token;
    }


    public static Claims checkJWT(String token) {
        try {
            final Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
            return claims;
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 获取用户Id
     *
     * @param token
     * @return
     */
    public static String getUserId(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("userId").toString();
    }


    /**
     * 获取用户名
     *
     * @param token
     * @return
     */
    public static String getUsername(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        return claims.get("username").toString();
    }

    /**
     * 获取用户角色的权限列表, 没有返回空
     *
     * @param token
     * @return
     */
    public static List<SimpleGrantedAuthority> getUserAuth(String token) throws IOException {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        List<LinkedHashMap> auths = (List) claims.get(AUTH_CLAIMS);
        List<SimpleGrantedAuthority> grantedAuthorityList = new LinkedList<>();
        auths.forEach((map->{
            grantedAuthorityList.add(new SimpleGrantedAuthority(map.get("name").toString()));
        }));
        return grantedAuthorityList;
    }

    /**
     * 是否过期
     *
     * @param token
     * @return
     */
    public static boolean isExpiration(String token) {
        Claims claims = Jwts.parser().setSigningKey(APPSECRET_KEY).parseClaimsJws(token).getBody();
        System.out.println("过期时间: " + claims.getExpiration());
        return claims.getExpiration().before(new Date());
    }
}

MD5Util.java

import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 1.MD5加密字符串(32位大写)
 * 2.MD5加密字符串(32位小写)
 * <p>
 * MD5在线加密:https://md5jiami.51240.com/
 * 3.将二进制字节数组转换为十六进制字符串
 * 4.Unicode中文编码转换成字符串
 */
public class MD5Util {

    /**
     * MD5加密字符串(32位大写)
     *
     * @param string 需要进行MD5加密的字符串
     * @return 加密后的字符串(大写)
     */
    public static String md5Encrypt32Upper(String string) {
        byte[] hash;
        try {
            //创建一个MD5算法对象,并获得MD5字节数组,16*8=128位
            hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Huh, MD5 should be supported?", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Huh, UTF-8 should be supported?", e);
        }

        //转换为十六进制字符串
        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) hex.append("0");
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString().toUpperCase();
    }

    /**
     * MD5加密字符串(32位小写)
     *
     * @param string 需要进行MD5加密的字符串
     * @return 加密后的字符串(小写)
     */
    public static String md5Encrypt32Lower(String string) {
        byte[] hash;
        try {
            //创建一个MD5算法对象,并获得MD5字节数组,16*8=128位
            hash = MessageDigest.getInstance("MD5").digest(string.getBytes("UTF-8"));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException("Huh, MD5 should be supported?", e);
        } catch (UnsupportedEncodingException e) {
            throw new RuntimeException("Huh, UTF-8 should be supported?", e);
        }

        //转换为十六进制字符串
        StringBuilder hex = new StringBuilder(hash.length * 2);
        for (byte b : hash) {
            if ((b & 0xFF) < 0x10) hex.append("0");
            hex.append(Integer.toHexString(b & 0xFF));
        }
        return hex.toString().toLowerCase();
    }

    /**
     * 将二进制字节数组转换为十六进制字符串
     *
     * @param bytes 二进制字节数组
     * @return 十六进制字符串
     */
    public static String bytesToHex(byte[] bytes) {
        StringBuffer hexStr = new StringBuffer();
        int num;
        for (int i = 0; i < bytes.length; i++) {
            num = bytes[i];
            if (num < 0) {
                num += 256;
            }
            if (num < 16) {
                hexStr.append("0");
            }
            hexStr.append(Integer.toHexString(num));
        }
        return hexStr.toString().toUpperCase();
    }

    /**
     * Unicode中文编码转换成字符串
     */
    public static String unicodeToString(String str) {
        Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))");
        Matcher matcher = pattern.matcher(str);
        char ch;
        while (matcher.find()) {
            ch = (char) Integer.parseInt(matcher.group(2), 16);
            str = str.replace(matcher.group(1), ch + "");
        }
        return str;
    }
}

3. JWT配置

1. JWT登陆Filter

**JWTLoginFilter.java:用于用户登陆的校验以及Token的生产

import com.fasterxml.jackson.databind.ObjectMapper;
import com.gmf.entity.SecurityUser;
import com.gmf.error.ErrorException;
import com.gmf.util.JJwtUtil;
import lombok.SneakyThrows;
import org.springframework.security.authentication.*;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;

/**
 * TODO  登录  ===> POST请求( 账号:username=?, 密码:password=?)
 *
 * 登录会调用springSecurity的登录方法进行验证
 *<p>
 * ===== 登录成功
 * http状态status状态返回200,并且自定义响应状态code返回200,响应头存放token,key = token,value = jwt生成的token内容
 * ===== 登录失败
 * http状态status状态返回401,并且自定义响应状态code返回401,并提示对应的内容
 * ===== 权限不足
 *  http状态status状态返回403,并且自定义响应状态code返回403,并提示对应的内容
 * </p>
 * @author GMF
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
    /**
     * 获取授权管理, 创建JWTLoginFilter时获取
     */
    private AuthenticationManager authenticationManager;

    /**
     * 异常处理类
     */
    private HandlerExceptionResolver resolver;


    /**
     * 创建JWTLoginFilter,构造器,定义后端登陆接口-【/auth/login】,当调用该接口直接执行 attemptAuthentication 方法
     *
     * @param authenticationManager
     */
    public JWTLoginFilter(AuthenticationManager authenticationManager, HandlerExceptionResolver resolver) {
        this.authenticationManager = authenticationManager;
        this.resolver = resolver;
        super.setFilterProcessesUrl("/auth/login");
    }


    /**
     * TODO 一旦调用登录接口 /auth/login,立即执行该方法
     *
     * @param request
     * @param response
     * @return
     */
    @SneakyThrows
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) {
        SecurityUser user = new SecurityUser();
        request.setCharacterEncoding("UTF-8");
        try {
            String username = request.getParameter("username");
            user.setUsername(username);
            String password = request.getParameter("password");
            user.setPassword(password);
            //user = new ObjectMapper().readValue(request.getInputStream(), SecurityUser.class);
        } catch (Exception e) {
            // e.printStackTrace();
            resolver.resolveException(request, response, null, new ErrorException(401, "没有传递对应的参数"));
            return null;
        }
        // 调用springSecurity的 XiJiaUserDetailsServiceImpl 的 loadUserByUsername 方法进行登录认证,传递账号密码
        return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), new ArrayList<>()));
    }

    /**
     * TODO  一旦调用 springSecurity认证登录成功,立即执行该方法
     *
     * @param request
     * @param response
     * @param chain
     * @param authResult
     * @throws IOException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException {
        // 生成jwt 放入 Header
        SecurityUser userEntity = (SecurityUser) authResult.getPrincipal();
        String jwtToken = JJwtUtil.generateToken(userEntity);
        response.addHeader("token", jwtToken);

        // 响应
        response.setContentType("application/json;charset=utf-8");
        response.setStatus(HttpServletResponse.SC_OK);
        PrintWriter out = response.getWriter();
        Map<String, Object> map = new HashMap<>();
        map.put("code", 200);
        map.put("message", "登录成功!");
        out.write(new ObjectMapper().writeValueAsString(map));
        out.flush();
        out.close();
    }


    /**
     * TODO 一旦调用 springSecurity认证失败 ,立即执行该方法
     *
     * @param request
     * @param response
     * @param ex
     * @throws IOException
     */
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException ex) {
        if (ex instanceof UsernameNotFoundException || ex instanceof BadCredentialsException) {
            resolver.resolveException(request, response, null, new ErrorException(401, "用户名或密码错误"));
        } else if (ex instanceof InternalAuthenticationServiceException) {
            resolver.resolveException(request, response, null, new ErrorException(401, "没有账号信息"));
        } else if (ex instanceof DisabledException) {
            resolver.resolveException(request, response, null, new ErrorException(401, "账户被禁用"));
        } else {
            resolver.resolveException(request, response, null, new ErrorException(401, "登录失败!"));
        }
    }
}

2. JWT校验Filter

JWTValidFilter.java:请求需经过 JWT Toekn 校验

import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.gmf.error.ErrorException;
import com.gmf.util.JJwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;

@Slf4j
public class JWTValidFilter extends BasicAuthenticationFilter {
    // 异常处理类
    private HandlerExceptionResolver resolver;

    /**
     * SecurityConfig 配置中创建该类实例
     */
    public JWTValidFilter(AuthenticationManager authenticationManager, HandlerExceptionResolver resolver) {
        // 获取授权管理
        super(authenticationManager);
        // 获取异常处理类
        this.resolver = resolver;
        this.resolver = resolver;
    }


    /**
     * 拦截请求
     *
     * @param request
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        log.info("请求方式:{} 请求URL:{} ", request.getMethod(), request.getServletPath());
        // 获取token, 没有token直接放行
        String token = request.getHeader("token");
        if (StringUtils.isBlank(token) || "null".equals(token)) {
            super.doFilterInternal(request, response, chain);
            return;
        }
        // 有token进行权限验证
        List<SimpleGrantedAuthority> userAuthList = null;
        String username = null;
        try {
            //  权限列表
            userAuthList = JJwtUtil.getUserAuth(token);
            //  获取账号
            username = JJwtUtil.getUsername(token);
        } catch (ExpiredJwtException ex) {
            resolver.resolveException(request, response, null, new ErrorException(10000, "登录过期"));
            return;
        } catch (Exception e) {
            resolver.resolveException(request, response, null, new ErrorException(10000, "JWT解析错误"));
            return;
        }
        //  添加账户的权限信息,和账号是否为空,然后保存到Security的Authentication授权管理器中
        if (StringUtils.isNotBlank(username) && userAuthList != null) {
            SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(username, null, userAuthList));
        }
        super.doFilterInternal(request, response, chain);
    }
}

_

4. 配置SpringSecurity

1. UserDetailsService实现类进行登陆校验

UserService.java

import org.springframework.security.core.userdetails.UserDetailsService;

public interface UserService extends UserDetailsService {
}

UserServiceImpl.java

import com.gmf.entity.SecurityUser;
import com.gmf.mapper.AuthorityAdminMapper;
import com.gmf.mapper.UserMapper;
import com.gmf.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;


@Service
public class UserServiceImpl implements UserService {
    @Autowired
    UserMapper userMapper;

    @Autowired
    AuthorityAdminMapper authorityAdminMapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SecurityUser user = userMapper.findByName(username);
        // 账号查询( 账号必须唯一)
        if (user == null) {
            throw new UsernameNotFoundException("用户不存在!");
        }

        // 账号密码及禁用过期等, // 状态 false, springSecurity 将验证失败,并返回不同的异常,失败方法根据不同异常返回不同的提示信息
        user.setUsername(user.getUsername());
        user.setPassword(user.getPassword());
        user.setAccountNonExpired(true);       // 是否过期
        user.setAccountNonLocked(true);        // 是否解锁
        user.setCredentialsNonExpired(true);   // 凭据(密码)是否过期
        user.setEnabled(true);                 // 是否禁用

        return user;
    }
}

2. SecurityConfig配置类

SecurityConfig.java

import com.gmf.entity.AuthorityAdmin;
import com.gmf.filter.JWTLoginFilter;
import com.gmf.filter.JWTValidFilter;
import com.gmf.mapper.AuthorityAdminMapper;
import com.gmf.service.impl.UserServiceImpl;
import com.gmf.util.MD5Util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.servlet.HandlerExceptionResolver;

import java.util.List;

/**
 * @className: SecurityConfig
 * @description SpringSecurity相关配置
 * @author GMF
*/
@Configuration
//@EnableGlobalMethodSecurity(securedEnabled = true)
public class SecurityConfig {
    // 登录-认证方法->loadUserByUsername
    @Autowired
    private UserServiceImpl userService;

    // 异常处理类,在 filter无法使用全局异常,在 .addFilter(new JWTValidFilter 中传递该对象过去,便于返回异常信息
    @Autowired
    @Qualifier("handlerExceptionResolver")
    private HandlerExceptionResolver resolver;

    // 当前系统权限表
    @Autowired
    private AuthorityAdminMapper authorityAdminMapper;

    /**
     * 认证
     *
     * @return
     */
    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        //对默认的UserDetailsService进行覆盖
        authenticationProvider.setUserDetailsService(userService);
        authenticationProvider.setPasswordEncoder(new PasswordEncoder() {

            // 对密码MD5
            @Override
            public String encode(CharSequence rawPassword) {
                return MD5Util.md5Encrypt32Upper((String) rawPassword);
            }

            // 判断密码是否正确, rawPassword 用户输入的密码,  encodedPassword 数据库DB的密码,当 XiJiaUserDetailsServiceImpl的loadUserByUsername方法执行完后执行
            @Override
            public boolean matches(CharSequence rawPassword, String encodedPassword) {
                //String rawPass = MD5Util.encode((String) rawPassword);
                boolean result = rawPassword.equals(encodedPassword);
                return result;
            }
        });
        return authenticationProvider;
    }

    @Bean
    public WebSecurityConfigurerAdapter webSecurityConfigurerAdapter() {
        return new WebSecurityConfigurerAdapter() {
            @Override
            protected void configure(HttpSecurity http) throws Exception {
                // 只拦截需要拦截的所有接口, 拦截数据库权限表中的所有接口
                List<AuthorityAdmin> authoritys = authorityAdminMapper.selectList(null);
                ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry eiur = http.authorizeRequests();
                authoritys.forEach((auth) -> {
                    eiur.antMatchers(auth.getUrl()).hasAnyAuthority(auth.getAuthority());
                });
                // 配置token验证及登录认证,过滤器
                eiur
                        // 登录接口不需要权限控制,可删除,目前该接口不在权限列表中
                        .antMatchers("/auth/login").permitAll()
                        // 设置JWT过滤器
                        .and()
                        .addFilter(new JWTValidFilter(authenticationManager(), resolver))
                        .addFilter(new JWTLoginFilter(authenticationManager(), resolver)).csrf().disable()
                        // 剔除session
                        .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

                // 开启跨域访问
                http.cors().disable();
                // 开启模拟请求,比如API POST测试工具的测试,不开启时,API POST为报403错误
                http.csrf().disable();
                // iframe 跳转错误处理 Refused to display 'url' in a frame because it set 'X-Frame-Options' to 'deny'
                http.headers().frameOptions().disable();
            }
        };
    }
}

5. 系统错误配置

WebServerAutoConfiguration.java

import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory;
import org.springframework.boot.web.server.ErrorPage;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;

/**
 * @className: WebServerAutoConfiguration
 * @description Servlet容器中定义错误页面跳转
 * @author GMF
*/
@Configuration
public class WebServerAutoConfiguration {
    @Bean
    public ConfigurableServletWebServerFactory webServerFactory() {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        ErrorPage errorPage400 = new ErrorPage(HttpStatus.BAD_REQUEST, "/error/400");
        ErrorPage errorPage401 = new ErrorPage(HttpStatus.UNAUTHORIZED, "/error/401");
        ErrorPage errorPage403 = new ErrorPage(HttpStatus.FORBIDDEN, "/error/403");
        ErrorPage errorPage404 = new ErrorPage(HttpStatus.NOT_FOUND, "/error/404");
        ErrorPage errorPage415 = new ErrorPage(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "/error/415");
        ErrorPage errorPage500 = new ErrorPage(HttpStatus.INTERNAL_SERVER_ERROR, "/error/500");
        factory.addErrorPages(errorPage400, errorPage401, errorPage403, errorPage404, errorPage415, errorPage500);
        return factory;
    }
}

ErrorController.java

import com.gmf.error.ErrorException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * TODO  WebServerAutoConfiguration 转发异常转发过来的信息,返回 json参数
 * 方式1、直接跳转到具体错误页面
 * 方式2、返回json格式数据,由前端处理
 */
@Slf4j
@Controller
@SuppressWarnings("all")
public class ErrorController {

    /**
     * TODO  方式2:系统错误返回json
     *
     * @param code 对应错误码,ErrorPageConfig配置
     * @return java.lang.String
     */
    @RequestMapping(value = "/error/{code}")
    //@ResponseBody
    public String error(@PathVariable int code) {
        log.error(code+"认证异常");
        //throw  new ErrorException(code,"认证异常");
        return "redirect:/index.html";
    }
}

6. 测试接口

import lombok.extern.slf4j.Slf4j;
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;


@Slf4j
@RestController
@RequestMapping("/auth")
public class AuthController {

    @PostMapping(value = "/login")
    public String login() {
        return "登陆成功";
    }

    @PostMapping("/admin")
    public String admin(){
        return "Authority ADMIN";
    }

    @PostMapping("/vip")
    public String vip(){
        return "Authority VIP";
    }

    @GetMapping("/test")
    public String test(){
        return "Authority Test";
    }
}

6. 测试结果

访问登陆接口,登陆成功并返回Token

image.png

测试携带Token访问权限接口

image.png

测试不懈怠Token访问接口

image.png
重定向至登陆页面