跳至主要內容

微服务安全与监控

wangdx大约 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


上次编辑于: