ARTICLE DETAIL

资讯详情

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

Sa-Token 多账号体系认证完全指南:一套系统轻松隔离 User 与 Admin 两套账号鉴权

Sa-Token 多账号体系认证完全指南:一套系统轻松隔离 User 与 Admin 两套账号鉴权 Sa-Token 多账号体系认证完全指南一套系统轻松隔离 User 与 Admin 两套账号鉴权【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token导读在电商、后台管理等实际业务中一个项目常常同时存在user表前台用户与admin表后台管理员等多套账号体系。本指南基于 Sa-Token 开源权限认证框架系统讲解多账号体系认证Multiple Account Authentication的核心模型与全套落地方案——从LoginType机制原理、StpUserUtil复制式扩展、StpKit门面式管理到注解鉴权指定账号体系、注解合并、同端多登录、独立配置、拦截器混合鉴权等进阶技巧并给出必须规避的运行时修改 LoginType陷阱。读完你将能在一个项目中优雅地隔离并管理任意多套账号的登录、权限与会话。1、需求场景为什么单一 StpUtil 撑不起两套账号在真实业务中一个项目多套账号体系是常态。典型如电商系统user表面向 C 端消费者负责前台登录、下单、个人中心admin表面向 B 端运营负责后台管理、商品审核、数据看板。如果两套账号都使用StpUtil的 API 进行登录鉴权势必发生逻辑冲突因为 Sa-Token 的默认工具类StpUtil只维护一套账号体系的会话状态。在 Sa-Token 中这个问题的模型就叫作多账号体系认证。要解决它必须有一个合理的机制把两套账号的授权彻底区分开让它们互不干扰——这也正是本文后续全部内容的目标。2、演进思路为什么加前缀不是好方案假设user表和admin表都恰好存在一个id 10001的账号它们的登录代码长得一模一样StpUtil.login(10001);问题随之而来当执行StpUtil.getLoginId()时拿到的10001究竟是 User 用户还是 Admin 用户一个朴素的想法是给账号 id 加固定前缀来区分StpUtil.login(User_ 10001); // 用户体系登录 StpUtil.login(Admin_ 10001); // 管理员体系登录这样做确实能区分但代价是每次StpUtil.getLoginId()取回账号 id 时都需要再裁剪掉前缀才能还原真实 id。一增一减让业务代码变得无比啰嗦且极易出错。因此我们需要一个从框架层面原生支持的、更优雅的解决方案——即本文核心的LoginType账号体系机制。3、解决方案StpUtil 只是 StpLogic 的门面阅读 Sa-Token 核心源码可以发现一个关键事实StpUtil本身几乎没有任何业务逻辑它唯一的工作就是把成员变量stpLogic的各个 API 包装转发一下。见 StpUtil.javapublic class StpUtil { // 多账号体系下的类型标识 public static final String TYPE login; // 底层使用的 StpLogic 对象 public static StpLogic stpLogic new StpLogic(TYPE); // 下面所有静态方法都是对 stpLogic 的转发…… public static void login(Object id) { stpLogic.login(id); } public static boolean isLogin() { return stpLogic.isLogin(); } // ... }而 StpLogic.java 中loginType字段正是账号体系的唯一标识构造时即被固定public class StpLogic { // 账号类型标识多账号体系时用此值区分具体要校验的是哪套用户比如login、user、admin public String loginType; // 初始化 StpLogic, 并指定账号类型 public StpLogic(String loginType) { setLoginType(loginType); } // ... }这套门面 底层逻辑的架构带来两个巨大好处StpLogic 类的所有函数都可以被重写可按需扩展甚至改造 token 拼接规则在构造方法时随意传入一个不同的loginType就能再造出一套全新的账号登录体系且各体系之间完全隔离。同时每个StpLogic在构造/重置时都会注册进全局管理器 SaManager后续可通过SaManager.getStpLogic(type)按类型名随时取回这正是注解鉴权能按 type 找到对应体系的底层支撑。4、操作示例复制式扩展出 StpUserUtil最简单的落地方式是仿照StpUtil复制出专属工具类。假设我们规定原生StpUtil只负责admin账号认证user账号则新建一个StpUserUtil。操作只需三步新建权限认证类例如StpUserUtil.java将StpUtil.java的全部代码复制粘贴到StpUserUtil.java更改其LoginType标识public class StpUserUtil { /** * 账号体系标识 */ public static final String TYPE user; // 将 LoginType 从login改为user // 其它代码 ... }仓库 demo 中已提供完整成品样例StpUserUtil.java其内部结构与StpUtil完全同构仅将TYPE改为user、stpLogic改为new StpLogic(TYPE)。4、接下来就可以像调用StpUtil一样调用StpUserUtil两套账号的认证逻辑完全隔离、互不影响// 凡是在 StpUtil 上有的方法都可以在 StpUserUtil 上调用 StpUserUtil.login(10001); // 在当前会话以 10001 账号进行 User 体系登录 StpUserUtil.checkLogin(); // 校验当前账号是否以 User 身份登录 StpUserUtil.getSession(); // 获取当前 User 账号的 Access-Session 对象 StpUserUtil.checkPermission(xx); // 校验当前登录的 user 账号是否具有 xx 权限 // ...从源码看StpUserUtil.java 与StpUtil保持完全一致的 API 面登录、注销、会话、角色、权限、封禁、二级认证、身份切换等全部覆盖因此复制式扩展不会损失任何框架能力。5、Kit 模式用 StpKit 门面统一管理所有体系如果觉得复制粘贴整份源码不够优雅还有更轻量的方案建立一个StpKit.java门面类把项目中所有StpLogic引用集中声明、统一管理/** * StpLogic 门面类管理项目中所有的 StpLogic 账号体系 */ public class StpKit { /** * 默认原生会话对象 */ public static final StpLogic DEFAULT StpUtil.stpLogic; /** * Admin 会话对象管理 Admin 表所有账号的登录、权限认证 */ public static final StpLogic ADMIN new StpLogic(admin); /** * User 会话对象管理 User 表所有账号的登录、权限认证 */ public static final StpLogic USER new StpLogic(user); /** * XX 会话对象项目中有多少套账号表就声明几个 StpLogic 会话对象 */ public static final StpLogic XXX new StpLogic(xx); }在需要登录、权限认证的地方直接通过StpKit门面调用// 在当前会话进行 Admin 账号登录 StpKit.ADMIN.login(10001); // 在当前会话进行 User 账号登录 StpKit.USER.login(10001); // 检测当前会话是否以 Admin 账号登录并具有 article:add 权限 StpKit.ADMIN.checkPermission(article:add); // 检测当前会话是否以 User 账号登录并通过了二级认证 StpKit.USER.checkSafe(); // 获取当前 User 会话的 Session 对象并进行写值操作 StpKit.USER.getSession().set(name, zhang);Kit 模式的本质是直接持有StpLogic实例比复制工具类更省代码也便于集中查看项目当前注册了哪些账号体系。6、多账户模式下的注解鉴权用 type 指定体系框架默认的注解鉴权如SaCheckLogin只针对原生StpUtil进行鉴权。例如在一个方法上标注SaCheckLogin它只会放行通过StpUtil.login(id)登录的会话而通过StpUserUtil.login(id)登录的会话始终不会通过校验。如何告诉注解要鉴别哪套账号体系只需指定注解的type属性即可// 通过 type 属性指定此注解校验的是自定义的 StpUserUtil而不是原生 StpUtil SaCheckLogin(type StpUserUtil.TYPE) RequestMapping(info) public String info() { return 查询用户信息; }从源码可以验证这套机制的实现链路SaCheckLogin.java 中定义了String type() default 默认空串代表使用原生StpUtil体系其处理器 SaCheckLoginHandler.java 会调用SaManager.getStpLogic(type, false)取出对应体系再执行stpLogic.checkLogin()——type 不同取出的StpLogic就不同鉴权对象自然被精准隔离。同样SaCheckRole(xxx)、SaCheckPermission(xxx)也支持通过type属性指定账号体系type默认为代表使用原生StpUtil账号体系。7、注解合并用 Spring 注解处理器简化重复代码有同学反馈虽然可以用SaCheckLogin(type user)指定账号类型但几十上百个注解都要带上这个参数依然繁琐、不够优雅。有没有更简单的方案我们期待一种「注解继承/合并」的能力自定义一个注解内部标注SaCheckLogin(type user)然后在方法上标注这个自定义注解效果等同于直接标注SaCheckLogin(type user)。遗憾的是JDK 默认的注解处理器并不提供这种「注解继承/合并」能力。但好消息是可以利用 Spring 的注解处理器达到同样目的。具体分三步Step 1重写 Sa-Token 默认的注解处理器Configuration public class SaTokenConfigure { PostConstruct public void rewriteSaStrategy() { // 重写 Sa-Token 的注解处理器增加注解合并功能 SaAnnotationStrategy.instance.getAnnotation (element, annotationClass) - { return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass); }; } }Step 2自定义一个合并注解/** * 登录认证(User版)只有登录之后才能进入该方法 * p 可标注在函数、类上效果等同于标注在此类的所有方法上 */ SaCheckLogin(type user) Retention(RetentionPolicy.RUNTIME) Target({ ElementType.METHOD, ElementType.TYPE}) public interface SaUserCheckLogin { }Step 3使用自定义注解// 使用 SaUserCheckLogin 的效果等同于使用SaCheckLogin(type user) SaUserCheckLogin RequestMapping(info) public String info() { return 查询用户信息; }SaCheckRole(xxx)、SaCheckPermission(xxx)同理。仓库 demo 中提供了完整的注解合并示例见 merge_annotation 目录内含SaUserCheckLogin、SaUserCheckPermission、SaUserCheckRole、SaUserCheckSafe四个合并注解。进阶提示除注解合并外Sa-Token 还支持完全自定义注解的方案自定义注解 自定义 Handler配合SaAnnotationStrategy注册可参考文档 自定义注解demo 中的 custom_annotation 目录 即提供了CheckAccount自定义注解及其SaUserCheckLoginHandler等完整实现。8、同端多登录重写 splicingKeyTokenName 避免 token 覆盖假设不仅要在一个后台同时集成两套账号还要支持在一个客户端同时登录两套账号业务场景举例一个 APP 里同时登录商家账号和用户账号。如果不做任何特殊处理客户端会发生token 覆盖新登录的 token 会覆盖旧登录的 token导致旧登录失效。具体表现为在浏览器先登录商家账号再登录用户账号商家账号的登录态就自动失效了。原因很好理解两套体系默认使用同一个 token 名称默认satoken前端 Cookie/Header 里同一个名字只能保存一个值。解决办法更改StpUserUtil的TokenName。通过匿名子类重写stpLogic的splicingKeyTokenName()方法返回与StpUtil不同的 token 名称即可避免冲突public class StpUserUtil { // 使用匿名子类 重写 stpLogic 对象的一些方法 public static StpLogic stpLogic new StpLogic(user) { // 重写 StpLogic 类下的 splicingKeyTokenName 函数返回一个与 StpUtil 不同的 token 名称, 防止冲突 Override public String splicingKeyTokenName() { return super.splicingKeyTokenName() -user; } // 同理你可以按需重写一些其它方法 ... }; // ... }再次调用StpUserUtil.login(10001)进行登录授权时token 的名称将不再是satoken而是重写后的satoken-user这样客户端就不会再发生 token 相互覆盖了。从源码看StpLogic.splicingKeyTokenName() 正是 token 名称的最终拼接出口getTokenName()也委托给它见 StpLogic.getTokenName()因此重写它即可从根上改变某套体系的 token 存取 key属于官方支持的标准扩展点。9、不同体系使用不同的 SaTokenConfig 配置如果自定义的StpUserUtil需要一套与StpUtil完全不同的配置如不同的 token 名称、超时时间、token 风格可通过StpLogic.setConfig()为各体系注入独立的SaTokenConfig对象Configuration public class SaTokenConfigure { PostConstruct public void setSaTokenConfig() { // 设定 StpUtil 使用的 SaTokenConfig 配置参数对象 SaTokenConfig config1 new SaTokenConfig(); config1.setTokenName(satoken1); config1.setTimeout(1000); config1.setTokenStyle(random-64); // 更多设置 ... StpUtil.stpLogic.setConfig(config1); // 设定 StpUserUtil 使用的 SaTokenConfig 配置参数对象 SaTokenConfig config2 new SaTokenConfig(); config2.setTokenName(satoken2); config2.setTimeout(2000); config2.setTokenStyle(tik); // 更多设置 ... StpUserUtil.stpLogic.setConfig(config2); } }从 StpLogic 源码可以看到配置的解析逻辑setConfig(config)为当前StpLogic单独写入配置getConfig()返回该体系自己的配置未设置时返回nullgetConfigOrGlobal()优先返回体系级配置若为空则回退到全局配置SaManager.getConfig()。也就是说各体系配置以局部优先、全局兜底为原则你既可以为每个体系单独定制也可以让某些体系直接继承全局默认配置。10、多账号体系混合鉴权在 SaInterceptor 拦截器中灵活编排多账号体系下如何在SaInterceptor拦截器中给一个接口做登录鉴权这个问题主要由业务需求决定。以后台 Admin 账号 前台 User 账号为例借助SaRouter.match().check()可以针对不同接口、不同体系做任意组合编排// 注册 Sa-Token 拦截器 Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new SaInterceptor(handle - { // 如果这个接口要求客户端登录了后台 Admin 账号才能访问 SaRouter.match(/art/getInfo).check(r - StpUtil.checkLogin()); // 如果这个接口要求客户端登录了前台 User 账号才能访问 SaRouter.match(/art/getInfo).check(r - StpUserUtil.checkLogin()); // 如果这个接口要求客户端同时登录 Admin 和 User 账号才能访问 SaRouter.match(/art/getInfo).check(r - { StpUtil.checkLogin(); StpUserUtil.checkLogin(); }); // 如果这个接口要求客户端登录 Admin 和 User 账号任意一个就能访问 SaRouter.match(/art/getInfo).check(r - { if(StpUtil.isLogin() false StpUserUtil.isLogin() false) { throw new SaTokenException(请登录后再访问接口); } }); })).addPathPatterns(/**); }这种写法的优势在于路由匹配与鉴权策略完全解耦同一套拦截器可以为不同接口分配不同的账号体系要求甚至支持同时登录两套任选一套登录等复合策略。11、在一个接口里判断当前是哪个体系的账号在登录如果接口需要区分当前到底是哪个体系的账号正在登录可以分别用两个体系的isLogin()去判断——哪个返回true就代表正在登录哪个体系RequestMapping(test) public SaResult test2() { String loginType ; if(StpUtil.isLogin()) { loginType StpUtil.getLoginType(); } if(StpUserUtil.isLogin()) { loginType StpUserUtil.getLoginType(); } System.out.println(当前登录的 loginType loginType); return SaResult.ok(); }请注意此处可能出现的两种边际情况务必在业务中考虑周全两个 if 均返回 false代表客户端在两个账号体系都没有登录未登录状态两个 if 均返回 true代表客户端在两个账号体系都登录了同端双登录状态。实际项目中应根据业务需求决定是报错、提示重新登录还是优先返回某一体系的loginType。12、重要注意点运行时不可更改 LoginType在社区答疑过程中发现有些同学会写出类似下列形式的代码StpUtil.login(10001); StpUtil.getStpLogic().setLoginType(user); StpUtil.getSession().set(name, zhangsan);这是一种错误写法。LoginType不可在运行时更改只能在项目启动时指定。从 StpLogic.setLoginType() 的源码注释可以明确看到官方警告注意此方法只能在项目启动时调用项目启动后不可动态更改 loginType。若在运行时修改可能造成线程安全问题和严重的逻辑问题。原因在于setLoginType不仅修改字段值还会同步执行SaManager.removeStpLogic(oldType)与SaManager.putStpLogic(this)即把该对象在全局 StpLogic 注册表中的索引键整体更换。运行期间一旦有并发请求正按旧 type 查找或使用该StpLogic就会产生错乱同时已经签发出去的 Session、token 中的loginType信息也已固化中途篡改会导致归属关系对不上引发严重的鉴权逻辑错误。正确姿势是在项目启动阶段如PostConstruct初始化配置中一次性指定好各体系的LoginType此后保持恒定。附本章完整代码示例多账号体系认证的完整可运行样例已内置在仓库 demo 中推荐结合阅读工具类成品StpUserUtil.javaUser 账号体系专属工具类注解合并示例merge_annotation 目录自定义注解示例custom_annotation 目录配置类示例SaTokenConfigure.java至此你已经掌握了 Sa-Token 多账号体系认证的完整知识链路从LoginType机制与StpLogic门面原理出发到复制式StpUserUtil与门面式StpKit两种落地模式再到注解type指定、注解合并、同端多登录、独立配置、拦截器混合鉴权与体系判断最后规避运行时改 LoginType的致命陷阱。这套方法论可以无缝扩展到任意数量的账号体系user、admin、merchant、operator……让复杂的多角色业务在一个项目中保持清晰、优雅、互不干扰。【免费下载链接】Sa-Token✨ 开源、免费、一站式 Java 权限认证框架让鉴权变得简单、优雅—— 登录认证、权限认证、分布式 Session 会话、微服务网关鉴权、SSO 单点登录、OAuth2.0 统一认证、jwt 集成、API Key 秘钥授权、API 参数签名项目地址: https://gitcode.com/GitHub_Trending/sa/Sa-Token创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表