ARTICLE DETAIL

资讯详情

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

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

从登录到鉴权:图库管理平台的Token、RBAC与中间件实战 做图库管理平台最怕什么怕的是功能做到一半发现连“谁能看、谁能传、谁能删”都说不清楚。这个智能协图云图库项目做到第三天正好卡在这个节骨眼上。今天这篇我不打算绕弯子直接拆解用户登录与鉴权这套东西——从需求设计、接口实现到中间件拦截再到我在实战里踩过的几个坑全都摊开讲一下。这个项目本质上是一个面向团队的协作图库图片素材多、角色杂管理员、设计师、访客如果不在登录和鉴权上把地基打牢后面加协作、加审核、加分享都会非常痛苦。所以第三天没有急着写业务接口先花了一整天把用户身份和访问控制理顺。这篇文章适合正在做类似平台的后端开发、全栈开发者以及对“登录鉴权怎么落地”有困惑的读者。1. 为什么“登录”和“鉴权”必须拆开设计1.1 两个概念的本质区别我见过太多项目把登录和鉴权混成一锅粥。登录解决的是“你是谁”鉴权解决的是“你能做什么”。这么说可能还是有点抽象我拿图库平台的场景举例登录用户提交用户名密码服务端校验通过后给用户发一张“通行证”也就是Token。这张证证明了“我是张三”。鉴权用户拿着张三的证去访问“删除图库”接口时服务端要检查“张三有没有删除图库的权限”。没有就直接拒绝哪怕张三确实是合法登录用户。注意这一步的关键在于鉴权一定是基于登录结果但绝不等于登录。把两者混在一起设计最典型的后果就是所有登录用户拥有同样的权限然后你就会陷入“加权限就写死if判断”的泥潭。1.2 一次完整请求的链路图库平台的典型请求链路是这样的用户浏览器输入账号密码POST到登录接口。登录接口校验密码生成Token返回给前端。前端把Token存起来后续每次请求都带上通常放在Authorization头里。后端中间件先解析Token确认用户身份再把请求交给具体业务处理器。业务处理器判断当前用户是否有权限操作该资源例如某个图库、某个相册。这里把第4步和第5步分开是有讲究的。第4步是认证第5步是授权。工程上认证通常由全局拦截器统一完成授权由业务层或细粒度拦截器完成。如果全局拦截器顺带把所有资源访问都做掉那资源之间的权限差异就很难表达。1.3 为什么选Token而不是Session第三天做技术选型的时候团队里也争论过一轮图库平台要不要引入SessionSession的方案很经典登录成功后服务端存一份会话记录给客户端一个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 / viewerstatustinyint1启用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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里有个细节值得说display_name和username分开是因为图库里很多地方要显示昵称而登录名属于敏感信息不宜暴露在其他协作者面前。哪怕内部团队无所谓养成字段分离的习惯后面做外部分享时会省事很多。2.2 登录接口的完整流程登录接口大致逻辑如下接收用户名和密码。根据用户名查用户表。如果用户不存在或status为禁用直接返回统一错误。用bcrypt校验密码哈希。校验通过后生成JWT。Payload里至少放用户ID、角色、Token版本号。把JWT返回给前端。这里有几个容易忽略的细节。第一登录失败的信息不要区分太细统一返回“用户名或密码错误”防止攻击者用接口探活账号。第二登录接口要做频率限制图库平台是内部协作工具一天试错三次五次算正常但一分钟内连续十几次显然不对劲直接限流。生成JWT时我给Token加了30分钟的过期时间。图库场景里设计师可能把页面开着半天30分钟太短会频繁踢人。不过没关系配套了刷新Token机制访问时如果Token过期了前端拿refresh_token去换新的access_token。2.3 Token的生命周期管理完整生命周期登录成功发放access_token30分钟过期 refresh_token7天过期存数据库或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思路完全一致。核心流程从Authorization头取Token。解析Token校验签名和过期时间。把用户ID、角色等信息放入请求上下文。放行到业务层业务层再做细粒度检查。但这里有一个很多新手会漏掉的问题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 图库项目实际测试记录第三天下午我做了几组简单测试主要验证这几个场景正常登录正确的用户名密码返回Token随后调用获取图库列表接口200正常。错误密码返回“用户名或密码错误”连续错误5次后触发限流。游客访问受保护接口不带Token返回401。过期Token手动把Token的过期时间改成1秒请求返回401前端自动跳转登录页。删除权限校验用一个访客账号调删除图库接口返回403同时日志里能看到切面拦截记录。测试结果基本符合预期唯一发现的小问题是限流策略最开始用的是单机内存计数器如果后续部署多实例单个实例各自计数效果会打折。这个留给后续接Redis时优化。4.5 排查思路的通用清单经过这一天的折腾我整理了一份通用排查清单遇到登录鉴权问题可以按顺序自查现象优先排查项登录成功后请求仍401Token是否放进了Authorization头Bearer前缀是否完整部分接口能访问部分不能接口是否遗漏了权限注解是否被切面拦截覆盖改密码后旧Token还能用是否维护了token_version鉴权时是否比对当前版本号Token过期时间到了仍在用客户端是否忽略了过期时间是否有刷新逻辑删除操作权限错乱角色判断正确之外是否做了资源归属判断多设备同时登录异常登录时是否把旧Token顶掉是否有必要踢人下线5. 第三天做完后的几个体会最后分享一点个人经验。这个图库项目我踩过最深的坑不是技术不会写而是设计时把登录和权限混着搞。上午刚决定拆开做的时候还觉得是不是过度设计了下午写完拦截器和切面之后才发现分开之后业务代码干净得多。写业务接口的时候完全不用关心身份怎么来只用关心当前用户够不够格做这件事。还有一个小技巧值得说数据库里用户表的token_version字段加上JWT的sub用户ID其实就能实现大部分会话控制需求。不一定非要上Spring Session或者引入Redis做黑名单前期团队规模不大这个方案够用且简单。等图库用户量真的大了再迁移到Redis存refresh_token也来得及接口层面不用大改。鉴权这东西看着枯燥但它决定了平台能不能安心给团队用。第三天把这一块理顺后面写图库上传、协作标注、分享链接的时候就能只盯着业务逻辑了。如果这篇对你有点用或者你自己也遇到过更奇葩的登录鉴权问题欢迎交流。
返回列表