微服务安全与监控
大约 25 分钟
SpringCloud 认证管理简介
虽然微服务工作在内网,但是也需要进行有效的微服务安全保护,这样可以防止可能出现的安全漏洞微服务安全访问
微服务统-认证
- 由于微服务集群架构设计之中会存在有大量的服务节点,所以此时就需要一种统一的认证服务来进行管理,于是很多的读者首先到的就是 OAuth2 认证管理模式
OAuth2 整合问题
- 在 SpringBoot 中可以直接基于 SpringSecurity 来实现 OAuth2 的相关开发,这样就可以轻松的实现 OAuth2 服务端的搭建,并且结合 SpringSecurity 的提供的“@EnableGlobalMethodSecurity”注解还可以轻松的实现授权检查的处理,但是基于 OAuth2 实现的统一认证会存在有如下的几个问题:OAuth2 是为了 WEB 应用而设计的:OAuth2 在进行平台接入管理时会比较方便,同时也是一个实现的标准,但是其在最初设计时并为考虑到与 SpringCloud 的技术整合,所以存在有先天性的不足处理逻辑繁琐:OAuth2 认证需要首先进行客户端接入,而后再生成授权码,最后再通过客户端信息授权码再生成访问 Token,这样繁琐的逻辑必然带来较低的处理性能;SpringSecurity 支持不友好:在 SpringSecurity 之中已经默认取消了 OAuth2 支持维护,所以在整合中就有可能出现很多非正常的因素,而导致开发成本的上升;
JWT 安全认证
- 在新版本的 SpringCloud 项目开发中考虑到服务处理性能以及代码的可维护性,所以本书并不推荐使用 OAuth2 方式进行统一认证管理,并且 SpringCloud 官方推荐的技术是基于 JWT (JSON Web Token)的方式进行认证处理。这样只需要在每次请求前获取 JWT 数据,并且在每次请求时传递 JWT 数据即可轻松的实现认证与授权检查。
JWT 工具模块
JWT 工具模块
- 为了便于 JWT 数据操作管理的统一性,可以创建一个“yootk-starter-jwt”子模块,在该模块中可以提供相关的配置 Bean,例如:JWT 生成与解析(ITokenService)、密码考虑到微服务架加密处理 (lEncryptService)、响应状态(WTResponseCode)构中的多个模块会进行此工具模块的引用,所以可以将其定义为一个自动装配模块
1、
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.11.5'
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'
2、
ext.versions = [ // 定义全部的依赖库版本号
jjwt : '0.9.1', // jwt依赖库
jaxb : '2.3.0', // JAXB依赖库
]
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}",
]
3、
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')
}
}
4、
jar { enabled = true} // 允许打包为jar文件
bootJar { enabled = false } // 不允许打包为Boot执行文件
javadocJar { enabled = false } // 不需要打包为jar文件
javadocTask { enabled = false } // 不需要打包为doc文件
5、
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 + "}";
}
}
6、
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; // 失效时间
}
7、
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内容
}
8、
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容器启动时会自动提供Jacks实例
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;
}
}
9、
package com.yootk.jwt.service;
public interface IEncryptService { // 密码加密
public String getEncryptPassword(String password); // 得到一个加密后的密码
}
10、
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、
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;
}
}
12、
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();
}
}
13、
yootk:
security:
config:
jwt:
sign: muyan
issuer: MuyanYootk
secret: www.yootk.com
expire: 100 # 单位:秒
password:
encrypt:
repeat: 5
salt: www.yootk.com
spring:
application:
name: JWT-TEST
14、
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);
}
}
15、
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));
}
}
16、
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"));
}
}
Token 认证服务
TokenServer 应
- 微服务架构之中,如果要想获取有意义的 JT 数据信息,则需要凭借有效的身份信息用户名和密码来获取用户数据,并在认证成功之后此数据保存在 JWT 数据结构之中所以为了便于 Token 的操作,则可以创建一个“TokenServer"的应用,来进行此功能的统一管理
认证与授权数据库
- 在消费端每次启动时,都可以根据获取到的用户名和密码向 Token 服务器进行 Token 申而在 Token 申请的时候则可以通过 JT 的结构特点,将用户的授权数据进行保存随后就可以通过网关来进行 JWT 有效性的检查,随后在进行微服务资源调用的时候,再通过传递的 JWIT 数据进行授权信息检测,从而实现授权管理
1、
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(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匹配的依赖库
}
}
2、
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、
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 # 配置集群名称
username: muyan # 用户名
password: yootk # 密码
metadata: # 根据自身的需要配置元数据
version: 1.0 # 自定义元数据项
company: 沐言科技 # 自定义元数据项
url: www.yootk.com # 自定义元数据项
author: 李兴华(爆可爱的小李老师) # 自定义元数据项
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监控
yootk:
security:
config:
jwt:
sign: muyan
issuer: MuyanYootk
secret: www.yootk.com
expire: 100000 # 单位:秒
password:
encrypt:
repeat: 5
salt: www.yootk.com
4、
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;
}
5、
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;
}
6、
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;
}
7、
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> {
}
8、
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); // 根据用户名查询角色
}
9、
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); // 获取权限信息
}
10、
<?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>
11、
<?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>
12、
<?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>
13、
package com.yootk.common.dto;
import lombok.Data;
@Data
public class MemberDTO {
private String mid;
private String password;
}
14、
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);
}
15、
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;
}
}
16、
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);
}
}
17、
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));
}
}
18、
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(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解析处理
}
}
19、
127.0.0.1 token-server-8201
20、
token-server-8201:8201/token/create?mid=admin&password=hello
21、
token-server-8201:8201/token/parse?token=eyJhdXRob3IiOiLmnY7lhbTljY4iLCJtb2R1bGUiOiJ0b2tlbi1wcm92aWRlciIsImFsZyI6IkhTMjU2In0.eyJtc2ciOiLkuJbnlYzkuIrniIblj6_niLHnmoTogIHluIgg4oCU4oCUIOeIhuWPr-eIseeahOWwj-adjuiAgeW4iCIsInN1YiI6IntcInJvbGVzXCI6W1wibWVtYmVyXCIsXCJlbXBcIixcImRlcHRcIl0sXCJhY3Rpb25zXCI6W1wiZW1wOmxpc3RcIixcImRlcHQ6ZWRpdFwiLFwiZGVwdDpsaXN0XCIsXCJlbXA6ZWRpdFwiLFwibWVtYmVyOmFkZFwiLFwiZGVwdDphZGRcIixcImVtcDphZGRcIixcIm1lbWJlcjplZGl0XCIsXCJkZXB0OmRlbGV0ZVwiLFwibWVtYmVyOmRlbGV0ZVwiLFwibWVtYmVyOmxpc3RcIixcImVtcDpkZWxldGVcIl19Iiwic2l0ZSI6Ind3dy55b290ay5jb20iLCJpc3MiOiJNdXlhbllvb3RrIiwiZXhwIjoxNzEwOTkyMjE5LCJpYXQiOjE3MTA4OTIyMTksIm5pY2UiOiJHb29kIEdvb2QgR29vZCIsImp0aSI6ImFkbWluIn0.Vgda-rY6eZYeJIk_JZ2_RDIJ70X--B_OALmBZVnBYMk
JWT 授权检测
JWT 数据拦截
- 微服务中的所有资源需要进行有效的授权管理,而每一个生成的 JT 数据之中都可以包含有完整的授权信息,这样就可以基于注解的方式对指定的 REST 资源进行保护,同时授权保护以保护的形式也可以分为三种:JWT 认证保护(存在有正确的 Token 信息)及权限保护。所有的 JWT 校验操作都应该在资源访问前进行处理,所以本次将基于拦截器的模式进行 JWT 的相关操作
1、
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、
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、
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、
project(":provider-dept-8002") { // 部门微服务
dependencies {
implementation(project(":common-api")) // 导入公共的子模块
implementation(project(":yootk-starter-jwt")) // 导入JWT子模块
}
}
5、
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));
}
}
}
6、
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;
}
}
7、
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();
}
}
8、
yootk:
security:
config:
jwt:
sign: muyan
issuer: MuyanYootk
secret: www.yootk.com
expire: 100000 # 单位:秒
password:
encrypt:
repeat: 5
salt: www.yootk.com
网关认证过滤
1、
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、
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、
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、
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、
[
{
"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、
gateway-9501:9501/token/create?mid=admin&password=hello
消费端获取 JWT
消费端 Token 操作流程
- 此时的网关需要进行 Token 有效性检查,而资源微服务需要进行 Token 授权检查,所以在消费端进行服务资源调用时就必须通过头信息的形式进行 Token 传递。考虑到代码整体的结构性需要,可以利用 CommandLineRunner 接口在容器启动时进行 Token 加载并将其保存在系统属性之中,这样在每次请求时基于拦截器的方式通过系统属性加载 Token 数据,并以头信息的形式发送 Token 数据,这样就可以实现服务的正确调用
1、
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、
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、
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、
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 { // 沐言科技:www.yootk.com
public static void main(String[] args) {
SpringApplication.run(StartConsumerApplication.class, args);
}
}
5、
spring:
main:
allow-bean-definition-overriding: true
6、
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.RequestBody;
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解析处理
}
}
demo