防火门 东莞网站建设,做网站优化推广多少钱,获取网站缩略图,如何制作效果图Sa-Token介绍 Sa-Token 是一个轻量级Java 权限认证框架#xff0c;主要解决#xff1a;登录认证、权限认证#xff08;RBAC#xff0c;全称是基于角色的权限认证#xff0c;主要是定义两个#xff0c;身份和权限#xff09;、单点登录、OAuth2.0、分布式Session会话、微…Sa-Token介绍Sa-Token 是一个轻量级Java 权限认证框架主要解决登录认证、权限认证RBAC全称是基于角色的权限认证主要是定义两个身份和权限、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权等一系列权限相关问题。要理解 Sa-Token 的 Session 模型区别于 Servlet 的 HttpSessionSa-Token 维护了自己的 Session共有3种Session 创建时机1、Account-session指的是框架为每个 账号 id 分配的 Session。是分配给账号id的而不是分配给指定客户端的也就是说在 PC、APP 上登录的同一账号所得到的 Session 也是同一个所以两端可以非常轻松的同步数据。2、Token-session指的是框架为每个 token 分配的 Session。不同的设备端哪怕登录了同一账号只要它们得到的 token 不一致它们对应的 Token-session 就不一致这就为我们不同端的独立数据读写提供了支持。比如实现“指定客户端超过两小时无操作就自动下线如果两小时内有操作就再续期两小时直到新的两小时无操作”。3、custom-session指的是以一个特定的值作为 Sessionld来分配的 Session。不依赖特定的账号id 或者 token当成一个 Map 去使用即可比如可以为一个团队的用户指定相同的 Sessionld让一个团队最多 N 个用户同时在线等。强烈建议仔细阅读学习一遍Sa-Token文档。同端登录冲突检测在多用户系统中如电商平台、社交应用用户的账户安全和系统的正常使用至关重要。如果同一个账户同时在多个设备上登录可能由于之前在别的地方临时登录后忘记退出账号导致数据泄露或账户被滥用等问题。此外还有可能会导致数据不一致问题例如重复提交订单、聊天记录不同步等。为了防止这些情况我们的系统需要能够实时监控和检测同一账户在多个设备上的登录情况在检测到冲突时及时通知用户并采取相应的安全措施(如强制下线或警告)。账号冲突检测策略常见业务上有3种冲突检测策略1)单点登录模式同一时间只允许同一账户在一个设备上登录。即每次新设备登录时检测当前账户是否已经在其他设备上登录若已登录则将其他设备强制下线。一般应用于企业内部系统或包含敏感数据的系统中。2)多设备登录限制允许同一账户在多个设备上登录但需限制设备数量(如最多两台)。即每次登录时检测当前账户已登录设备数量若超过限制系统阻止登录或强制最早登录设备下线。这种模式常见于视频网站、买断制/订阅制软件。一个软件激活码只能同时在X台设备使用也是类似的机制。3)同设备类型限制允许同一账户在不同设备类型(如手机和 PC)上同时登录但同一类型设备只能登录一个。例如用户可同时在电脑和 iPad 上使用我们的平台但不能在两台手机上登录在线。(QQ 就是这样)要想实现这种策略需要记录账户登录设备的类型每次登录时检查是否有同类设备已登录如果有则强制下线相同设备类型的旧登录。这种策略常应用于一些需兼顾多端体验的应用。比如刷题平台用户经常有 iPad 和电脑端同时刷题的需求因此我们采用第3种策略 同设备类型限制(同端互斥登录)。实现思路如何实现同端互斥登录呢?1.用户登录时获取当前设备信息(通过 User-Agent 获取)2.将用户登录信息与设备信息一起保存(本地或三方缓存中)3.用户登录时判断同设备是否已经登录(本地或缓存中是否已存在)4.如果检测到冲突可以直接顶号(将前一个设备下线也就是移除登录态)其实自主实现这个需求也并不难但涉及到登录这样的核心业务场景经验不足的情况下更建议使用一些成熟的第三方框架。比如轻量级 Java 权限认证框架 Sa-Token 内置了 同端互斥登录功能可以更快更稳地实现。首先我们引入Sa-Token的依赖!-- Sa-Token 权限认证 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-spring-boot-starter/artifactId version1.39.0/version /dependency接着我们编写application.yml配置定制sa-token框架行为如登录态过期时间、不活跃自动下线等重点关注 is-concurrent 需要设置为 false这样才能实现同端冲突下线# Sa-Token 配置 sa-token: # token 名称同时也是 cookie 名称 token-name: zhimianguan # token 有效期单位秒 默认30天-1 代表永久有效 timeout: 2592000 # token 最低活跃频率单位秒如果 token 超过此时间没有访问系统就会被冻结默认-1 代表不限制永不冻结 active-timeout: -1 # 是否允许同一账号多地同时登录为 true 时允许一起登录, 为 false 时新登录挤掉旧登录 is-concurrent: false # 在多人登录同一账号时是否共用一个 token 为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token is-share: true # token 风格默认可取值uuid、simple-uuid、random-32、random-64、random-128、tik token-style: uuid # 是否输出操作日志 is-log: true因为我们现在的项目里是使用 AOP 自定义权限校验注解来实现的登录认证和权限认证所以我们使用注解鉴权的模式来实现同端登录互斥。在根包下新建一个satoken包然后写一个satoken全局拦截器/** * 配置 Sa-Token 全局拦截器为了支持注解鉴权模式 */ Configuration public class SaTokenConfigure implements WebMvcConfigurer { // 注册 Sa-Token 拦截器打开注解式鉴权功能 Override public void addInterceptors(InterceptorRegistry registry) { // 注册 Sa-Token 拦截器打开注解式鉴权功能 registry.addInterceptor(new SaInterceptor()).addPathPatterns(/**); } }有了sa-token鉴权拦截器如果项目原本的aop包下有自己写AOP定义的AuthInterceptor权限拦截器可以把里面的两个注解注释掉就不会被springboot扫描执行了然后可以用sa-token的鉴权拦截器//Aspect //Component接着我们就需要去定义我们有哪些角色有哪些权限需要去让sa-token自动校验需要在satoken包下新建一个实现类去实现StpInterface接口/** * 自定义权限验证接口StpInterface实现类扩展 */ Component public class StpInterfaceImpl implements StpInterface { /** * 返回一个账号所拥有的权限码集合目前没用 */ Override public ListString getPermissionList(Object loginId, String s) { return new ArrayList(); } /** * 返回一个账号所拥有的角色标识集合权限与角色可分开校验 */ Override public ListString getRoleList(Object loginId, String s) { //从当前登录用户信息中获取角色 User user (User)StpUtil.getSessionByLoginId(loginId).get(USER_LOGIN_STATE); return Collections.singletonList(user.getUserRole()); } }接着我们要实现同端登录冲突那肯定需要新建一个设备信息获取工具类在satoken包下新建一个DeviceUtils类/** * 设备工具类 */ public class DeviceUtils { /** * 根据请求获取设备信息 **/ public static String getRequestDevice(HttpServletRequest request) { String userAgentStr request.getHeader(Header.USER_AGENT.toString()); // 使用 Hutool 解析 UserAgent UserAgent userAgent UserAgentUtil.parse(userAgentStr); ThrowUtils.throwIf(userAgent null, ErrorCode.OPERATION_ERROR, 非法请求); // 默认值是 PC String device pc; // 是否为小程序 if (isMiniProgram(userAgentStr)) { device miniProgram; } else if (isPad(userAgentStr)) { // 是否为 Pad device pad; } else if (userAgent.isMobile()) { // 是否为手机 device mobile; } return device; } /** * 判断是否是小程序 * 一般通过 User-Agent 字符串中的 MicroMessenger 来判断是否是微信小程序 **/ private static boolean isMiniProgram(String userAgentStr) { // 判断 User-Agent 是否包含 MicroMessenger 表示是微信环境 return StrUtil.containsIgnoreCase(userAgentStr, MicroMessenger) StrUtil.containsIgnoreCase(userAgentStr, MiniProgram); } /** * 判断是否为平板设备 * 支持 iOS如 iPad和 Android 平板的检测 **/ private static boolean isPad(String userAgentStr) { // 检查 iPad 的 User-Agent 标志 boolean isIpad StrUtil.containsIgnoreCase(userAgentStr, iPad); // 检查 Android 平板包含 Android 且不包含 Mobile boolean isAndroidTablet StrUtil.containsIgnoreCase(userAgentStr, Android) !StrUtil.containsIgnoreCase(userAgentStr, Mobile); // 如果是 iPad 或 Android 平板则返回 true return isIpad || isAndroidTablet; } }然后来修改一下登录接口运用同端登录冲突在UserServiceImpl的userLogin中给sa-token设置登录态保存登录用户信息// 3. 记录用户的登录态 // request.getSession().setAttribute(USER_LOGIN_STATE, user); StpUtil.login(user.getId(), DeviceUtils.getRequestDevice(request)); StpUtil.getSession().set(USER_LOGIN_STATE, user);补充说明user.getId()就是登录成功的用户 id可以在 StpInterfaceImpl 中通过方法参数 loginld 获取到。注意 Stputil.getsession()是 SaSession ,它是获取当前账号id的Account-Session必须登录后才能调用与 HttpSession 没任何关系 SaSession 是 Sa-Token 提供的会话中专业的数据缓存组件通过 Sasession 我们可以很方便的缓存一些高频读写数据(比如登录用户信息)提高程序性能。接着我们来修改获取用户信息的逻辑将UserServiceImpl的getLoginUser的原本通过request.getSession()获取登录用户的id改为从Sa-Token中获取这边我们采用是通过Sa-Token先获取id然后再根据id去数据库查到用户的信息这样保证了数据的一致性如果用户信息修改不频繁的话可以不查数据库你还可以直接从Sa-Token的Session中获取之前保存的用户登录态/** * 获取当前登录用户 * * param request * return */ Override public User getLoginUser(HttpServletRequest request) { // 先判断是否已登录 Object loginUserId StpUtil.getLoginIdDefaultNull(); if (loginUserId null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } // 从数据库查询追求性能的话可以注释直接走缓存 User currentUser this.getById((String) loginUserId); if (currentUser null) { throw new BusinessException(ErrorCode.NOT_LOGIN_ERROR); } return currentUser; }然后原本从Servlet Session中获取的登录态的地方都要修改为从Sa-Token中获取UserServiceImpl中的isAdmin方法/** * 是否为管理员 * * param request * return */ public boolean isAdmin(HttpServletRequest request) { // 仅管理员可查询 // 基于 Sa-Token 改造 Object userObj StpUtil.getSession().get(USER_LOGIN_STATE); // Object userObj request.getSession().getAttribute(USER_LOGIN_STATE); User user (User) userObj; return isAdmin(user); }如果还有用户注销功能也修改一下/** * 用户注销 * * param request */ Override public boolean userLogout(HttpServletRequest request) { StpUtil.checkLogin(); // 移除登录态 StpUtil.logout(); return true; }接着就是前面说到的如果有自己用AOP定义我们需要将controller里的所有原本使用AuthCheck(mustRole UserConstant.ADMIN_ROLE)权限注解修改为Sa-Token的权限注解SaCheckRole(UserConstant.ADMIN_ROLE)使用ctrlshiftR快捷键全局替换。SaCheckRole(UserConstant.ADMIN_ROLE)最后我们来测试一下看看是否成功实现同端登录冲突。经过测试发现如果不做任何处理接口返回的无权限和未登录的报错如下{ code: 50000, data: null, message: 系统错误 }因此我们还可以给Sa-Token增加一个全局异常处理器使效果更直观而不是接口报错返回的是系统错误在exception包下的GlobalExceptionHandler类里添加以下异常处理//Sa-Token全局异常处理器 ExceptionHandler(NotRoleException.class) public BaseResponse? notRoleExceptionHandler(RuntimeException e) { log.error(NotRoleException, e); return ResultUtils.error(ErrorCode.NO_AUTH_ERROR, 无权限); } ExceptionHandler(NotLoginException.class) public BaseResponse? notLoginExceptionHandler(RuntimeException e) { log.error(NotLoginException, e); return ResultUtils.error(ErrorCode.NOT_LOGIN_ERROR, 未登录); }经过测试发现我们项目重启后会丢失之前的token因为它默认是将数据保存在内存中而且目前无法在分布式环境中共享数据所以要解决这两个问题我们可以将Sa-Token集成Redis可以参考Sa-Token官方文档来集成Redis集成redis之后不需要我们手动保存数据Sa-Token框架会自动帮我们保存步骤如下首先引入依赖!-- Sa-Token 整合 Redis 使用 jackson 序列化方式 -- dependency groupIdcn.dev33/groupId artifactIdsa-token-redis-jackson/artifactId version1.39.0/version /dependency !-- 提供Redis连接池 -- dependency groupIdorg.apache.commons/groupId artifactIdcommons-pool2/artifactId /dependency然后在pom.xml中添加redis配置# Redis 配置 redis: # Redis数据库索引默认为0 database: 1 # Redis服务器地址 host: 127.0.0.1 # Redis服务器连接端口 port: 6379 # Redis服务器连接密码默认为空 # password: # 连接超时时间 timeout: 10s lettuce: pool: # 连接池最大连接数 max-active: 200 # 连接池最大阻塞等待时间使用负值表示没有限制 max-wait: -1ms # 连接池中的最大空闲连接 max-idle: 10 # 连接池中的最小空闲连接 min-idle: 0我们还可以补充实现同端登录冲突时被踢下线的用户前端会收到通知实现思路:1、自定义“登录冲突被踢下线异常比如状态码 401102、用户在一个设备A登录后如果在另一个设备B登录时系统判断该用户已经在其他设备上登录过了会移除设备A的登录态并且在 Redis 里记录一条设备A的 sessionld 的冲突标识之后用户在设备 A 请求任何一个接口(除登录接口)都会返回一个冲突的错误响应消息{ code: 40110, message: 用户已经在其他设备登录 }3、然后就可以移除在 Redis 里的冲突标识。之后请求任何接口都能正常响应如果需要登录会和之前一样返回未登录。4、前端全局响应拦截器中捕获该异常并弹出消息通知框即可。相比轮询或者 WebSocket这种懒加载惰性请求的方案更轻量要是用户一天都不用电脑就不用每隔一段时间就轮询浪费性能能够减少服务器的压力。我们还需要去学习了解单点登录和 OAuth2可以直接阅读 Sa-Token 的官方文档学习介绍地非常详细:SSO 单点登录:https://sa-token.cc/doc.html#/sso/readmeOAuth2:https://sa-token.cc/doc.html#/oauth2/readme