拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

从登录到鉴权:图库管理平台的Token、RBAC与中间件实战

从登录到鉴权:图库管理平台的Token、RBAC与中间件实战

做图库管理平台最怕什么?怕的是功能做到一半,发现连“谁能看、谁能传、谁能删”都说不清楚。这个智能协图云图库项目做到第三天,正好卡在这个节骨眼上。今天这篇我不打算绕弯子,直接拆解用户登录与鉴权这套东西——从需求设计、接口实现,到中间件拦截,再到我在实战里踩过的几个坑,全都摊开讲一下。

这个项目本质上是一个面向团队的协作图库,图片素材多、角色杂(管理员、设计师、访客),如果不在登录和鉴权上把地基打牢,后面加协作、加审核、加分享都会非常痛苦。所以第三天没有急着写业务接口,先花了一整天把用户身份和访问控制理顺。这篇文章适合正在做类似平台的后端开发、全栈开发者,以及对“登录鉴权怎么落地”有困惑的读者。

1. 为什么“登录”和“鉴权”必须拆开设计

1.1 两个概念的本质区别

我见过太多项目把登录和鉴权混成一锅粥。登录解决的是“你是谁”,鉴权解决的是“你能做什么”。这么说可能还是有点抽象,我拿图库平台的场景举例:

  • 登录:用户提交用户名密码,服务端校验通过后,给用户发一张“通行证”,也就是Token。这张证证明了“我是张三”。
  • 鉴权:用户拿着张三的证去访问“删除图库”接口时,服务端要检查“张三有没有删除图库的权限”。没有就直接拒绝,哪怕张三确实是合法登录用户。

注意,这一步的关键在于:鉴权一定是基于登录结果,但绝不等于登录。把两者混在一起设计,最典型的后果就是:所有登录用户拥有同样的权限,然后你就会陷入“加权限就写死if判断”的泥潭。

1.2 一次完整请求的链路

图库平台的典型请求链路是这样的:

  1. 用户浏览器输入账号密码,POST到登录接口。
  2. 登录接口校验密码,生成Token返回给前端。
  3. 前端把Token存起来,后续每次请求都带上(通常放在Authorization头里)。
  4. 后端中间件先解析Token,确认用户身份,再把请求交给具体业务处理器。
  5. 业务处理器判断当前用户是否有权限操作该资源(例如某个图库、某个相册)。

这里把第4步和第5步分开是有讲究的。第4步是认证,第5步是授权。工程上,认证通常由全局拦截器统一完成,授权由业务层或细粒度拦截器完成。如果全局拦截器顺带把所有资源访问都做掉,那资源之间的权限差异就很难表达。

1.3 为什么选Token而不是Session

第三天做技术选型的时候,团队里也争论过一轮:图库平台要不要引入Session?

Session的方案很经典:登录成功后,服务端存一份会话记录,给客户端一个SessionID。客户端拿着SessionID来,服务端查会话表。好处是随时可以踢人下线,坏处也很明显——集群环境下必须引入会话共享(Redis或粘性会话),并且前后端分离架构下,SessionID多放在Cookie里,跨域处理比较麻烦。

Token方案则是无状态的。服务端不存会话,Token本身携带用户ID、过期时间、签名。图库平台后续很可能要支持多端(网页、小程序、甚至触摸屏这类工控终端),无状态Token在多种客户端之间流转更省事。

最终这个项目里选了JWT。理由无非是:结构简单、自包含、适合前后端分离、也方便后续扩展到多种终端。当然,JWT有个天生的缺点是“难以主动失效”,后面我会讲我如何用Token版本号来缓解这个问题。

2. 登录模块的实现细节

2.1 用户表设计与密码存储

用户表的结构不复杂,但有一点必须重视:不要用明文密码。项目里我用了bcrypt加盐哈希存储,密码字段叫password_hash。顺便提一下,很多教程喜欢用SHA-256直接哈希,这在高速GPU面前其实不太安全,因为SHA系列天生算得快,暴力破解成本低。bcrypt自带盐和计算成本因子,能显著提高暴力破解的难度。

用户表大致字段:

字段类型说明
idbigint用户ID,主键自增
usernamevarchar(50)登录名,唯一索引
password_hashvarchar(100)bcrypt哈希结果
display_namevarchar(50)展示名,图库里的水印、评论都会用到
rolevarchar(20)角色,admin / editor / viewer
statustinyint1启用,0禁用
token_versionintToken版本号,用于主动失效
created_atdatetime创建时间

建表SQL大致长这样:

CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT, `username` varchar(50) NOT NULL, `password_hash` varchar(100) NOT NULL, `display_name` varchar(50) DEFAULT '', `role` varchar(20) NOT NULL DEFAULT 'viewer', `status` tinyint NOT NULL DEFAULT '1', `token_version` int NOT NULL DEFAULT '0', `created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有个细节值得说:display_name和username分开,是因为图库里很多地方要显示昵称,而登录名属于敏感信息,不宜暴露在其他协作者面前。哪怕内部团队无所谓,养成字段分离的习惯,后面做外部分享时会省事很多。

2.2 登录接口的完整流程

登录接口大致逻辑如下:

  1. 接收用户名和密码。
  2. 根据用户名查用户表。
  3. 如果用户不存在或status为禁用,直接返回统一错误。
  4. 用bcrypt校验密码哈希。
  5. 校验通过后,生成JWT。Payload里至少放用户ID、角色、Token版本号。
  6. 把JWT返回给前端。

这里有几个容易忽略的细节。第一,登录失败的信息不要区分太细,统一返回“用户名或密码错误”,防止攻击者用接口探活账号。第二,登录接口要做频率限制,图库平台是内部协作工具,一天试错三次五次算正常,但一分钟内连续十几次显然不对劲,直接限流。

生成JWT时,我给Token加了30分钟的过期时间。图库场景里,设计师可能把页面开着半天,30分钟太短会频繁踢人。不过没关系,配套了刷新Token机制:访问时如果Token过期了,前端拿refresh_token去换新的access_token。

2.3 Token的生命周期管理

完整生命周期:

  • 登录成功:发放access_token(30分钟过期) + refresh_token(7天过期,存数据库或Redis)。
  • 正常访问:请求带上access_token。
  • access_token过期:前端用refresh_token换新access_token。
  • 退出登录:把refresh_token作废,同时给用户表的token_version加1,让旧access_token立刻失效。

这里最需要解释的是token_version这个操作。纯JWT是没法主动失效的,但通过版本号可以做到。我签发Token时把当前用户的token_version写进Payload,鉴权中间件每次解析完Token后,拿解析出的版本号和数据库里的当前版本号比对。如果不一致,说明Token是旧的,直接拒绝。这样一来,修改密码、封禁用户、强制退出等操作就都好办了。

3. 鉴权的边界与实现

3.1 授权模型:先分清资源和操作

图库平台的资源和操作可以这样划分:

资源层级:

  • 图库(Library):最顶层,相当于一个项目空间。
  • 相册(Album):图库里的分组。
  • 图片(Image):最小单位。

操作:

  • 查看(read)
  • 上传(create)
  • 编辑(update):包括改标签、移动相册、覆盖文件。
  • 删除(delete)
  • 审核(approve):某些场景下新素材需要审核后才能公开可见。

我采用的授权模型是RBAC加资源归属。角色分四类:

  • 管理员:可以管理所有图库,包括删除、改成员、配置。
  • 编辑者:可以上传、编辑、删除自己创建的素材。
  • 协作者:可以上传,但修改和删除受限制(例如只能改自己的评论)。
  • 访客:只能查看。

单一RBAC解决不了“谁能删这张图”的问题,因为删除权限通常不光看角色,还要看资源归属。所以我在中间件里做了两层校验:第一层是角色门槛,第二层是资源归属判断。

3.2 鉴权中间件的实现要点

这个项目用的是Spring Boot的拦截器,其实换成Go的中间件、NestJS的Guard,思路完全一致。核心流程:

  1. 从Authorization头取Token。
  2. 解析Token,校验签名和过期时间。
  3. 把用户ID、角色等信息放入请求上下文。
  4. 放行到业务层,业务层再做细粒度检查。

但这里有一个很多新手会漏掉的问题:Token要不要在中间件里查库?如果每次都查库,性能会有损耗;如果完全不查库,用户被禁用之后,在Token过期前依然可以访问。我的做法是:中间件里只做签名和过期校验,不查库;但遇到写操作时,业务层会查一次用户状态。读操作对实时性要求不高,用无状态校验就够了。

拦截器大致结构如下:

public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new UnauthorizedException("缺少访问令牌"); } Claims claims = JwtUtil.parse(token); if (claims == null) { throw new UnauthorizedException("令牌无效或已过期"); } // 把用户信息放入线程上下文或Request Attribute request.setAttribute("userId", claims.get("userId")); request.setAttribute("userRole", claims.get("role")); return true; } }

鉴权注解可以这样设计:

@RequirePermission(resource = "library", action = "delete") public void deleteLibrary(Long libraryId) { // 业务逻辑 }

用AOP切面读取注解,先判断角色门槛,再通过切面注入的用户ID判断资源归属。这样业务方法里就完全不用写权限判断代码,逻辑清晰,也方便测试。

3.3 服务端校验与“鉴权绕过”问题

聊到鉴权,行业里经常有人提到“鉴权绕过”这个词。说实话,绝大多数被绕过的案例,问题都不出在加密算法,而是出在“该校验的地方没校验”。

比如前端只隐藏了删除按钮,但后端接口照样接受所有人调用;比如某些接口没走全局拦截器,在新加接口时忘了加权限注解;再比如直接把用户提供的角色明文放在请求体里,服务端完全没有从Token里取值。这些都是典型的漏洞来源。

我在图库项目里专门做了一个防御习惯:任何写操作的接口,都必须显式声明需要的权限注解。图库新加接口的时候,凡是涉及资源写操作的,都要通过切面校验来兜底。这样哪怕业务代码写得再烂,也有一层底线。

4. 实战中的问题排查与记录

4.1 从其他登录场景里看到的共性问题

搜资料的时候看到“威纶通触摸屏怎么使用宏进行用户登录”,挺有意思的。威纶通的宏在HMI里常用来做用户登录逻辑,比如在窗口打开时用宏指令读取用户名和密码输入框,调用系统函数完成用户校验,再根据用户等级控制画面切换。这个思路和图库平台其实完全一致,只是运行环境从浏览器变成了工控屏。多端登录鉴权的核心原则是通用的:把身份验证从业务界面中独立出来,让所有终端都走同一套接口和校验逻辑。

还有“nacos开启鉴权”也值得一记。如果是用Nacos做配置中心的微服务架构,Nacos默认不带鉴权,公网部署很容易被别人直接拉走配置。开启鉴权后,客户端需要带用户名密码。这属于基础设施级的鉴权,和业务登录是两个层次。图库平台如果上了微服务,这步一定要做。

4.2 客户端Key绑定类问题

另一个经典场景是“百度地图改包名后鉴权失败”。这类问题的本质很典型:鉴权Key和应用的包名/签名是绑定的。你在开放平台申请Key的时候填了包名和签名指纹,改包名之后,Key和包名对不上,鉴权自然就失败。这在图库平台上看其实是一个很好的“客户端可信度”反面案例——它提醒我们:客户端这边做的任何鉴权,本质上都是可以被改的,好的做法是把关键鉴权放到服务端。

4.3 登录后状态异常类问题

“域用户登录的时候变成temp了”这个热词我也关注到了。Windows域环境下,用户登录后如果配置文件加载失败,系统可能创建临时配置文件,用户的桌面、文档全都不见了,看起来像“变成了temp”。排查思路一般是:检查注册表ProfileList下的用户SID项,看看ProfileImagePath指向的路径是否存在,如果路径不对或权限有问题,就会触发临时配置。这个场景虽然不是图库项目的核心,但值得记上一笔:登录鉴权不只是Web的事,在桌面域环境里同样有各种“登录后状态异常”的问题。

4.4 图库项目实际测试记录

第三天下午我做了几组简单测试,主要验证这几个场景:

  1. 正常登录:正确的用户名密码,返回Token,随后调用获取图库列表接口,200正常。
  2. 错误密码:返回“用户名或密码错误”,连续错误5次后触发限流。
  3. 游客访问受保护接口:不带Token,返回401。
  4. 过期Token:手动把Token的过期时间改成1秒,请求返回401,前端自动跳转登录页。
  5. 删除权限校验:用一个访客账号调删除图库接口,返回403,同时日志里能看到切面拦截记录。

测试结果基本符合预期,唯一发现的小问题是:限流策略最开始用的是单机内存计数器,如果后续部署多实例,单个实例各自计数,效果会打折。这个留给后续接Redis时优化。

4.5 排查思路的通用清单

经过这一天的折腾,我整理了一份通用排查清单,遇到登录鉴权问题可以按顺序自查:

现象优先排查项
登录成功后请求仍401Token是否放进了Authorization头,Bearer前缀是否完整
部分接口能访问,部分不能接口是否遗漏了权限注解,是否被切面拦截覆盖
改密码后旧Token还能用是否维护了token_version,鉴权时是否比对当前版本号
Token过期时间到了仍在用客户端是否忽略了过期时间,是否有刷新逻辑
删除操作权限错乱角色判断正确之外,是否做了资源归属判断
多设备同时登录异常登录时是否把旧Token顶掉,是否有必要踢人下线

5. 第三天做完后的几个体会

最后分享一点个人经验。这个图库项目我踩过最深的坑,不是技术不会写,而是设计时把登录和权限混着搞。上午刚决定拆开做的时候,还觉得是不是过度设计了,下午写完拦截器和切面之后才发现,分开之后业务代码干净得多。写业务接口的时候完全不用关心身份怎么来,只用关心当前用户够不够格做这件事。

还有一个小技巧值得说:数据库里用户表的token_version字段,加上JWT的sub(用户ID),其实就能实现大部分会话控制需求。不一定非要上Spring Session或者引入Redis做黑名单,前期团队规模不大,这个方案够用且简单。等图库用户量真的大了,再迁移到Redis存refresh_token也来得及,接口层面不用大改。

鉴权这东西,看着枯燥,但它决定了平台能不能安心给团队用。第三天把这一块理顺,后面写图库上传、协作标注、分享链接的时候,就能只盯着业务逻辑了。如果这篇对你有点用,或者你自己也遇到过更奇葩的登录鉴权问题,欢迎交流。

返回列表