业务流程

在我们的实现的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 安全框架,主要用于处理:

  1. 认证(Authentication) —— 验证用户身份("你是谁")。

  2. 授权(Authorization) —— 判断用户是否有执行某个操作的权限("你能做什么")。

  3. 加密(Cryptography) —— 提供密码加密、哈希等安全功能。

  4. 会话管理(Session Management) —— 不依赖 Web 容器即可管理用户会话。

有如下的核心概念

概念

说明

Subject

当前与应用交互的用户或系统进程,代表“谁在访问系统”。

SecurityManager

Shiro 的核心组件,管理所有安全操作(类似 Spring 的 ApplicationContext)。

Realm

数据访问组件,用于连接安全数据源(如数据库、LDAP),完成身份验证和授权逻辑。相当于一层DAO

Token

封装用户提交的身份信息(如用户名、密码),供认证过程使用。

Authentication

验证用户身份信息是否正确。

Authorization

判断用户是否具备访问特定资源的权限。

基本用法流程
  1. 初始化 SecurityManager

Shiro 支持从 配置文件(shiro.ini)Java 代码 加载 SecurityManager:

 Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
 SecurityManager securityManager = factory.getInstance();
 SecurityUtils.setSecurityManager(securityManager);
  1. 认证(登录)

 Subject currentUser = SecurityUtils.getSubject();
 UsernamePasswordToken token = new UsernamePasswordToken("zhangsan", "123456");
 ​
 try {
     currentUser.login(token); // 执行身份验证
     System.out.println("认证成功!");
 } catch (AuthenticationException e) {
     System.out.println("认证失败!");
 }
  1. 授权(权限检查)

 if (currentUser.hasRole("admin")) {
     System.out.println("拥有 admin 角色");
 }
 ​
 if (currentUser.isPermitted("user:create")) {
     System.out.println("拥有创建用户的权限");
 }
  1. 退出

 ​
 currentUser.logout();

常见 Realm 类型

  1. IniRealm —— 从 ini 文件中读取用户和权限数据。

  2. JdbcRealm —— 从数据库读取用户信息(需配置数据源和 SQL)。

  3. 自定义 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) 被调用时。

  • 流程:

    1. SubjectToken 交给 SecurityManager

    2. SecurityManager 调用 Realm 中的 doGetAuthenticationInfo()

    3. Realm 根据 Token 中的用户名/密码到数据源验证。

    4. 验证成功 → 返回 AuthenticationInfo,失败 → 抛出异常。

  • 示例:

     try {
         currentUser.login(token);
         System.out.println("认证成功");
     } catch (AuthenticationException e) {
         System.out.println("认证失败");
     }

Authorization(授权)

  • 作用:判断已登录用户是否有权限执行某个操作。

  • 触发时机:访问需要权限的资源时(主动调用或注解检查)。

  • 流程:

    1. Subject 调用 hasRole()isPermitted()

    2. SecurityManager 调用 Realm 中的 doGetAuthorizationInfo()

    3. 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过期而验证不通过

参考资料