JWT 应用

JWT工具模块

如果要想在项目之中去使用JWT技术,那么就必须结合到已有的模块之中,最佳的做法就是将JWT的相关的处理
操作做为一个自动的starter组件进行接入

1、【microcloud项目】既然要开发一个starter组件,最佳的做法就是开发一个新的模块,模块名称:“yootk-starter.jwt ”

2、【microcloud 项目】需要为“yootk-starter-jwt”模块配置所需要的依赖库,这些依赖库包括

implementation group: 'org.springframework.boot', name: 'spring-boot-configuration-processor', version: '2.5.5'
compileOnly group: 'javax.servlet', name: 'javax.servlet-api', version: '4.0.1'
implementation group: 'commons-codec', name: 'commons-codec', version: '1.15'
implementation group: 'io.jsonwebtoken', name: 'jjwt', version: '0.9.1'
implementation group: 'javax.xml.bind', name: 'jaxb-api', version: '2.3.1'
implementation group: 'com.sun.xml.bind', name: 'jaxb-impl', version: '2.3.0'
implementation group: 'com.sun.xml.bind', name: 'jaxb-core', version: '2.3.0'

3、【microcloud项目】既然已经确定了所需要的项目依赖库,随后就可以修改“dependencies.gradle”配置文件,定义所依赖模块的配置。

ext.versions = [                // 定义全部的依赖库版本号
    servlet              : '4.0.1', // Servlet的依赖库
    commonsCodec         : '1.15', // codec依赖库
    jjwt                 : '0.9.1', // jwt依赖库
    jaxb                 : '2.3.0', // JAXB依赖库  JDK11需要加的
]
ext.libraries = [
    // 以下的配置为JWT的服务整合
    'servlet-api'                       : "javax.servlet:javax.servlet-api:${versions.servlet}",
    'commons-codec'                     : "commons-codec:commons-codec:${versions.commonsCodec}",
    'jjwt'                              : "io.jsonwebtoken:jjwt:${versions.jjwt}",
    'jaxb-api'                          : "javax.xml.bind:jaxb-api:${versions.jaxb}",
    'jaxb-impl'                         : "com.sun.xml.bind:jaxb-impl:${versions.jaxb}",
    'jaxb-core'                         : "com.sun.xml.bind:jaxb-core:${versions.jaxb}",
]

4、【microcloud项目】修改build.gradle配置文件,添加相关的依赖

project(":yootk-starter-jwt") { // JWT的实现组件
    dependencies {
        annotationProcessor('org.springframework.boot:spring-boot-configuration-processor')
        implementation(libraries.'servlet-api')
        implementation(libraries.'commons-codec')
        // 以下的组件会被其他的模块继续引用,所以必须将其的编译范围配置为compile
        compile(libraries.'jjwt')
        compile(libraries.'jaxb-api')
        compile(libraries.'jaxb-impl')
        compile(libraries.'jaxb-core')
    }
}

5、【yootk-starter-jwt子模块】由于该模块最终需要进行编译处理,所以此时要修改build.gradle配置文件,进行任务配置。

jar { enabled = true} // 允许打包为jar文件
bootJar { enabled = false } // 不允许打包为Boot执行文件
javadocJar { enabled = false } // 不需要打包为jar文件
javadocTask { enabled = false } // 不需要打包为doc文件

6、【yootk-starter-jwt子模块】为了便于用户的信息的相应,创建一个JWT响应代码枚举类。

package com.yootk.jwt.code;

import javax.servlet.http.HttpServletResponse;

public enum JWTResponseCode { // 定义为一个枚举类
    SUCCESS_CODE(HttpServletResponse.SC_OK, "Token数据正确,服务正常访问!"),
    TOKEN_TIMEOUT_CODE(HttpServletResponse.SC_BAD_REQUEST, "Token信息已经失效,需要重新申请!"),
    NO_AUTH_CODE(HttpServletResponse.SC_NOT_FOUND, "没有找到匹配的Token信息,无法进行服务访问!");
    private int code; // 响应的代码
    private String message; // 响应信息
    private JWTResponseCode(int code, String message) {
        this.code = code;
        this.message = message;
    }
    public String toString() {  // 直接将数据以JSON的形式返回
        return "{\"code\":" + this.code + ",\"message\":" + this.message + "}";
    }
}

7、 【yootk-starter-jwt】此时的yootk-starter-jwt模块最终是一个自动装配的组件,那么既然是组件就需要通过一个配置类来读取引用该模块时所添加的配置信息,那么创建一个JWTConfigProperties 配置类。

package com.yootk.jwt.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data // Lombok直接生成的所有代码
@ConfigurationProperties(prefix = "yootk.security.config.jwt") // 配置项的前缀
public class JWTConfigProperties { // JWT配置类
    private String sign; // 保存签名信息
    private String issuer; // 证书签发者
    private String secret; // 加密的密钥
    private long expire; // 失效时间
}

8、【yootk-starter-jwt子模块】创建ITokenService服务处理接口,专门实现JWT数据的相关处理。

package com.yootk.jwt.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtException;

import javax.crypto.SecretKey;
import java.util.Map;

public interface ITokenService { // 创建一个JWT的操作接口
    public SecretKey generalKey(); // 获取当前JWT数据的加密KEY
    // 创建Token的数据内容,同时要求保存用户的id以及所需要的附加数据
    public String createToken(String id, Map<String, Object> subject);
    public Jws<Claims> parseToken(String token) throws JwtException; // 解析Token数据
    public boolean verifyToken(String token); // 验证Token有效性
    public String refreshToken(String token); // 刷新Token内容
}

9.【yootk-starter-jwt子模块】创建TokenServicelmpl实现子类,很多的数据需要通过JSON实现传递。

package com.yootk.jwt.service.impl;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.jwt.config.JWTConfigProperties;
import com.yootk.jwt.service.ITokenService;
import io.jsonwebtoken.*;
import org.apache.commons.codec.binary.Base64;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
// 此时的组件中的代码需要被其他的模块去引用,所以未必会与扫描包相同
public class TokenServiceImpl implements ITokenService {
    
    @Autowired // SpringBoot容器启动时会自动提供Jackson 实例
    private ObjectMapper objectMapper; // Jackson的数据处理类对象
    
    @Autowired
    private JWTConfigProperties jwtConfigProperties; // 获取JWT的相关配置属性
    
    @Value("${spring.application.name}") // 通过SpEL进行配置注入
    private String applicationName; // 应用名称
    private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法
    
