权限认证是Java后端开发者绕不开的核心话题。我记得自己刚转Java那会儿,接手的第一个项目用的还是传统的Session + Filter方案,后来换到Spring Security,配置XML写了一堆,登录流程、权限模型、过滤器链层层嵌套,每次调权限问题都要翻文档翻半天。后来做的项目多了,我发现中小型系统真正需要的其实是一个拿过来就能用、源码能看懂、改起来不费劲的轻量级认证框架。于是我自己动手写了一个,也算是把这么多年对权限认证的理解沉淀下来。这篇博文就把这个轻量级Java权限认证框架(含源码)的核心设计思路、关键实现和踩坑记录全部拆开讲清楚,希望能够帮到正在选型或者想自己写一套认证组件的朋友。
先说清楚这个框架到底解决什么问题。它不是一个替代Spring Security的通用方案,而是面向单体应用、中小型微服务、内部管理系统这类场景,提供一套够用且不臃肿的认证授权能力:用户登录后签发Token,请求接口时校验Token,通过注解或拦截器控制接口的访问权限,支持角色和权限点的灵活配置。整个框架代码量控制在千行级别,核心模块没有引入额外的重型依赖,阅读源码的成本极低。适合的人群:Java初学者想搞懂权限认证原理、中级开发者在项目中被Spring Security的复杂度困扰、以及需要在简历上展示一个完整开源项目的朋友。
1. 为什么主流框架对中小项目过于笨重,以及自研的初衷
1.1 大型权限框架的复杂度来源
Spring Security的功能确实强大,但它解决问题的维度很多:认证过滤器链、方法级安全、OAuth2/OIDC协议支持、CSRF防护、CORS配置、会话并发控制等等。每个维度都有一堆抽象类、配置类和SPI扩展点。对一个只需要“登录、校验、权限判断”这三个基础能力的CRUD管理系统来说,Spring Security的引入成本远大于收益。
我举个实际例子。一个企业内部的管理后台,用户角色无非是管理员、运营、普通员工,权限控制到菜单和按钮级别。用Spring Security做这件事,你得先搞清楚SecurityFilterChain的执行顺序,理解AuthenticationManager、ProviderManager、DaoAuthenticationProvider这些类之间的关系,还要设计UserDetailsService的实现。光是这些概念的梳理,新接手项目的同学至少需要一周时间。而且很多项目团队的Spring Security配置是从网上复制粘贴的,一旦出现权限绕过或者登录失效的Bug,排查链路非常长。
对比之下,一个轻量级框架只需要做好三件事:登录时验证身份并颁发凭证、请求时校验凭证是否有效、根据凭证携带的角色权限信息决定是否放行。这个框架从设计之初就把这三件事作为核心目标,不引入任何超出这三个需求之外的复杂概念。
1.2 轻量级框架的定位与需求边界
在设计这个框架之前,我先给自己的项目划定了明确的需求边界:
- 基于Token的认证方式,不依赖Session,便于后续横向扩展
- 支持注解式权限控制和拦截器式权限控制两种方式
- 用户、角色、权限点三层模型,权限点精确到接口级别
- 提供默认的密码加密工具,同时也兼容自定义加密策略
- 预留存储抽象层,默认基于内存,也可以扩展为Redis或数据库存储
- 代码足够少,核心包源码量不超过1500行
这个边界划定非常重要。很多自研框架之所以后期失控,就是因为一开始需求边界模糊,今天加个OAuth支持,明天加个单点登录,最终代码膨胀到和Spring Security一样复杂。我在开发过程中坚持一个原则:只做场景需要的事,不为想象中可能出现的需求写代码。
1.3 什么时候适合用自研框架
我也要说句公道话。如果你的项目面向公网用户,涉及到第三方授权登录、多端扫码、复杂的密码找回流程,或者公司的安全合规部门要求必须使用业界成熟方案,那直接选Spring Security或者Sa-Token这类成熟框架更稳妥。自研轻量级框架的适用场景是:团队对权限需求有清晰认知、系统面向内部或受控环境、希望完整掌控权限链路的所有代码、或者学习目的远大于生产目的。
明白这些边界之后,下面来拆解这个框架的架构设计。
2. 框架整体架构与核心设计思想
2.1 认证、授权、会话管理三件事的职责划分
整个框架的核心是三个模块的清晰分工。很多初学者容易把认证和授权混在一起,实际上这是两件完全不同的事。
**认证(Authentication)**是确认“你是谁”。用户提交用户名和密码,系统验证密码是否正确,正确则认为登录成功,签发一个Token。这个环节解决的问题是身份确认。
**授权(Authorization)**是决定“你能做什么”。用户登录成功后,系统需要根据用户的角色或权限点,决定他能否访问某个接口、操作某个按钮。这个环节解决的是访问控制。
**会话管理(Session Management)**是维护“你登录后的状态”。Token发出去了,服务端怎么记录这个Token?Token过期了怎么办?用户退出了如何让Token失效?这些都是会话管理要处理的问题。
三个模块在框架源码中的体现是三个核心包:auth包负责登录认证和Token签发,authorization包负责权限判断,session包负责Token的存储与过期管理。三个包之间的依赖关系是单向的,auth依赖session,authorization依赖auth,互不循环。这样的设计让后期维护变得非常轻松,改授权逻辑不需要动认证代码,换Token存储方案也不影响权限判断。
2.2 Token方案选型:为什么不用Session而选择自研Token
有人会问,Servlet体系自带的Session机制用起来不是更简单吗?确实,传统单体应用中Session + Cookie方案的开发体验非常顺滑,框架自动帮你维护会话。但它有一个硬伤:Session状态保存在应用服务器内存中。
一旦应用需要部署多个实例,Session同步问题就来了。要么引入Session共享组件(如Spring Session + Redis),要么做粘性会话让用户的请求始终打到同一台机器。这两个方案都有自己的麻烦,前者增加了架构复杂度,后者在节点故障时会导致会话丢失。
Token方案把状态从服务端转移到了客户端。用户登录成功后,服务端签发一个经过签名或携带加密信息的Token,客户端在后续请求中把这个Token放在请求头里。服务端不需要在内存中保存会话信息,只需要验证Token的合法性和有效期即可。这意味着应用可以很自然地水平扩展,多台服务器共享同一个密钥或同一个Token校验逻辑就能协同工作。
我这个框架的Token设计没有采用JWT标准,而是走了一条更轻的路线。Token本质上是一个随机生成的UUID加上时间戳信息,然后服务端在session存储中记录Token与用户ID的映射关系。选择这个方案的原因有两个:一是JWT标准虽然成熟,但引入了额外的编解码库和签名库;二是JWT一旦签发,在有效期内服务端无法主动使其失效,这在用户注销、强制退出的场景下非常尴尬。而基于服务端存储的Token方案,注销时直接删除存储中的映射即可。
需要说明的是,这个选择不等于否定JWT。在无状态服务、跨域认证、第三方开放API这些场景下,JWT的优势非常明显。我的框架定位是中小型业务系统,用户量和Token数量有限,服务端存储方案的可控性更好。
2.3 模块划分与源码目录设计
框架的源码目录如下所示,每个模块的职责非常单一:
source/ ├── pom.xml └── src/main/java/com/lightauth/ ├── auth/ │ ├── LoginService.java // 登录认证服务 │ ├── PasswordEncoder.java // 密码加密接口 │ └── DefaultPasswordEncoder.java // 默认MD5+盐实现 ├── authorization/ │ ├── RequireLogin.java // 需要登录注解 │ ├── RequireRole.java // 需要角色注解 │ ├── RequirePermission.java // 需要权限点注解 │ └── AccessDecisionManager.java // 权限决策管理器 ├── session/ │ ├── TokenSession.java // 会话实体 │ ├── SessionStore.java // 会话存储抽象 │ └── InMemorySessionStore.java // 内存存储实现 ├── config/ │ ├── LightAuthProperties.java // 框架配置项 │ └── LightAuthAutoConfiguration.java // Spring Boot自动配置 └── interceptor/ └── AuthInterceptor.java // 登录拦截器这个目录结构遵循一个很重要的设计原则:按业务能力分包,而不是按技术层次分包。很多新手喜欢用entity、service、controller、mapper这种方式分层,短时间内看着清晰,但一旦项目变大,同层的类之间没有业务逻辑上的内聚关系,维护起来会越来越吃力。按能力分包之后,要改认证逻辑就直接进auth包,要加权限判断就进authorization包,上下文信息一目了然。
3. 认证流程核心实现拆解
3.1 登录认证与密码加密处理
登录接口的处理逻辑是框架最核心的入口。在LoginService中,登录流程被拆成四个步骤:
- 根据用户名从用户存储中加载用户信息和密码摘要
- 将用户提交的原始密码加盐后计算摘要,与存储的摘要比对
- 比对通过则创建会话,生成Token
- 返回Token给客户端
密码加密这里要重点强调。任何生产系统都严禁使用明文密码,这是安全底线。这个框架默认提供DefaultPasswordEncoder,实现的是加盐MD5方案。所谓加盐,就是在原始密码后面拼接一段随机字符串,再计算摘要。为什么需要加盐?因为单纯对密码做MD5,两个密码相同的用户会得到相同的摘要值,而且攻击者可以使用彩虹表直接反查出常见密码。加了盐之后,每个用户的盐值不同,即使密码相同,摘要值也不同,彩虹表失效。
提一下为什么默认方案选择MD5加盐而不是BCrypt。主要原因是保持框架的零依赖特性,MD5和加盐逻辑用JDK自带的MessageDigest类就能实现,不需要额外引入spring-security-crypto依赖。但我同时留了一个扩展口,PasswordEncoder是一个接口,项目里如果需要更强的加密方案,实现自己的PasswordEncoder并在配置类中替换Bean即可。我自己在框架的示例Demo中写了一个BCrypt实现,用的JDK自带加密库中可用的PBKDF2算法,效果和BCrypt类似,也是自适应的慢哈希算法。
密码比对时还需要注意时间恒定比较的问题。如果直接使用字符串的equals方法比较摘要,攻击者可以通过响应时间的微小差异来猜测密码摘要的每一位。正确做法是使用MessageDigest.isEqual()方法,它以恒定时间执行比较,不泄露任何位置信息。
3.2 Token的生成、存储与续期机制
Token生成逻辑位于LoginService中,核心代码如下:
public String login(String username, String rawPassword) { // 1. 加载用户信息 UserAccount user = userLoader.loadByUsername(username); if (user == null) { throw new AuthException("用户不存在"); } // 2. 验证密码 String encodedPassword = passwordEncoder.encode(rawPassword, user.getSalt()); if (!passwordEncoder.matches(encodedPassword, user.getPassword())) { throw new AuthException("密码错误"); } // 3. 创建会话 String token = UUID.randomUUID().toString().replace("-", ""); TokenSession session = new TokenSession(); session.setToken(token); session.setUserId(user.getId()); session.setUsername(user.getUsername()); session.setRoles(user.getRoles()); session.setPermissions(user.getPermissions()); session.setCreateTime(new Date()); session.setExpireTime(new Date(System.currentTimeMillis() + expireTime * 1000L)); // 4. 保存会话 sessionStore.save(session); return token; }Token本身就是一个UUID字符串,没有携带任何业务信息。这么做的好处是:Token无法被客户端解密伪造,所有业务数据都在服务端会话中管理。框架中的TokenSession实体携带了用户的角色和权限点信息,这样在权限校验时不需要每次都从数据库加载用户角色权限,直接从会话对象中获取即可。
关于Token的有效期设置,我参考了互联网大厂的安全实践:活跃用户自动续期,不活跃用户到期失效。具体实现是在AuthInterceptor中每次校验Token时检查是否超过了续期时间点(比如有效期的一半),如果超过了就重新计算过期时间并更新会话。这套滑动过期策略的体验非常接近Session机制,用户只要持续使用系统就不会被踢出登录状态。
3.3 拦截器与注解驱动的鉴权控制
框架提供两种鉴权方式:拦截器方式和注解方式。
拦截器方式适合做全局的登录状态校验。系统里大部分接口都需要登录后才能访问,通过AuthInterceptor统一拦截,判断请求头中是否携带Token、Token是否有效、会话是否过期。这个拦截器注册在Spring MVC的拦截器链中,只对配置的路径生效。
Java的安全模型要求绝对禁止使用反射获取字段或绕过方法签名检查,通过AccessDecisionManager配合Spring AOP实现注解式鉴权。核心的权限判断逻辑是这样的:
public void check(TokenSession session, String requiredRole, String requiredPermission) { if (requiredRole != null && !requiredRole.isEmpty()) { if (!session.getRoles().contains(requiredRole)) { throw new ForbiddenException("缺少角色: " + requiredRole); } } if (requiredPermission != null && !requiredPermission.isEmpty()) { if (!session.getPermissions().contains(requiredPermission)) { throw new ForbiddenException("缺少权限点: " + requiredPermission); } } }这段代码看起来简单,但权限模型设计的关键在于:角色的集合和权限点的集合被冗余存储在了会话中。权限判断因此避开了数据库查询,变成了纯内存的集合判断,性能极高。对于权限数据相对固定的系统来说,这种方案的性价比非常突出。
4. 关键代码实战——从零搭建一套可运行的框架
4.1 核心注解定义
注解是框架的门面,也是使用者感受最直观的部分。框架提供了三个核心注解:@RequireLogin、@RequireRole、@RequirePermission。
@Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface RequirePermission { String value(); }这里有两个设计细节。第一,@Target同时允许注解加在方法上和类上,这样设计是为了方便开发者把一组接口的公共权限提升到类级别。第二,@Retention必须是RUNTIME,因为框架运行时要通过反射读取注解信息。很多新手在这里容易踩坑,如果保留策略选错,框架运行时就永远读不到注解,权限控制会静默失效。
4.2 权限校验器的实现
权限校验器是基于Spring AOP实现的。核心思路是:定义环绕通知,在方法执行前读取方法上的注解,然后从当前请求上下文中获取会话信息,最后做权限比对。
@Aspect @Component public class AuthorizationAspect { @Around("@annotation(requirePermission)") public Object checkPermission(ProceedingJoinPoint joinPoint, RequirePermission requirePermission) throws Throwable { // 从ThreadLocal中获取当前会话 TokenSession session = LoginContextHolder.getSession(); if (session == null) { throw new AuthException("未登录"); } // 检查权限点 accessDecisionManager.checkPermission(session, requirePermission.value()); return joinPoint.proceed(); } }这里有个重要的设计:会话信息存放在ThreadLocal中。AuthInterceptor在请求进入Controller之前完成Token校验,并把解析出的TokenSession对象放入LoginContextHolder(一个ThreadLocal容器)。这样,在Controllers请求处理链中的任何位置,都能通过LoginContextHolder.getSession()获取当前登录用户。请求结束时,拦截器会调用LoginContextHolder.clear()清理ThreadLocal,避免线程池复用线程时出现用户数据错乱的问题。
4.3 Spring Boot集成配置
为了让框架在Spring Boot项目中开箱即用,我实现了自动配置类LightAuthAutoConfiguration。通过@ConfigurationProperties绑定配置前缀light.auth,支持以下配置项:
| 配置项 | 默认值 | 说明 |
|---|---|---|
| light.auth.enabled | true | 是否启用权限框架 |
| light.auth.expire-time | 7200 | Token有效期(秒) |
| light.auth.renew-threshold | 3600 | 距离过期时间少于该值则自动续期 |
| light.auth.header-name | Authorization | 从哪个请求头读取Token |
自动配置类负责创建工作所需的Bean,包括sessionStore、accessDecisionManager、authInterceptor等,避免使用者手动声明大量@Bean方法。对于需要自定义行为的场景,我在自动配置类上加了@ConditionalOnMissingBean,只要用户自己声明了同名Bean,框架就跳过默认创建,优先使用用户自定义的实现。
4.4 一个业务场景的完整演示
为了方便理解,我在框架源码中打包了一个Demo工程。这个Demo模拟了一个典型的管理系统接口权限控制场景:游客可以查看公告列表,登录用户可以创建公告,管理员可以删除公告。
@RestController @RequestMapping("/api/notice") public class NoticeController { @GetMapping("/list") public Result<List<Notice>> list() { return Result.success(noticeService.listAll()); } @PostMapping("/create") @RequireRole("ADMIN") public Result<Void> create(@RequestBody Notice notice) { noticeService.save(notice); return Result.success(); } @DeleteMapping("/{id}") @RequirePermission("notice:delete") public Result<Void> delete(@PathVariable Long id) { noticeService.remove(id); return Result.success(); } }从这个例子可以直观看到,权限控制的代码和业务逻辑完全解耦。方法上加上注解,权限校验的事就交给框架处理。网友在试用这个框架后反馈最多的就是:终于不用在业务方法里写一长串if (user.hasRole("ADMIN"))这种判断了。还有一个细节是,@RequireRole("ADMIN")意味着只有管理员角色的用户能访问这个方法,而@RequirePermission("notice:delete")要求的是权限点,权限点和角色是多对多的关系,更灵活。
这个Demo工程我放在源码的examples目录下,clone下来后可以直接运行,提供了一个完整的“启动—登录—鉴权—退出”的闭环体验。
5. 实操避坑:我在开发这个框架时踩过的雷
5.1 线程安全问题:SimpleDateFormat与ThreadLocal的经典陷阱
框架中需要处理Token的过期时间计算,这离不开日期时间的格式化与运算。我之前写工具类时图省事,把SimpleDateFormat定义成了类的静态成员变量。结果在压测环境下,一个接口偶尔出现时间错乱几小时的情况。
原因是SimpleDateFormat是线程不安全的,它的内部维护了一个Calendar实例,多线程并发调用format()或parse()时,多个线程的操作会互相覆盖,导致计算结果错乱。修复方案很简单:要么每次调用时新建一个SimpleDateFormat实例,要么使用ThreadLocal让每个线程持有自己的实例。我在框架中最终采用了ThreadLocal方案,性能更好。
同样的ThreadLocal问题还有一个容易被忽视的点:在登录拦截器中向ThreadLocal写入会话信息后,必须保证请求结束时的清理操作一定会执行。框架中把所有业务逻辑放在try-finally块中,确保任何情况下都会执行LoginContextHolder.clear()。如果忘了这一步,在高并发场景下Tomcat线程池的线程复用会导致用户A的会话被用户B读取到,这是隐藏极深的安全漏洞。
5.2 Token过期与并发刷新问题
滑动过期策略实现起来也遇到过难题。最初的代码是这样设计的:拦截器发现Token离过期时间不到阈值时,直接修改TokenSession的expireTime字段并保存。这在单线程请求下没有问题,但一旦同一用户的两个请求并发到达,两个请求都会执行续期逻辑,可能产生重复的更新操作。虽然在这个场景下重复更新没有实质危害,但如果是写入数据库的更新逻辑,就会产生不必要的开销和锁竞争。
后来我调整了设计思路:续期操作不要求强一致。既然Token只要在有效期内就能通过校验,那么即使两个并发请求都完成了续期,结果也是一样的——过期时间被延长。这个场景可以接受最终一致性的解决方案。这个思路帮助我在框架设计中做了一次重要取舍:有些操作不需要精确得如同数据库事务一般,理解业务场景对一致性要求的边界,比盲目追求技术上的完美更重要。
还有一个真实的Bug要提醒大家。框架在会话存储中保存的是TokenSession对象的引用,而AuthInterceptor从请求头中取出Token字符串后,每次都要调用sessionStore.get(token)获取会话。如果你在业务代码中直接修改了从LoginContextHolder获取的Session对象的某个字段,这个修改会影响所有后续请求读取到的会话数据。这是一个典型的共享可变状态问题,解决方法是:业务需要修改用户数据时,通过tokenSession.copy()创建副本进行修改,修改完成后重新更新到sessionStore中。
5.3 注解切面失效的典型原因
我在开发过程中遇到的另一个典型问题是Spring AOP注解切面失效。具体场景是:在一个类内部,一个方法调用另一个带@RequirePermission注解的方法,注解完全不生效。
@Service public class NoticeService { public void createAndPublish(Notice notice) { this.delete(notice.getId()); // 这里delete方法上的@RequirePermission不会生效 } @RequirePermission("notice:delete") public void delete(Long id) { // 删除逻辑 } }这个问题的根本原因是Spring AOP的代理机制。Spring AOP默认使用的JDK动态代理或CGLIB代理,只拦截外部通过代理对象发起的调用。类内部this引用调用的是原始对象的方法,不经过代理,所以切面不会触发。
解决方案有三种:一是把delete方法拆分到一个独立的Bean中,通过依赖注入调用;二是使用AopContext.currentProxy()获取当前代理对象再调用;三是说服自己这个设计本身有问题——在同一个事务性/业务性方法内调用另一个受权限保护的方法,本身就不符合职责边界清晰的原则。我在框架文档中特别标注了这种情况,避免使用者踩坑。
5.4 性能优化:减少不必要的数据库查询
轻量级框架的一个优势是性能可控。在写这个框架时,我特别在意每一次请求会触发多少次数据库查询。最初的版本中,登录校验的流程是:拦截器校验Token → 从数据库加载用户信息 → 比对角色权限 → 放行。这个流程每一次请求都有一次数据库查询。
后来我做了优化:将用户信息(包括角色、权限点)加入TokenSession冗余存储在sessionStore中,拦截器只需要从内存态的会话对象中读取角色权限数据,完全避免数据库访问。优化后,大多数请求的鉴权耗时从毫秒级降到了微秒级。这个优化思路在很多场景可以通用:把不经常变化的数据冗余到高频访问的载体上,用存储换性能。
当然这也有代价。如果角色的权限发生变化,已登录用户的会话中缓存的是旧的权限信息,需要用户重新登录才能生效。针对这个问题,框架在session层预留了一个forceLogout(token)方法,管理员调整权限后可以主动让对应用户的会话失效,强制用户重新登录获取最新权限。
6. 框架的业务扩展思路与应用场景
6.1 从简易模型到RBAC的多级扩展
基础版的框架模型是“用户—角色—权限点”三层结构,用户直接关联角色,角色关联权限点。但在真实业务中,往往需要更灵活的模型。
我在源码目录中预留了一个ext扩展包,里面提供了一个基于RBAC(基于角色的访问控制)的扩展示例。这个扩展版本引入了用户组的概念:一个用户可以属于多个用户组,用户组可以关联角色,用户也可以直接关联角色。权限判断时,框架会合并用户自身的角色和用户组继承的角色。这个扩展的代码量只有两百多行,却复用了核心框架的所有鉴权逻辑。
权限点本身也可以区分类型。框架内部定义了PERMISSION_TYPE_MENU和PERMISSION_TYPE_ACTION两种权限点类型,分别对应菜单可见性和操作权限。在前端做菜单动态渲染时,可以根据当前用户拥有的菜单权限点控制菜单的显隐,真正实现“菜单级+按钮级”的双层权限控制。
6.2 分布式场景下的扩展方案
在框架的设计中,SessionStore是会话存储的唯一抽象接口,正是这个接口让分布式扩展成为可能。默认实现是InMemorySessionStore,但切换为Redis分布式存储只需要两步:
第一步,引入Spring Data Redis依赖。第二步,实现SessionStore接口,把存储和查询逻辑委托给Redis的Hash结构。token作为key,TokenSession序列化后作为value,设置过期时间与Token有效期保持一致。这个实现不超过50行代码。
核心框架中,所有模块只依赖这个抽象接口,不关心会话数据的存储细节。我曾经在同一个Demo中分别用内存存储和Redis存储跑过同一套接口测试,完全无缝切换。这再次验证了一个架构真理:面向接口编程是扩展灵活性的根源。
不过也要提醒,引入Redis扩展时要注意序列化方案的一致性。如果你的项目中同时存在其他框架(如Spring Session)也在往Redis里写会话数据,一定确认序列化方式不冲突,避免数据互相覆盖。
6.3 框架使用建议与适用边界
总结一下我对这个框架使用边界的理解:
- 适合:内部管理系统、中小型Web应用、快速原型验证、教学演示、前后端分离项目的登录鉴权
- 避免:高安全等级场景(如金融核心系统)、需要复杂OAuth2协议的开放平台、极大规模集群下的统一认证中心
- 注意:无论框架多轻量,都无法替代完善的日志记录和安全审计。我在框架Demo中集成了登录日志和访问日志模块,仅供参照,生产环境需要结合具体的日志框架做更完整的审计
框架的授权协议是Apache 2.0,允许商用和二次开发,源码全部开放。你可以直接在项目中拷贝核心包源码进行定制,也可以借鉴其中的部分设计思路到自己的项目中。
最后分享一点个人的体会。写这个框架的过程让我对权限认证的理解上了一个台阶,以前用Spring Security时,很多问题只能靠翻阅文档解决,现在遇到权限相关的Bug,可以直接从原理层面分析。自己动手写一遍核心逻辑,比读十遍文档都有用。如果你也想深入理解权限认证机制,我建议你基于这个框架的源码,自己试着改一改:比如把Token从UUID换成JWT实现,或者把SessionStore从内存换成你自己的缓存组件。在这个过程中你会遇到很多文档里不会写的问题,而这些问题恰恰是最宝贵的成长机会。