文章目录
- JWT工具模块
- Token认证微服务
- JWT授权监测
- 网关认证过滤
- 消费端获取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')// 以下的组件会被其他的模块继续引用,所以必须将其的编译范围配置为compilecompile(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的数据处理类对象@Autowiredprivate JWTConfigProperties jwtConfigProperties; // 获取JWT的相关配置属性@Value("${spring.application.name}") // 通过SpEL进行配置注入private String applicationName; // 应用名称private SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 签名算法@Overridepublic SecretKey generalKey() {byte [] encodeKey = Base64.decodeBase64(Base64.encodeBase64(this.jwtConfigProperties.getSecret().getBytes()));SecretKey key = new SecretKeySpec(encodeKey, 0, encodeKey.length, "AES"); // 获取加密KEYreturn key;}@Overridepublic 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}@Overridepublic 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}@Overridepublic boolean verifyToken(String token) {try {Jwts.parser().setSigningKey(this.generalKey()).parseClaimsJws(token).getBody();return true; // 没有异常就返回true} catch (Exception e) {}return false;}@Overridepublic 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 {@Autowiredprivate 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();}}@Overridepublic 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: muyanissuer: Muyansecret: yootkexpire: 100 # 单位:秒password:encrypt:repeat: 5salt: yootkspring: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 { // 代码测试@Autowiredprivate ITokenService tokenService;private String jwt = "eyJhdXRob3IiOiLmnY7lhbTljY4iLCJtb2R1bGUiOiJKV1QtVEVTVCIsImFsZyI6IkhTMjU2In0.eyJtc2ciOiLkuJbnlYzkuIrniIblj6_niLHnmoTogIHluIgg4oCU4oCUIOeIhuWPr-eIseeahOWwj-adjuiAgeW4iCIsInN1YiI6IntcInJpZHNcIjpcIlVTRVI7QURNSU47REVQVDtFTVA7Uk9MRVwiLFwibmFtZVwiOlwi5rKQ6KiA56eR5oqAIOKAlOKAlCDmnY7lhbTljY5cIixcIm1pZFwiOlwibXV5YW5cIn0iLCJzaXRlIjoid3d3Lnlvb3RrLmNvbSIsImlzcyI6Ik11eWFuWW9vdGsiLCJleHAiOjE2MzM2NzE3NjcsImlhdCI6MTYzMzU3MTc2NywibmljZSI6Ikdvb2QgR29vZCBHb29kIiwianRpIjoieW9vdGstMDgwNGI3NDQtNTBjZC00NjI2LTgzNmEtNjA1MmFiZWMyYzQ4In0.O71QGGPtWYwL7Tyhx8iOLQFAWc1DmVlAS4i0N99OJJk"; // 测试解析使用的@Testpublic 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));}@Testpublic 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);});} @Testpublic void testVerifyJWT() {System.out.println("【JWT数据验证】" + this.tokenService.verifyToken(jwt));}@Testpublic 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 {@Autowiredprivate IEncryptService encryptService;@Testpublic 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: 80service: ${spring.application.name} # 使用微服务的名称作为注册的服务名称server-addr: nacos-server:8848 # Nacos服务地址namespace: 96c23d77-8d08-4648-b750-1217845607ee # 命名空间IDgroup: 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 # 允许执行批量SQLdelete-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: muyanissuer: MuyanYootksecret: www.yootk.comexpire: 100000 # 单位:秒password:encrypt:repeat: 5salt: 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 {@TableIdprivate 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 {@TableIdprivate 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 {@Autowiredprivate IMemberDAO memberDAO;@Autowiredprivate IRoleDAO roleDAO;@Autowiredprivate IActionDAO actionDAO;@Overridepublic 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 {@Autowiredprivate IMemberService memberService;@Autowiredprivate IEncryptService encryptService; // 自动装配模块提供的@Testpublic 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 {@Autowiredprivate IMemberService memberService; // 本模块提供的@Autowiredprivate IEncryptService encryptService; // yootk-starter-jwt模块提供的@Autowiredprivate 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 { // 自定义的数据的解析类@Autowiredprivate ITokenService tokenService;@Autowiredprivate 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头信息转为Mapheaders.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 {@Autowiredprivate 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) { // 后面会修改参数模式为JSONthis.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;@Overridepublic 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 {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(this.getDefaultHandlerInterceptor()).addPathPatterns("/**");}@Beanpublic HandlerInterceptor getDefaultHandlerInterceptor() {return new JWTAuthorizeInterceptor();}
}
9、【provider-dept-8002子模块】如果要想正确的驱动“yootk-starter-jwt”组件,那么就需要在application.yml里面添加与JWT组件模块有关的配置项
yootk:security:config:jwt:sign: muyanissuer: MuyanYootksecret: www.yootk.comexpire: 100000 # 单位:秒password:encrypt:repeat: 5salt: 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: muyanissuer: MuyanYootksecret: www.yootk.comexpire: 100000 # 单位:秒password:encrypt:repeat: 5salt: 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 { // 全局过滤器@Autowiredprivate GatewayJWTConfigProperties jwtConfig; // JWT的相关配置属性@Autowiredprivate ITokenService tokenService; // 进行Token处理@Overridepublic 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 {@Autowiredprivate IMemberTokenService memberTokenService; // 远程接口映射@Overridepublic void run(String... args) throws Exception {MemberDTO dto = new MemberDTO();dto.setMid("admin");dto.setPassword("hello");String token = this.memberTokenService.login(dto); // 获取Tokenif (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配置类@Beanpublic Logger.Level level() {return Logger.Level.FULL; // 输出完全的日志信息}@Beanpublic 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是由前端传过来的)