    @Override
    public SecretKey generalKey() {
        byte [] encodeKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));
        SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); // 获取加密KEY
        return key;
    }

    @Override
    public String createToken(String id, Map<String, Object> subject) {
        // 使用JWT数据结构进行开发,目的之一就是不需要进行JWT数据的分布式存储,所以所谓的缓存组件、数据库都用不到
        // 所有的Token都存在有保存时效的问题,所以就需要通过当前时间来进行计算
        Date nowDate = new Date(); // 获取当前的日期时间
        Date expireDate = new Date(nowDate.getTime() + this.jwtConfigProperties.getExpire() * 1000); // 证书过期时间
        Map<String, Object> cliams = new HashMap<>(); // 保存所有附加数据
        cliams.put("site", "www.yootk.com"); // 视频下载地址,顶部有一个下载资源
        cliams.put("msg", "世界上爆可爱的老师 —— 爆可爱的小李老师"); // 随便添加内容
        cliams.put("nice", "Good Good Good");
        Map<String, Object> headers = new HashMap<>(); // 保存头信息
        headers.put("author", "李兴华"); // 作者,也可以通过配置处理
        // 后续由于很多的模块都会引用此组件,所以为了后续的安全,最佳的做法就是设置一个模块名称的信息
        headers.put("module", this.applicationName);
        JwtBuilder builder = null;
        try {
            builder = Jwts.builder()    // 进行JWTBuilder对象实例化
                    .setClaims(cliams) // 保存附加的数据内容
                    .setHeader(headers) // 保存头信息
                    .setId(id)// 保存ID信息
                    .setIssuedAt(nowDate) // 签发时间
                    .setIssuer(this.jwtConfigProperties.getIssuer()) // 设置签发者
                    .setSubject(this.objectMapper.writeValueAsString(subject)) // 所要传递的数据转为JSON
                    .signWith(this.signatureAlgorithm, this.generalKey()) // 获取签名算法
                    .setExpiration(expireDate); // 配置失效时间
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return builder.compact(); // 创建Token
    }

    @Override
    public Jws<Claims> parseToken(String token) throws JwtException {
        if (this.verifyToken(token)) {  // 只有正确的时候再进行Token解析
            Jws<Claims> claims = Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token);
            return claims;
        }
        return null; // 解析失败返回null
    }

    @Override
    public boolean verifyToken(String token) {
        try {
            Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();
            return true; // 没有异常就返回true
        } catch (Exception e) {}
        return false;
    }

    @Override
    public String refreshToken(String token) {
        if (this.verifyToken(token)) {
            Jws<Claims> jws = this.parseToken(token); // 解析Token数据
            return this.createToken(jws.getBody().getId(), this.objectMapper.readValue(jws.getBody().getSubject(), Map.class));
        }
        return null;
    }
}

10、【yootk-starter-jwt子模块】定义一个加密的属性配置

package com.yootk.jwt.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

@Data
@ConfigurationProperties(prefix = "yootk.security.config.password.encrypt") // 配置前缀
public class EncryptConfigProperties { // 加密配置属性
    private Integer repeat; // 定义重复的次数
    private String salt; // 加密的盐值
}

11、【yootk-starter-jwt子模块】既然所有的用户的信息都要保存在数据表里面,那么就需要进行密码的加密处理。

package com.yootk.jwt.service;

public interface IEncryptService { // 密码加密
    public String getEncryptPassword(String password); // 得到一个加密后的密码
}

12、【yootk-starter-jwt子模块】定义具体的实现子类

package com.yootk.jwt.service.impl;

import com.yootk.jwt.config.EncryptConfigProperties;
import com.yootk.jwt.service.IEncryptService;
import org.springframework.beans.factory.annotation.Autowired;

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

