业务流程
在我们的实现的API网关中,当接收 HTTP 请求以后,开始调用对应的 RPC 接口前,其实还应该做一步权限验证。也就是说你当前调用的 HTTP 接口是否含带了我授予的 Token 信息,这个 Token 是否在有效期范围等控制,这样才能保证一个 HTTP 的调用和返回结果是安全可靠的。
添加验证后流程如下图(图片来源于xfg)
如上图,我们需要有两部分的工作
关于网关中权限的校验会使用到 Shiro + Jwt,同时还要提供单独的 Handler 来处理 Netty 中的通信对信息的校验处理。但鉴于这部分属于两块功能,所以本章只先完成关于 Shiro + Jwt 部分。至于单独的handler将在下一章完成。
Apache Shiro 是 Java 的一个安全框架。目前,使用 Apache Shiro 的人越来越多,因为它相当简单。对比于 Spring Security,可能没有做的功能强大,但是在实际工作时并不需要那么复杂的东西,所以使用小而简单的 Shiro 就足够了。
本章节的代码工程结构如下
Shiro、Jwt 是两套东西,Shiro 是安全验证框架,Jwt 是(JSON Web Tokens)是一套 JSON 网络令牌,一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式在不同实体之间安全传输信息(JSON格式)。本章节的主要功能就是把这两套东西同时使用。
其中有关jwt,Shiro的东西请参考其官方文档
相关知识补充
JWT
JWT (JSON Web Token) 是目前最流行的跨域认证解决方案,是一种基于 Token 的认证授权机制。 从 JWT 的全称可以看出,JWT 本身也是 Token,一种规范化之后的 JSON 结构的 Token。
JWT 自身包含了身份验证所需要的所有信息,因此,我们的服务器不需要存储 Session 信息。这显然增加了系统的可用性和伸缩性,大大减轻了服务端的压力。
以下是 JSON Web Tokens 有用的一些场景:
授权 :这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是如今 JWT 广泛使用的一项功能,因为它开销小,并且易于跨域使用。
信息交换 :JSON Web Token 是各方之间安全传输信息的有效方式。由于 JWT 可以签名(例如,使用公钥/私钥对),因此您可以确保发送者的身份与其声明相符。此外,由于签名是使用标头和有效负载计算得出的,因此您还可以验证内容未被篡改。
JWT 本质上就是一组字串,通过(.
)切分成三个为 Base64 编码的部分:
Header(头部) : 描述 JWT 的元数据,定义了生成签名的算法以及
Token
的类型。Header 被 Base64Url 编码后成为 JWT 的第一部分。Payload(载荷) : 用来存放实际需要传递的数据,包含声明(Claims),如
sub
(subject,主题)、jti
(JWT ID)。Payload 被 Base64Url 编码后成为 JWT 的第二部分。Signature(签名):服务器通过 Payload、Header 和一个密钥(Secret)使用 Header 里面指定的签名算法(默认是 HMAC SHA256)生成。生成的签名会成为 JWT 的第三部分。
JWT 通常是这样的:xxxxx.yyyyy.zzzzz
。
如
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.KMUFsIDTnFmyG3nMiGM6H9FNFUROf3wh7SmqJp-QV30
JSON Web Token (JWT) 的验证和确认对于安全性至关重要,但它们处理的是 JWT 安全性的略微不同的方面:验证确保令牌格式正确且包含可执行的声明;确认确保令牌是真实的且未被修改。
JWT 验证一般是指检查 JWT 的结构、格式、内容:
结构 :确保令牌具有由点分隔的标准三个部分(标头、有效负载、签名)。
格式 :验证每个部分是否正确编码(Base64URL)以及有效载荷是否包含预期的声明。
内容 :检查有效载荷内的声明是否正确,例如到期时间(exp)、签发时间(iat)、不早于(nbf)等,以确保令牌未过期、未在其时间之前使用等。
另一方面, JWT 验证涉及确认令牌的真实性和完整性:
签名验证 :这是验证的主要环节,其中会根据标头和有效负载检查 JWT 的签名部分。此过程使用标头中指定的算法(例如 HMAC、RSA 或 ECDSA)以及密钥或公钥来完成。如果签名与预期不符,则表示令牌可能已被篡改或并非来自可信来源。
发行人验证 :检查 iss 声明是否与预期发行人相符。
Audience Check :确保 claim 要求与预期相符。
Shiro
这里只介绍一些Shiro相关的基本概念和用法,详细的信息还是得查看Shiro官方文档
Apache Shiro 是一个功能齐全且易于使用的 Java 安全框架,主要用于处理:
认证(Authentication) —— 验证用户身份("你是谁")。
授权(Authorization) —— 判断用户是否有执行某个操作的权限("你能做什么")。
加密(Cryptography) —— 提供密码加密、哈希等安全功能。
会话管理(Session Management) —— 不依赖 Web 容器即可管理用户会话。
有如下的核心概念
基本用法流程
初始化 SecurityManager
Shiro 支持从 配置文件(shiro.ini) 或 Java 代码 加载 SecurityManager:
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
认证(登录)
Subject currentUser = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
try {
currentUser.login(token); // 执行身份验证
System.out.println("认证成功!");
} catch (AuthenticationException e) {
System.out.println("认证失败!");
}
授权(权限检查)
if (currentUser.hasRole("admin")) {
System.out.println("拥有 admin 角色");
}
if (currentUser.isPermitted("user:create")) {
System.out.println("拥有创建用户的权限");
}
退出
currentUser.logout();
常见 Realm 类型
IniRealm —— 从 ini 文件中读取用户和权限数据。
JdbcRealm —— 从数据库读取用户信息(需配置数据源和 SQL)。
自定义 Realm —— 实现
AuthorizingRealm
来定制认证与授权逻辑。
核心概念关系
SecurityManager
核心大脑:Shiro 的总指挥,负责管理认证、授权、会话等所有安全操作。
作用:接收来自
Subject
的请求,调度Realm
去验证数据。生命周期:一般在系统启动时创建,并全局设置:
SecurityUtils.setSecurityManager(securityManager);
Realm(这里是 IniRealm)
数据提供者:SecurityManager 自己不存数据,它把认证/授权请求交给 Realm 去查。
IniRealm 特点:
从
shiro.ini
配置文件中加载用户、密码、角色和权限信息。适合 demo 或小型项目,不依赖数据库。
配置示例:
[users] zhangsan=123456,admin lisi=123,user [roles] admin=user:*,system:* user=user:read
Subject
当前操作的主体:代表“谁”在和系统交互,可以是用户,也可以是一个程序。
使用者只需要和
Subject
打交道,不必直接调用SecurityManager
。例:
Subject currentUser = SecurityUtils.getSubject();
关系图
Subject <-----> SecurityManager <-----> Realm(IniRealm)
Subject:发出认证/授权请求。
SecurityManager:安全控制核心,协调调度。
Realm:负责和真实数据打交道,验证用户信息和权限。
Token、Authentication、Authorization 的使用时机
Token(令牌)
作用:封装用户的登录信息(如用户名、密码、JWT)。
使用时机:在用户发起登录请求时创建。
例:
UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456"); currentUser.login(token);
注意:Token 并不包含“验证结果”,只是一个数据载体。
Authentication(认证)
作用:验证用户身份信息的正确性。
触发时机:
Subject.login(token)
被调用时。流程:
Subject
把Token
交给SecurityManager
。SecurityManager
调用Realm
中的doGetAuthenticationInfo()
。Realm 根据 Token 中的用户名/密码到数据源验证。
验证成功 → 返回
AuthenticationInfo
,失败 → 抛出异常。
示例:
try { currentUser.login(token); System.out.println("认证成功"); } catch (AuthenticationException e) { System.out.println("认证失败"); }
Authorization(授权)
作用:判断已登录用户是否有权限执行某个操作。
触发时机:访问需要权限的资源时(主动调用或注解检查)。
流程:
Subject
调用hasRole()
或isPermitted()
。SecurityManager
调用Realm
中的doGetAuthorizationInfo()
。Realm 返回该用户的角色和权限集合。
示例:
if (currentUser.hasRole("admin")) { System.out.println("有 admin 角色"); } if (currentUser.isPermitted("user:create")) { System.out.println("有创建用户的权限"); }
总结时序
初始化阶段
创建
SecurityManager
,配置Realm
(这里是IniRealm
),交给 Shiro。
认证阶段
用户提交 Token →
Subject.login()
→ SecurityManager 调用 Realm 验证。
授权阶段
已登录用户访问资源 → 检查角色/权限 → SecurityManager 调用 Realm 获取授权信息。
退出阶段
Subject.logout()
清除会话与缓存。
流程实现
首先我们要使用JWT,那就涉及到jwt的编解码,自定义编解码工具如下
JwtUtil.java
package com.zshunbao.gateway.authorization;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @program: api-gateway-core
* @ClassName JwtUtil
* @description: JWT工具,详情参考官方文档:https://jwt.io/
* @author: zs宝
* @create: 2025-08-12 15:35
* @Version 1.0
**/
public class JwtUtil {
//定义密钥,不能泄漏
private static final String signingKey = "B*B^5Fe";
/**
* 生成 JWT Token 字符串
* @param issuer 签发人
* @param ttlMillis 有效期
* @param claims 额外信息
* @return Token
*/
public static String encode(String issuer, long ttlMillis, Map<String, Object> claims) {
if(null==claims){
claims=new HashMap<>();
}
//签发时间:载荷部分的标准字段之一
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//签发操作
JwtBuilder builder = Jwts.builder()
// 荷载部分
.setClaims(claims)
// 签发时间
.setIssuedAt(now)
// 签发人;类似 userId、userName
.setSubject(issuer)
// 设置生成签名的算法和秘钥
.signWith(SignatureAlgorithm.HS256, signingKey);
if(ttlMillis>=0){
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
// 过期时间(exp):荷载部分的标准字段之一,代表这个 JWT 的有效期。
builder.setExpiration(exp);
}
return builder.compact();
}
public static Claims decode(String token) {
return Jwts.parser()
// 设置签名的秘钥
.setSigningKey(signingKey)
// 设置需要解析的 jwt
.parseClaimsJws(token)
.getBody();
}
}
在有我们自己的关于JWT的编解码工具后,我们需要自定义我们自己的属于网关项目的安全校验工具
我们使用JTW是希望用jwt加密我们的token,从而进行保密,结合Shiro 安全验证框架,我们需要自定义以下两个东西
首先就是我们的token相关的东西
package com.zshunbao.gateway.authorization;
import org.apache.shiro.authc.AuthenticationToken;
/**
* @program: api-gateway-core
* @ClassName GatewayAuthorizingToken
* @description: 验证token
* @author: zs宝
* @create: 2025-08-12 15:55
* @Version 1.0
**/
public class GatewayAuthorizingToken implements AuthenticationToken {
private static final long serialVersionUID = 1L;
// 通信管道ID
private String channelId;
// JSON WEB TOKEN
private String jwt;
public GatewayAuthorizingToken() {
}
public GatewayAuthorizingToken(String channelId, String jwt) {
this.channelId = channelId;
this.jwt = jwt;
}
public String getChannelId() {
return channelId;
}
public void setChannelId(String channelId) {
this.channelId = channelId;
}
public String getJwt() {
return jwt;
}
public void setJwt(String jwt) {
this.jwt = jwt;
}
@Override
public Object getPrincipal() {
return channelId;
}
@Override
public Object getCredentials() {
return this.jwt;
}
}
然后就是我们这里使用Shiro 框架后,我们对于自己token该如何进行验证,这就涉及到Shiro 框架后在Realm的使用,因此我们需要自定义我们自己网关的Realm,如下
package com.zshunbao.gateway.authorization;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
/**
* @program: api-gateway-core
* @ClassName GatewayAuthorizingRealm
* @description: 验证领域
* @author: zs宝
* @create: 2025-08-12 15:59
* @Version 1.0
**/
public class GatewayAuthorizingRealm extends AuthorizingRealm {
@Override
public Class<?> getAuthenticationTokenClass(){
return GatewayAuthorizingToken.class;
}
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
return null;
}
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
try {
JwtUtil.decode(((GatewayAuthorizingToken)token).getJwt());
}catch (Exception e){
throw new AuthenticationException("无效令牌");
}
return new SimpleAuthenticationInfo(token.getPrincipal(),token.getCredentials(),this.getName());
}
}
现在有了我们网关自定义的验证,加密工具后,我们需要将自定义好最外层的认证接口
package com.zshunbao.gateway.authorization;
/**
* @program: api-gateway-core
* @ClassName IAuth
* @description: 认证服务接口
* @author: zs宝
* @create: 2025-08-12 15:33
* @Version 1.0
**/
public interface IAuth {
boolean validate(String id,String token);
}
其实现为
package com.zshunbao.gateway.authorization.auth;
import com.zshunbao.gateway.authorization.GatewayAuthorizingToken;
import com.zshunbao.gateway.authorization.IAuth;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
/**
* @program: api-gateway-core
* @ClassName AuthService
* @description: 认证服务实现
* @author: zs宝
* @create: 2025-08-12 16:07
* @Version 1.0
**/
public class AuthService implements IAuth {
private Subject subject;
public AuthService(){
// 1. 获取 SecurityManager 工厂,此处使用 shiro.ini 配置文件初始化 SecurityManager
Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
//2. 得到 SecurityManager 实例 并绑定给 SecurityUtils
SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3. 得到 Subject 及 Token
this.subject = SecurityUtils.getSubject();
}
@Override
public boolean validate(String id, String token) {
try {
//身份验证
subject.login(new GatewayAuthorizingToken(id,token));
//返回结果
return subject.isAuthenticated();
}finally {
subject.logout();
}
}
}
这里一旦进行安全验证将会有一下代码流程
最后还有一个关于Shiro 框架的配置
shiro.ini
[main]
# 声明1个Realm,也可以声明多个,多个则顺序执行
gatewayAuthorizingRealm=com.zshunbao.gateway.authorization.GatewayAuthorizingRealm
# 指定 securityManager 的 realms 实现。如果是多个则用逗号隔开。
securityManager.realms=$gatewayAuthorizingRealm
测试
针对本章内容的测试为
package com.zshunbao.gateway.test;
import com.zshunbao.gateway.authorization.IAuth;
import com.zshunbao.gateway.authorization.JwtUtil;
import com.zshunbao.gateway.authorization.auth.AuthService;
import io.jsonwebtoken.Claims;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.HashMap;
import java.util.Map;
public class ShiroTest {
private final static Logger logger = LoggerFactory.getLogger(ShiroTest.class);
@Test
public void test_auth_service() {
IAuth auth = new AuthService();
boolean validate = auth.validate("DPij8iUY", "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ6c2h1bmJhbyIsImV4cCI6MTc1NTU5MTU1NSwiaWF0IjoxNzU0OTg2NzU1LCJrZXkiOiJ6c2h1bmJhbyJ9.hD5GJIDEynXDjQCGl2AwDrKszL6t98uumMue0DpMm3E");
System.out.println(validate ? "验证成功" : "验证失败");
}
@Test
public void test_jwt() {
String issuer = "zshunbao";
long ttlMillis = 7 * 24 * 60 * 60 * 1000L;
Map<String, Object> claims = new HashMap<>();
claims.put("key", "zshunbao");
// 编码
String token = JwtUtil.encode(issuer, ttlMillis, claims);
System.out.println(token);
// 解码
Claims parser = JwtUtil.decode(token);
System.out.println(parser.getSubject());
}
@Test
public void test_shiro() {
// 1. 获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager
Factory<org.apache.shiro.mgt.SecurityManager> factory =
new IniSecurityManagerFactory("classpath:test-shiro.ini");
// 2. 得到SecurityManager实例 并绑定给SecurityUtils
org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
SecurityUtils.setSecurityManager(securityManager);
// 3. 得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证)
Subject subject = SecurityUtils.getSubject();
// 4. 默认提供的验证方式;UsernamePasswordToken
UsernamePasswordToken token = new UsernamePasswordToken("zs", "456");
try {
//5.1、登录,即身份验证
subject.login(token);
} catch (AuthenticationException e) {
//5.2、身份验证失败
System.out.println("身份验证失败");
}
System.out.println(subject.isAuthenticated() ? "验证成功" : "验证失败");
// 6. 退出
subject.logout();
}
}
test-shiro.ini
如下
[users]
zshunbao=123
zs=456
注意上述内容测试时关于test_auth_service
方法的token最好是使用test_jwt
新生成的,不然很有可能因为jwt过期而验证不通过