public class EncryptServiceImpl implements IEncryptService {
    @Autowired
    private EncryptConfigProperties encryptConfigProperties; // 属性配置
    private static MessageDigest MD5_DIGEST; // MD5加密处理
    private static final Base64.Encoder BASE64_ENCODER = Base64.getEncoder(); // 加密器
    static {    // 初始化操作
        try {
            MD5_DIGEST = MessageDigest.getInstance("MD5");
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
    @Override
    public String getEncryptPassword(String password) {
        String saltPassword = "{" + this.encryptConfigProperties.getSalt() + "}" + password;
        for (int x = 0 ; x < this.encryptConfigProperties.getRepeat(); x ++) {
            saltPassword = BASE64_ENCODER.encodeToString(MD5_DIGEST.digest(saltPassword.getBytes()));
        }
        return saltPassword;
    }
}

13、【yootk-starter-jwt子模块】创建JWT自动配置类

package com.yootk.jwt.autoconfig;

import com.yootk.jwt.config.EncryptConfigProperties;
import com.yootk.jwt.config.JWTConfigProperties;
import com.yootk.jwt.service.IEncryptService;
import com.yootk.jwt.service.ITokenService;
import com.yootk.jwt.service.impl.EncryptServiceImpl;
import com.yootk.jwt.service.impl.TokenServiceImpl;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({JWTConfigProperties.class, EncryptConfigProperties.class}) // 配置注入属性
public class JWTAutoConfiguration {
    @Bean("tokenService")
    public ITokenService getTokenServiceBean() {
        return new TokenServiceImpl();
    }
    @Bean("encryptService")
    public IEncryptService getEncryptServiceBean() {
        return new EncryptServiceImpl();
    }
}

14、【yootk-starter-jwt子模块】在“src/main/resources”目录之中创建“META-INF/spring.factories”配置文件

org.springframework.boot.autoconfigure.EnableAutoConfiguration= com.yootk.jwt.autoconfig.JWTAutoConfiguration

15、【yootk-starter-jwt子模块】模块开发完成之后来进行编译: gradle build

16、【yootk-starter-jwt子模块】既然已经成功的实现了模块的编译处理,随后就需要进行一些环境上的测试,创建SpringBoot的配置文件: application.yml

yootk:
  security:
    config:
      jwt:
        sign: muyan
        issuer: Muyan
        secret: yootk
        expire: 100 # 单位:秒
      password:
        encrypt:
          repeat: 5
          salt: yootk

spring:
  application:
    name: JWT-TEST          

测试JWT工具模块

17、【yootk-starter-jwt子模块】创建一个程序启动的主类,主要是进行测试用的

package com.yootk.jwt;

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

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

18、【yootk-starter-jwt子模块】编写测试程序进行TokenService测试

package com.yootk.test;

import com.yootk.jwt.StartJWTConfiguration;
import com.yootk.jwt.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwsHeader;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@SpringBootTest(classes = StartJWTConfiguration.class) // 随便写的测试类
public class TestTokenService { // 代码测试
    @Autowired
    private ITokenService tokenService;
    private String jwt = "eyJhdXRob3IiOiLmnY7lhbTljY4iLCJtb2R1bGUiOiJKV1QtVEVTVCIsImFsZyI6IkhTMjU2In0.eyJtc2ciOiLkuJbnlYzkuIrniIblj6_niLHnmoTogIHluIgg4oCU4oCUIOeIhuWPr-eIseeahOWwj-adjuiAgeW4iCIsInN1YiI6IntcInJpZHNcIjpcIlVTRVI7QURNSU47REVQVDtFTVA7Uk9MRVwiLFwibmFtZVwiOlwi5rKQ6KiA56eR5oqAIOKAlOKAlCDmnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImlzcyI6Ik11eWFuWW9vdGsiLCJleHAiOjE2MzM2NzE3NjcsImlhdCI6MTYzMzU3MTc2NywibmljZSI6Ikdvb2QgR29vZCBHb29kIiwianRpIjoieW9vdGstMDgwNGI3NDQtNTBjZC00NjI2LTgzNmEtNjA1MmFiZWMyYzQ4In0.O71QGGPtWYwL7Tyhx8iOLQFAWc1DmVlAS4i0N99OJJk"; // 测试解析使用的
    @Test
    public void testCreateToken() {
        Map<String, Object> map = new HashMap<>(); // 测试生成
        map.put("mid", "muyan");
        map.put("name", "沐言科技 —— 李兴华");
        map.put("rids", "USER;ADMIN;DEPT;EMP;ROLE"); // 用户角色信息
        String id = "yootk-" + UUID.randomUUID(); // 随意生成一个JWT-ID数据
        System.out.println(this.tokenService.createToken(id, map));
    }
    @Test
    public void testParseToken() {  // 解析Token数据内容
        Jws<Claims> jws = this.tokenService.parseToken(jwt);
        System.out.println("JWT签名数据:" + jws.getSignature()); // 获取签名数据
        JwsHeader headers = jws.getHeader(); // 获取头信息
        headers.forEach((headerName, headerValue) -> {
            System.out.println("【JWT头信息】" + headerName + " = " + headerValue);
        });
        Claims claims = jws.getBody();
        claims.forEach((bodyName, bodyValue) -> {
            System.out.println("【JWT数据】" + bodyName + " = " + bodyValue);
        });
    } 
    @Test
    public void testVerifyJWT() {
        System.out.println("【JWT数据验证】" + this.tokenService.verifyToken(jwt));
    }
    @Test
    public void testRefreshJWT() {
        System.out.println("【JWT数据刷新】" + this.tokenService.refreshToken(jwt));
    }
}

19、【yootk-starter-jwt子模块】随后进行密码加密的测试

package com.yootk.test;

import com.yootk.jwt.StartJWTConfiguration;
import com.yootk.jwt.service.IEncryptService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@SpringBootTest(classes = StartJWTConfiguration.class) // 随便写的测试类
public class TestEncryptService {
    @Autowired
    private IEncryptService encryptService;
    @Test
    public void testCreatePassword() {
        System.out.println(this.encryptService.getEncryptPassword("hello"));
    }
}

此时已经成功的开发出了一套完整的与JWT相关的应用组件模块,使用的时候直接导入依赖库即可应用。

Token认证微服务

虽然此时已经给出了JWT相关的自动装配组件,但是这个组件最终如果要想应用起来还需要提供有一个具体的Token微服务,功能就是根据用户的认证的请求来实现相关Token数据的生成了。
在这里插入图片描述
由于当前的设计是 通过JWT存储了认证数据以及授权数据,在这样的过程里面就需要通过数据库来实现用户的统一管理,即:应该提供有用户表、授权表的信息。
在这里插入图片描述

1、【microcloud项目】创建“token-server-8201”子模块,随后修改build.gradle配置文件,为其添加相关的依赖

project(":token-server-8201") {    // 部门微服务
    dependencies {
        implementation(project(":common-api")) // 导入公共的子模块
        implementation(project(":yootk-starter-jwt")) // 导入JWT子模块
        implementation(libraries.'mybatis-plus-boot-starter')
        implementation(libraries.'mysql-connector-java')
        implementation(libraries.'druid')
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel')
        // 以下的依赖库为Nacos注册中心所需要的依赖配置
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation(libraries.'nacos-client') // 引入与当前的Nacos匹配的依赖库
    }
}

2、【token-server-8201子模块】根据当前的需要创建数据表

DROP DATABASE IF EXISTS token8201;
CREATE DATABASE token8201 CHARACTER SET UTF8 ;
USE token8201 ;
CREATE TABLE member(
   mid			VARCHAR(50) NOT NULL,
   name			VARCHAR(30),
   password		VARCHAR(32),
   locked		INT,
   dbname		VARCHAR(50),
   CONSTRAINT pk_mid PRIMARY KEY (mid)
) engine='innodb';
CREATE TABLE role(
   rid			VARCHAR(50) ,
   title			VARCHAR(200) ,
   dbname		VARCHAR(50),
   CONSTRAINT pk_rid PRIMARY KEY(rid)
) engine='innodb' ;
CREATE TABLE action(
   actid			VARCHAR(50) ,
   title			VARCHAR(200) ,
   rid			VARCHAR(50) ,
   dbname		VARCHAR(50),
   CONSTRAINT pk_actid PRIMARY KEY(actid)
) engine='innodb' ;
CREATE TABLE member_role(
   mid			VARCHAR(50) ,
   rid			VARCHAR(50) ,
   dbname		VARCHAR(50)
) engine='innodb' ;
-- 1表示活跃、0表示锁定,用户密码铭文:hello
INSERT INTO member(mid, name, password, locked, dbname) VALUES
	 ('admin', '管理员', 'Wx7vJ71XD3TgJg5uiETnKA==', 0, database()) ;
INSERT INTO member(mid, name, password, locked, dbname) VALUES
	 ('yootk', '用户', 'Wx7vJ71XD3TgJg5uiETnKA==', 0, database()) ;
INSERT INTO member(mid, name, password, locked, dbname) VALUES
	 ('mermaid', '美人鱼', 'Wx7vJ71XD3TgJg5uiETnKA==', 1, database()) ;
-- 定义角色信息
INSERT INTO role(rid, title, dbname) VALUES ('member', '用户管理', database()) ;
INSERT INTO role(rid, title, dbname) VALUES ('dept', '部门管理', database()) ;
INSERT INTO role(rid, title, dbname) VALUES ('emp', '雇员管理', database()) ;
-- 定义权限信息
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('member:add', '创建用户', 'member', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('member:edit', '编辑用户', 'member', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('member:delete', '删除用户', 'member', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('member:list', '用户列表', 'member', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('dept:add', '创建部门', 'dept', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('dept:edit', '编辑部门', 'dept', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('dept:delete', '删除部门', 'dept', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('dept:list', '部门列表', 'dept', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('emp:add', '增加雇员', 'emp', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('emp:edit', '编辑雇员', 'emp', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('emp:delete', '删除雇员', 'emp', database()) ;
INSERT INTO action(actid, title, rid, dbname) VALUES 
	 ('emp:list', '雇员列表', 'emp', database()) ;
-- 定义用户与角色的关系
INSERT INTO member_role(mid, rid, dbname) VALUES ('admin', 'member', database()) ;
INSERT INTO member_role(mid, rid, dbname) VALUES ('admin', 'dept', database()) ;
INSERT INTO member_role(mid, rid, dbname) VALUES ('admin', 'emp', database()) ;
INSERT INTO member_role(mid, rid, dbname) VALUES ('yootk', 'emp', database()) ;
INSERT INTO member_role(mid, rid, dbname) VALUES ('mermaid', 'dept', database()) ;
COMMIT ;

3、【token-server-8201子模块】在项目之中application.yml文件

server: # 服务端配置
  port: 8201 # 8201端口
mybatis-plus: # MyBatisPlus配置
  type-aliases-package: com.yootk.provider.vo  # 别名配置
spring:
  application: # 配置应用信息
    name: token.provider # 是微服务的名称
  cloud: # Cloud配置
    sentinel: # 监控配置
      transport: # 传输配置
        port: 8719 # Sentinel组件启用之后默认会启动一个8719端口
        dashboard: sentinel-server:8888 # 控制台地址
    nacos: # Nacos注册中心配置
      discovery: # 发现服务
        weight: 80
        service: ${spring.application.name} # 使用微服务的名称作为注册的服务名称
        server-addr: nacos-server:8848 # Nacos服务地址
        namespace: 96c23d77-8d08-4648-b750-1217845607ee # 命名空间ID
        group: MICROCLOUD_GROUP # 一般建议大写
        cluster-name: MuyanCluster # 配置集群名称
        metadata:  # 根据自身的需要配置元数据
          version: 1.0 # 自定义元数据项
  datasource: # 数据源配置
    type: com.alibaba.druid.pool.DruidDataSource    # 数据源类型
    driver-class-name: com.mysql.cj.jdbc.Driver     # 驱动程序类
    url: jdbc:mysql://localhost:3306/token8201          # 连接地址
    username: root                                  # 用户名
    password: mysqladmin                            # 连接密码
    druid: # druid相关配置
      initial-size: 5                               # 初始化连接池大小
      min-idle: 10                                  # 最小维持连接池大小
      max-active: 50                                # 最大支持连接池大小
      max-wait: 60000                               # 最大等待时间
      time-between-eviction-runs-millis: 60000      # 关闭空闲连接间隔
      min-evictable-idle-time-millis: 30000         # 连接最小生存时间
      validation-query: SELECT 1 FROM dual          # 状态检测
      test-while-idle: true # 空闲时检测连接是否有效
      test-on-borrow: false # 申请时检测连接是否有效
      test-on-return: false # 归还时检测连接是否有效
      pool-prepared-statements: false # PSCache缓存
      max-pool-prepared-statement-per-connection-size: 20 # 配置PS缓存
      filters: stat, wall, slf4j # 开启过滤
      stat-view-servlet: # 监控界面配置
        enabled: true # 启用druid监控界面
        allow: 127.0.0.1      # 访问白名单
        login-username: muyan # 用户名
        login-password: yootk # 密码
        reset-enable: true # 允许重置
        url-pattern: /druid/* # 访问路径
      web-stat-filter:
        enabled: true # 启动URI监控
        url-pattern: /* # 跟踪全部服务
        exclusions: "*.js,*.gif,*.jpg,*.bmp,*.png,*.css,*.ico,/druid/*" # 跟踪排除
      filter:
        slf4j: # 日志
          enabled: true # 启用SLF4j监控
          data-source-log-enabled: true # 启用数据库日志
          statement-executable-sql-log-enable: true # 执行日志
          result-set-log-enabled: true # ResultSet日志启用
        stat: # SQL监控
          merge-sql: true # 合并统计
          log-slow-sql: true # 慢执行记录
          slow-sql-millis: 1 # 慢SQL执行时间
        wall: # SQL防火墙
          enabled: true   # SQL防火墙
          config: # 防火墙规则
            multi-statement-allow: true # 允许执行批量SQL
            delete-allow: false # 禁止执行删除语句
      aop-patterns: "com.yootk.provider.action.*,com.yootk.provider.service.*,com.yootk.provider.dao.*" # Spring监控

4、【token-server-8201子模块】除了以上的操作部分之外,那么剩下的就需要开发者自己去定义与WT有关的配置项。

yootk:
  security:
    config:
      jwt:
        sign: muyan
        issuer: MuyanYootk
        secret: www.yootk.com
        expire: 100000 # 单位:秒
      password:
        encrypt:
          repeat: 5
          salt: www.yootk.com

5、【token-server-8201子模块】创建Member表的映射转换

package com.yootk.provider.vo;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("member") // 映射表名称
public class Member {
    @TableId // 主键字段
    private String mid;
    private String name;
    private String password;
    private Integer locked;
    private String dbname;
}

6、【token-server-8201子模块】配置Role角色处理类

package com.yootk.provider.vo;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
@Data
@TableName("role")
public class Role {
    @TableId
    private String rid;
    private String title;
    private String dbname;
}

7、【token-server-8201子模块】定义权限表映射类

package com.yootk.provider.vo;

import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;

@Data
@TableName("action")
public class Action {
    @TableId
    private String actid;
    private String title;
    private String rid;
    private String dbname;
}

8、【token-server-8201子模块】创建IMemberDAO接口

package com.yootk.provider.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yootk.provider.vo.Member;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface IMemberDAO extends BaseMapper<Member> {
}

9、【token-server-8201子模块】创建IRoleDAO接口

package com.yootk.provider.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yootk.provider.vo.Role;
import org.apache.ibatis.annotations.Mapper;

import java.util.Set;

@Mapper
public interface IRoleDAO extends BaseMapper<Role> {
    public Set<String> findAllByMember(String mid); // 根据用户名查询角色
}

10、【token-server-8201子模块】创建IActionDAO接口

package com.yootk.provider.dao;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.yootk.provider.vo.Action;
import org.apache.ibatis.annotations.Mapper;

import java.util.Set;

@Mapper
public interface IActionDAO extends BaseMapper<Action> {
    public Set<String> findAllByMember(String mid); // 获取权限信息
}

11、【token-server-8201子模块】定义“src/main/resources/META-INF/mybatis/mapper/MemberMapper.xml”文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yootk.provider.dao.IMemberDAO">
</mapper>

12、【token-server-8201子模块】创建RoleMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yootk.provider.dao.IRoleDAO">
    <select id="findAllByMember" parameterType="string" resultType="string">
      SELECT rid FROM member_role WHERE mid=#{mid}
  </select>
</mapper>

13、【token-server-8201子模块】创建ActionMapper.xml文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.yootk.provider.dao.IActionDAO">
    <select id="findAllByMember" parameterType="string" resultType="string">
        SELECT actid FROM action WHERE rid IN(
          SELECT rid FROM member_role WHERE mid=#{mid})
    </select>
</mapper>

14、【common-api子模块】因为此时需要牵扯到用户认证信息的传输,所以这个时候需要创建一个DTO传输类

package com.yootk.common.dto;

import lombok.Data;

@Data
public class MemberDTO {
    private String mid;
    private String password;
}

15、【common-api子模块】创建IMemberService业务接口实现认证与授权信息获取

package com.yootk.service;

import com.yootk.common.dto.MemberDTO;

import java.util.Map;

public interface IMemberService {
    // 用户登录完成之后所有的数据通过Map集合进行返回,而后会包含有如下的一些数据内容:
    // 1、key = status、value = 登录状态(true、false);
    // 2、key = mid、value = 用户名;
    // 3、key = name、value = 姓名;
    // 4、key = resource、value = 授权信息
    // 4-1、key = roles、value = 用户拥有的全部角色
    // 4-2、key = roles、value = 用户拥有的全部的权限
    public Map<String, Object> login(MemberDTO memberDTO);
}

16、【token-server-8201子模块】定义IMemberService 业务接口的实现类

package com.yootk.provider.service.impl;

import com.yootk.common.dto.MemberDTO;
import com.yootk.provider.dao.IActionDAO;
import com.yootk.provider.dao.IMemberDAO;
import com.yootk.provider.dao.IRoleDAO;
import com.yootk.provider.vo.Member;
import com.yootk.service.IMemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.HashMap;
import java.util.Map;

@Service
public class MemberServiceImpl implements IMemberService {
    @Autowired
    private IMemberDAO memberDAO;
    @Autowired
    private IRoleDAO roleDAO;
    @Autowired
    private IActionDAO actionDAO;
    @Override
    public Map<String, Object> login(MemberDTO memberDTO) {
        Map<String, Object> result = new HashMap<>();
        Member member = this.memberDAO.selectById(memberDTO.getMid()); // 查询用户数据
        // 用户信息为空、密码不相等或者用户状态被锁定
        if (member == null || !member.getPassword().equals(memberDTO.getPassword()) || member.getLocked().equals(1)) {
            result.put("status", false); // 登录失败
        } else {    // 一切正常,获取其他信息
            result.put("status", true); // 登录成功
            result.put("mid", memberDTO.getMid());
            result.put("name", member.getName());
            Map<String, Object> resource = new HashMap<>();
            resource.put("roles", this.roleDAO.findAllByMember(memberDTO.getMid()));
            resource.put("actions", this.actionDAO.findAllByMember(memberDTO.getMid()));
            result.put("resource", resource);
        }
        return result;
    }
}

17、【token-server-8201子模块】编写一个程序启动类

package com.yootk.provider;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;

@SpringBootApplication
@EnableDiscoveryClient
public class StartTokenApplication8201 {
    public static void main(String[] args) {
        SpringApplication.run(StartTokenApplication8201.class, args);
    }
}

18、【token-server-8201子模块】编写测试类

package com.yootk.test;

import com.yootk.common.dto.MemberDTO;
import com.yootk.jwt.StartJWTConfiguration;
import com.yootk.jwt.service.IEncryptService;
import com.yootk.provider.StartTokenApplication8201;
import com.yootk.service.IMemberService;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;

@ExtendWith(SpringExtension.class)
@WebAppConfiguration
@SpringBootTest(classes = StartTokenApplication8201.class) // 随便写的测试类
public class TestMemberService {
    @Autowired
    private IMemberService memberService;
    @Autowired
    private IEncryptService encryptService; // 自动装配模块提供的
    @Test
    public void testLogin() {
        MemberDTO memberDTO = new MemberDTO();
        memberDTO.setMid("admin");
        memberDTO.setPassword(this.encryptService.getEncryptPassword("hello"));
        System.out.println(this.memberService.login(memberDTO));
    }
}

19、【token-server-8201子模块】Service接口测试通过之后下面就需要进行Action接口发布

package com.yootk.provider.action;

import com.yootk.common.dto.MemberDTO;
import com.yootk.jwt.service.IEncryptService;
import com.yootk.jwt.service.ITokenService;
import com.yootk.service.IMemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Map;

@RestController
@RequestMapping("/token/*")
public class TokenAction {
    @Autowired
    private IMemberService memberService; // 本模块提供的
    @Autowired
    private IEncryptService encryptService; // yootk-starter-jwt模块提供的
    @Autowired
    private ITokenService tokenService; // yootk-starter-jwt模块提供的
    @RequestMapping("create")
    public Object login(@RequestBody MemberDTO memberDTO) {
        // 对用户传入的密码信息进行加密处理
        memberDTO.setPassword(this.encryptService.getEncryptPassword(memberDTO.getPassword()));
        Map<String, Object> result = this.memberService.login(memberDTO); // 登录业务处理
        if (((Boolean)result.get("status"))) {  // 登录状态
            return this.tokenService.createToken(result.get("mid").toString(), (Map<String, Object>) result.get("resource"));
        }
        return null;
    }
    @RequestMapping("parse")
    public Object parseToken(String token) {
        return this.tokenService.parseToken(token); // Token解析处理
    }
}

20、【Nacos 控制台】在Nacos 控制台为Token 服务添加配置项
在这里插入图片描述
在这里插入图片描述
22、【Postman测试】测试Token生成

token-server-8201:8201/token/create?mid=admin&password=hello

在这里插入图片描述

23、【Postman测试】测试Token解析

token-server-8201:8201/token/parse?token=eyJhdXRob3IiOiLmnY7lhbTljY4iLCJtb2R1bGUiOiJ0b2tlbi5wcm92aWRlciIsImFsZyI6IkhTMjU2In0.eyJtc2ciOiLkuJbnlYzkuIrniIblj6_niLHnmoTogIHluIgg4oCU4oCUIOeIhuWPr-eIseeahOWwj-adjuiAgeW4iCIsInN1YiI6IntcInJvbGVzXCI6W1wibWVtYmVyXCIsXCJlbXBcIixcImRlcHRcIl0sXCJhY3Rpb25zXCI6W1wiZW1wOmxpc3RcIixcImRlcHQ6ZWRpdFwiLFwiZGVwdDpsaXN0XCIsXCJlbXA6ZWRpdFwiLFwibWVtYmVyOmFkZFwiLFwiZGVwdDphZGRcIixcImVtcDphZGRcIixcIm1lbWJlcjplZGl0XCIsXCJkZXB0OmRlbGV0ZVwiLFwibWVtYmVyOmRlbGV0ZVwiLFwibWVtYmVyOmxpc3RcIixcImVtcDpkZWxldGVcIl19Iiwic2l0ZSI6Ind3dy55b290ay5jb20iLCJpc3MiOiJNdXlhbllvb3RrIiwiZXhwIjoxNjMzNjc2MjIwLCJpYXQiOjE2MzM1NzYyMjAsIm5pY2UiOiJHb29kIEdvb2QgR29vZCIsImp0aSI6ImFkbWluIn0.3HA8dqdgi9Lr0Nlzg76CoJiiFcDwK-Vh9nf5facEfRQ

在这里插入图片描述

JWT授权监测

JWT本身不具备有这种所谓的授权检测支持,只是我们利用其附加数据的能力实现了这样的授权检测,在之前登录成功后就可以通过附加数据保存所有的授权的信息(分为了角色以及权限两种)。
在这里插入图片描述

后面肯定是由消费端来获取JWT数据,并且依据JWT数据实现微服务资源的调用,那么在这样的情况下,就可以设置有一个自定义的Annotation(注解),而后在注解里面可以根据需要进行角色或权限的检查,注解如果要想在微服务端生效,则可以利用拦截器的形式来进行处理。

1、【yootk-starter-jwt子模块】所有与JWT有关的操作实际上都由该模块提供,这样在代码之中就可以考虑将角色和权限检查的部分交给该模块来实现,定义一个专属的工具类。

package com.yootk.jwt.util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yootk.jwt.service.ITokenService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import org.springframework.beans.factory.annotation.Autowired;

import javax.servlet.http.HttpServletRequest;
import java.util.*;

// 所有的数据最终都是通过JSON的形式设置在JWT附加数据之中的
public class JWTMemberDataService { // 自定义的数据的解析类
    @Autowired
    private ITokenService tokenService;
    @Autowired
    private ObjectMapper objectMapper; // 解析JSON数据为Map集合

    public Map<String, String> headers(String token) {  // 通过JWT解析所有的头信息
        Jws<Claims> claimsJws = this.tokenService.parseToken(token);
        Map<String, String> headers = new HashMap<>(); // 保存所有的头信息的集合
        claimsJws.getHeader().forEach((key, value) -> { // 将JWT头信息转为Map
            headers.put(key.toString(), value.toString()); // 数据以String的方式存储
        });
        return headers;
    }
    public Set<String> roles(String token) {    // 解析全部的角色数据
        Jws<Claims> claimsJws = this.tokenService.parseToken(token);
        try {
            Map<String, List<String>> map = this.objectMapper.readValue(claimsJws.getBody().getSubject(), Map.class);
            Set<String> roles = new HashSet<>();
            roles.addAll(map.get("roles")); // 将获取的全部角色保存在Set集合
            return roles;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
    public Set<String> actions(String token) {    // 解析全部的权限数据
        Jws<Claims> claimsJws = this.tokenService.parseToken(token);
        try {
            Map<String, List<String>> map = this.objectMapper.readValue(claimsJws.getBody().getSubject(), Map.class);
            Set<String> actions = new HashSet<>();
            actions.addAll(map.get("actions")); // 将获取的全部角色保存在Set集合
            return actions;
        } catch (JsonProcessingException e) {
            e.printStackTrace();
        }
        return null;
    }
    public String id(String token) {
        Jws<Claims> claimsJws = this.tokenService.parseToken(token);
        return claimsJws.getBody().getId();
    }
    public String getToken(HttpServletRequest request, String name) {  // Token获取
        String token = request.getParameter(name); // name为参数的名称
        if (token == null || "".equals(token)) {    // 无法通过参数获取数据
            token = request.getHeader(name); // 通过头信息传递
        }
        return token;
    }
}

2、【yootk-starter-jwt子模块】既然最终该工具类要交由外部进行调用,所以在自动装配类里面追加一些配置

package com.yootk.jwt.autoconfig;

import com.yootk.jwt.config.EncryptConfigProperties;
import com.yootk.jwt.config.JWTConfigProperties;
import com.yootk.jwt.service.IEncryptService;
import com.yootk.jwt.service.ITokenService;
import com.yootk.jwt.service.impl.EncryptServiceImpl;
import com.yootk.jwt.service.impl.TokenServiceImpl;
import com.yootk.jwt.util.JWTMemberDataService;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties({JWTConfigProperties.class, EncryptConfigProperties.class}) // 配置注入属性
public class JWTAutoConfiguration {
    @Bean("tokenService")
    public ITokenService getTokenServiceBean() {
        return new TokenServiceImpl();
    }
    @Bean("encryptService")
    public IEncryptService getEncryptServiceBean() {
        return new EncryptServiceImpl();
    }
    @Bean("memberDataService")
    public JWTMemberDataService getMemberDataService() {
        return new JWTMemberDataService();
    }
}

3、【yootk-starter-jwt子模块】为项目添加一个注解,这个注解主要是根据JWT的数据来实现授权检测

package com.yootk.jwt.annotation;

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

@Target({ElementType.METHOD}) // 在方法上使用
@Retention(RetentionPolicy.RUNTIME) // 运行时生效
public @interface JWTCheckToken {
    boolean required() default true; // 配置的启用,认证排查
    String role() default ""; // 角色检查
    String action() default ""; // 权限检查
}

4、【yootk-starter-jwt子模块】对当前的模块进行重新打包: gradle build

打包之前 yootk-starter-jwt子模块的build.gradle
在这里插入图片描述
5、【provider-dept-8002子模块】在部门微服务之中引入jwt的组件模块

project(":provider-dept-8002") {    // 部门微服务
    dependencies {
        implementation(project(":common-api")) // 导入公共的子模块
        implementation(project(":yootk-starter-jwt")) // 导入JWT子模块
        implementation(libraries.'mybatis-plus-boot-starter')
        implementation(libraries.'mysql-connector-java')
        implementation(libraries.'druid')
        implementation(libraries.'springfox-boot-starter')
        implementation('org.springframework.boot:spring-boot-starter-security')
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-sentinel')
        // 以下的依赖库为Nacos注册中心所需要的依赖配置
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-config') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation(libraries.'nacos-client') // 引入与当前的Nacos匹配的依赖库
    }
}

6、【provider-dept-8002子模块】修改DeptAction程序类,添加JWT的注解

package com.yootk.provider.action;

import com.yootk.common.dto.DeptDTO;
import com.yootk.jwt.annotation.JWTCheckToken;
import com.yootk.service.IDeptService;
import io.swagger.annotations.ApiImplicitParam;
import io.swagger.annotations.ApiImplicitParams;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import java.util.Enumeration;

@RestController
@RequestMapping("/provider/dept/*") // 微服务提供者父路径
@Slf4j // 使用一个注解
public class DeptAction {
    
    @Autowired
    private IDeptService deptService;
   
    @ApiOperation(value="部门查询", notes = "根据部门编号查询部门详细信息")
    @GetMapping("get/{id}")
    @JWTCheckToken(role="dept") // 进行JWT的角色检查
    public Object get(@PathVariable("id") long id) {
        this.printRequestHeaders("get");
        return this.deptService.get(id);
    }
    
    @ApiOperation(value="部门增加", notes = "增加新的部门信息")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "deptDTO", required = true,
                    dataType = "DeptDTO", value = "部门传输对象实例")
    })
    @PostMapping("add")
    public Object add(@RequestBody  DeptDTO deptDTO) {    // 后面会修改参数模式为JSON
        this.printRequestHeaders("add");
        return this.deptService.add(deptDTO);
    }
    
    @ApiOperation(value="部门列表", notes = "查询部门的完整信息")
    @GetMapping("list")
    @JWTCheckToken(action = "dept:list") // 权限检查
    public Object list() {
        this.printRequestHeaders("list");
        return this.deptService.list();
    }
    
    @ApiOperation(value="部门分页查询", notes = "根据指定的数据库参数实现部门数据的分页加载")
    @ApiImplicitParams({
            @ApiImplicitParam(name="cp", value = "当前所在页", required = true, dataType = "int"),
            @ApiImplicitParam(name="ls", value = "每页显示的数据行数", required = true, dataType = "int"),
            @ApiImplicitParam(name="col", value = "模糊查询列", required = true, dataType = "String"),
            @ApiImplicitParam(name="kw", value = "模糊查询关键字", required = true, dataType = "String")
    })
    @GetMapping("split")
    @JWTCheckToken // 只要追加了此注解就表示要进行JWT有效性检查
    public Object split(int cp, int ls, String col, String kw) {
        this.printRequestHeaders("split");
        return this.deptService.split(cp, ls, col, kw);
    }
    
    @GetMapping("message")
    public Object message(String message) { // 接收参数
        log.info("接收到请求参数,message = {}", message);
        printRequestHeaders("message");
        return message;
    }
    
    private void printRequestHeaders(String restName) {    // 实现所有请求头信息的输出
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        Enumeration<String> headerEnums = request.getHeaderNames();
        while (headerEnums.hasMoreElements()) {
            String headerName = headerEnums.nextElement();
            log.info("【{}】头信息:{} = {}", restName, headerName, request.getHeader(headerName));
        }
    }
}

7、【provider-dept-8002子模块】如果要想让当前的注解生效,则一定要开发一个专属的JWT的拦截器

package com.yootk.provider.interceptor;

import com.yootk.jwt.annotation.JWTCheckToken;
import com.yootk.jwt.code.JWTResponseCode;
import com.yootk.jwt.service.ITokenService;
import com.yootk.jwt.util.JWTMemberDataService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.util.StringUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;

public class JWTAuthorizeInterceptor implements HandlerInterceptor {
    
    // 此时需要确定有一个Token数据接收的参数名称,这个Token可能通过地址重写传递,或者是利用头信息传递
    private static final String TOKEN_NAME = "yootk-token";
    
    @Autowired // 区分出角色和权限的信息
    private JWTMemberDataService memberDataService;
    
    @Autowired // JWT有效性的检查
    private ITokenService tokenService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        boolean flag = true; // 拦截
        if (!(handler instanceof HandlerMethod)) {  // 类型不匹配
           return flag;
        }
        HandlerMethod handlerMethod = (HandlerMethod) handler; // 因为需要对Action进行解析处理
        Method method = handlerMethod.getMethod(); // 获取调用的方法对象
        if (method.isAnnotationPresent(JWTCheckToken.class)) {  // 当前的方法上存在有指定注解
            // 如果发现此时出现了Token的错误则肯定要直接进行响应,不会走到Action响应上
            response.setCharacterEncoding("UTF-8");
            response.setContentType("application/json;charset=utf-8");
            JWTCheckToken checkToken = method.getAnnotation(JWTCheckToken.class); // 获取配置注解
            if (checkToken.required()) { // 启用JWT检查
                // JWT的数据可能来自于参数或者是头信息
                String token = this.memberDataService.getToken(request, TOKEN_NAME);
                if (!StringUtils.hasLength(token)) {    // 没有Token数据
                    flag = false;
                    response.getWriter().println(JWTResponseCode.NO_AUTH_CODE); // 直接响应错误代码
                } else {    // 此时的Token存在
                    if (!this.tokenService.verifyToken(token)) {    // Token校验失败
                        flag = false;
                        response.getWriter().println(JWTResponseCode.TOKEN_TIMEOUT_CODE);
                    } else {    // Token没有失败
                        if (!(checkToken.role() == null || "".equals(checkToken.role()))) { // 需要进行角色检查
                            // 根据Token字符串解析出所有的角色集合,而后判断是否存在有指定的角色信息
                            if (this.memberDataService.roles(token).contains(checkToken.role())) {
                                flag = true; // 允许访问
                            } else { // 失败访问
                                response.getWriter().println(JWTResponseCode.NO_AUTH_CODE);
                                flag = false; // 不允许访问
                            }
                        } else if (!(checkToken.action() == null || "".equals(checkToken.action()))) {
                            if (this.memberDataService.actions(token).contains(checkToken.action())) {
                                flag = true; // 允许访问
                            } else { // 失败访问
                                response.getWriter().println(JWTResponseCode.NO_AUTH_CODE);
                                flag = false; // 不允许访问
                            }
                        } else {
                            flag = true;
                        }
                    }
                }
            }
        }
        return flag;
    }
}

8、【provider-dept-8002子模块】拦截器开发完成之后需要进行拦截器的配置类的定义

package com.yootk.provider.config;

import com.yootk.provider.interceptor.JWTAuthorizeInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class JWTInterceptorConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(this.getDefaultHandlerInterceptor()).addPathPatterns("/**");
    }

    @Bean
    public HandlerInterceptor getDefaultHandlerInterceptor() {
        return new JWTAuthorizeInterceptor();
    }
}

9、【provider-dept-8002子模块】如果要想正确的驱动“yootk-starter-jwt”组件,那么就需要在application.yml里面添加与JWT组件模块有关的配置项

yootk:
  security:
    config:
      jwt:
        sign: muyan
        issuer: MuyanYootk
        secret: www.yootk.com
        expire: 100000 # 单位:秒
      password:
        encrypt:
          repeat: 5
          salt: www.yootk.com

10、【Postman工具】现在全部的代码已经改造完成之后下面直接启动Postman进行测试
在这里插入图片描述
在这里插入图片描述
只有传递了正确的Token数据,才可以实现相应的微服务的访问,此时就可以通过授权的检测形式来保护你微服务的安全。

网关认证过滤

所有的微服务最终都是通过网关来实现分配的,那么网关之中就首先必须对消费端的调用进行JWT认证检查,网关除了具备有服务分类治理功能之外,实际上还有认证的检查功能,它是一个入口。

在很早以前的SpringCloud还是使用OAuth2做单点登录统一管理的时候,网关也需要进行一系列的开发配置,而后随着版本的更新,OAuth2的支持度开始下降,于是各种新版本的整合就非常痛苦

在这里插入图片描述

如果要想实现这种JWT的检查机制,那么只能够在过滤器之中完成处理,而在过滤器检查的时候需要注意配置的问题,例如:JWT参数的名称、非检查路径(例如:“/token/create")。

1、【gateway-9501子模块】首先要追加上JWT模块有关的依赖库,修改application.yml配置文件,随后还需要添加网关的一些自定义的配置属性。

yootk:
  security:
    config:
      jwt:
        sign: muyan
        issuer: MuyanYootk
        secret: www.yootk.com
        expire: 100000 # 单位:秒
      password:
        encrypt:
          repeat: 5
          salt: www.yootk.com
gateway: # 自定义的配置项
  config:
    jwt:
      header-name: yootk-token # 头信息的参数名称
      skip-auth-urls: # 跳过的检查路径
        - /token/create

2、【gateway-9501子模块】定义一个与当前配置相关的程序类,实现配置项的读取

package com.yootk.gateway.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import java.util.List;

@Component
@Data
@ConfigurationProperties(prefix = "gateway.config.jwt") // 定义配置头
public class GatewayJWTConfigProperties { // 网关的配置项
    private List<String> skipAuthUrls; // 配置的跳过路径
    private String headerName; // 头信息名称
}

3、【microcloud 项目】修改build.gradle,为gateway-9501子模块添加依赖

project(":gateway-9501") {  // 网关模块
    dependencies {
        implementation('com.alibaba.cloud:spring-cloud-starter-alibaba-nacos-discovery') {
            exclude group: 'com.alibaba.nacos', module: 'nacos-client' // 移除旧版本的Nacos依赖
        }
        implementation(project(":yootk-starter-jwt")) // 导入JWT子模块
        implementation(libraries.'nacos-client') // 引入与当前的Nacos匹配的依赖库
        implementation('org.springframework.cloud:spring-cloud-starter-gateway') // 网关依赖
        implementation('org.springframework.boot:spring-boot-starter-actuator') // Actuator依赖库
        implementation('org.springframework.cloud:spring-cloud-starter-loadbalancer')
        implementation(libraries.'caffeine')
        implementation(libraries.'micrometer-registry-prometheus')
        implementation(libraries.'micrometer-core')
    }
}

4、【gateway-9501子模块】定义全局过滤器

package com.yootk.gateway.filter.global;

import com.alibaba.nacos.api.utils.StringUtils;
import com.yootk.gateway.config.GatewayJWTConfigProperties;
import com.yootk.jwt.code.JWTResponseCode;
import com.yootk.jwt.service.ITokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;

import java.nio.charset.StandardCharsets;

@Component
@Slf4j
public class JWTTokenCheckFilter implements GlobalFilter {  // 全局过滤器
    
    @Autowired
    private GatewayJWTConfigProperties jwtConfig; // JWT的相关配置属性
    
    @Autowired
    private ITokenService tokenService; // 进行Token处理
    
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        String url = exchange.getRequest().getURI().getPath(); // 获取路径
        if (this.jwtConfig.getSkipAuthUrls() != null && this.jwtConfig.getSkipAuthUrls().contains(url)) {
            return chain.filter(exchange); // 向下继续执行其他的后续操作
        }
        // 网关将通过头信息获取到JWT的数据内容,网关技术通过WebFlux技术开发的
        String token = exchange.getRequest().getHeaders().get(this.jwtConfig.getHeaderName()).get(0);
        log.info("网关Token检查,Token = {}", token); // 日志输出
        // 如果假设Token有错误了,那么网关是需要直接进行响应的,请求肯定不会发送给目标的微服务
        ServerHttpResponse response = exchange.getResponse();
        if (StringUtils.isBlank(token)) { // Token数据为空
            DataBuffer buffer = response.bufferFactory().wrap(JWTResponseCode.NO_AUTH_CODE.toString().getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Flux.just(buffer)); // 异步响应错误
        } else {    // Token数据不为空
            if (this.tokenService.verifyToken(token)) { // 校验成功
                return chain.filter(exchange);
            } else {
                DataBuffer buffer = response.bufferFactory().wrap(JWTResponseCode.TOKEN_TIMEOUT_CODE.toString().getBytes(StandardCharsets.UTF_8));
                return response.writeWith(Flux.just(buffer)); // 异步响应错误
            }
        }
    }
}

5、【Nacos控制台】所有的网关现在都是通过Nacos保存的配置项,这样就需要在Nacos之中增加Token访问的路由地址

[
  {
    "id": "dept",
    "uri": "lb://dept.provider",
    "order": 1,
    "predicates": [
      {
        "name": "Path",
        "args": {
          "pattern": "/provider/dept/**"
        }
      }
    ],
    "filters": [
      {
        "name": "AddRequestHeader",
        "args": {
          "_genkey_0": "Request-Token-Muyan",
          "_genkey_1": "www.yootk.com"
        }
      }
    ]
  },
  {
    "id": "token",
    "uri": "lb://token.provider",
    "order": 1,
    "predicates": [
      {
        "name": "Path",
        "args": {
          "pattern": "/token/**"
        }
      }
    ]
  }
]

6、 【Postman测试工具】访问Token 服务获取 Token信息:

gateway-9501:9501/token/create?mid=admin&password=hello

在这里插入图片描述
当前的路径不需要进行过滤器的排查,而其他微服务需要进行过滤器排查

7、【common-api子模块】修改部门微服务的接口 (改@Mapping)

@GetMapping("/dept.provider/provider/dept/get/{deptno}") // 远程REST接口
@GetMapping("/provider/dept/get/{deptno}") // 远程REST接口
package com.yootk.service;

import com.yootk.common.dto.DeptDTO;
import com.yootk.service.config.FeignConfig;
import com.yootk.service.fallback.DeptServiceFallbackFactory;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;

import java.util.List;
import java.util.Map;
@FeignClient(value = "microcloud.gateway", // 使用网关的名称进行访问
        configuration = FeignConfig.class,// 定义要访问的微服务实例名称
        fallbackFactory = DeptServiceFallbackFactory.class) // 部门降级配置
public interface IDeptService { // 业务接口
    /**
     * 根据部门的编号获取部门的完整信息
     * @param id 要查询的部门编号
     * @return 编号存在则以DTO对象的形式返回部门数据,如果不存在返回null
     */
    @GetMapping("/provider/dept/get/{deptno}") // 远程REST接口
    public DeptDTO get(@PathVariable("deptno") long id);
    /**
     * 增加部门对象
     * @param dto 保存要增加部门的详细数据
     * @return 增加成功返回true,否则返回false
     */
    @PostMapping("/provider/dept/add")
    public boolean add(DeptDTO dto);
    /**
     * 列出所有的部门数据信息
     * @return 全部数据的集合, 如果没有任何的部门数据则集合为空(size() == 0)
     */
    @GetMapping("/provider/dept/list")
    public List<DeptDTO> list();
    /**
     * 进行部门的分页数据加载操作
     * @param currentPage 当前所在页
     * @param lineSize 每页加载的数据行数
     * @param column 模糊查询的数据列
     * @param keyword 模糊查询关键字
     * @return 部门集合数据以及统计数据,返回的数据项包括:
     * 1、key = allDepts、value = List集合(部门的全部数据对象)
     * 2、key = allRecorders、value = 总记录数;
     * 3、key = allPages、value = 页数。
     */
    @GetMapping("/provider/dept/split")
    public Map<String, Object> split(
            @RequestParam("cp") int currentPage,
            @RequestParam("ls") int lineSize,
            @RequestParam("col") String column,
            @RequestParam("kw") String keyword);
}

消费端获取JWT

现在已经成功的搭建TokenServer、微服务的授权检测、网关过滤检查,那么剩下的部分就是要对消费端进行整改了,毕竟,最终可以发布给外部的就是消费端。
在这里插入图片描述

1、【common-api子模块】创建Token 业务接口并映射Token 操作路径

package com.yootk.service;

import com.yootk.common.dto.MemberDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient("microcloud.gateway")
public interface IMemberTokenService {
    @GetMapping("/token/create")
    public String login(MemberDTO memberDTO);
}

2、【common-api子模块】因为后面可能会有微服务之间混合调用的情况,所以可以考虑做一个公共的加载类。

package com.yootk.service.load;

import com.yootk.common.dto.MemberDTO;
import com.yootk.service.IMemberTokenService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class FeignTokenLoaderRunner implements CommandLineRunner {
    @Autowired
    private IMemberTokenService memberTokenService; // 远程接口映射
    @Override
    public void run(String... args) throws Exception {
        MemberDTO dto = new MemberDTO();
        dto.setMid("admin");
        dto.setPassword("hello");
        String token = this.memberTokenService.login(dto); // 获取Token
        if (token != null) {    // 已经获取到了Token数据
            log.info("获取Token数据成功:{}", token);
            System.setProperty("yootk.token", token); // 属性不允许为null
        }
    }
}

3、【common-api子模块】修改Feign配置类,由于每次请求时都需要传递JWT的数据内容,所以可以配置一个拦截器。

package com.yootk.service.config;

import feign.Logger;
import feign.RequestInterceptor;
import org.springframework.context.annotation.Bean;

public class FeignConfig { // 定义Feign配置类
    @Bean
    public Logger.Level level() {
        return Logger.Level.FULL; // 输出完全的日志信息
    }
    @Bean
    public RequestInterceptor getFeignRequestInterceptor() {    // 请求拦截器
        return (template -> {
            template.header("serviceName", "pc");
            // 将系统JVM进程保存的Token数据发送到目标请求端
            template.header("yootk-token", System.getProperty("yootk.token"));
        });
    }
}

4、【consumer-springboot-80子模块】修改程序启动类,并定义FeignConfig配置类,启动程序

( defaultConfiguration = FeignConfig.class )

package com.yootk.consumer;

import com.yootk.service.config.FeignConfig;
import muyan.yootk.config.ribbon.DeptProviderRibbonConfig;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;

@SpringBootApplication
@EnableDiscoveryClient
// 如果此时要有多个配置项,可以使用@RibbonClients注解,该注解可以配置多个@RibbonClient
@RibbonClient(name = "dept.provider", configuration = DeptProviderRibbonConfig.class) // 自定义Ribbon配置
@ComponentScan({"com.yootk.service", "com.yootk.consumer"})
@EnableFeignClients(basePackages = {"com.yootk.service"}, defaultConfiguration = FeignConfig.class) // Feign扫描包
public class StartConsumerApplication {
    public static void main(String[] args) {
        SpringApplication.run(StartConsumerApplication.class, args);

    }
}

启动程序出现如下错误,按要求设置spring.main
在这里插入图片描述

5、【comsuner-springboot-80子模块】修改application.yml配置文件,添加一个配置覆盖的选项

spring:
  main:
    allow-bean-definition-overriding: true

在这里插入图片描述

此时消费端启动时会通过远程接口实现 Token 数据的获取,随后在每次进行访问的时候都会将此Token进行请求头信息的设置,这样就实现了认证和授权的统一管理。

(实际Token是由前端传过来的)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

AloneDrifters

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值