类似于Security的认证授权的解决方案,但是使用起来非常方便,1.支持登录认证,授权权限验证,踢人;2.支持自定义Token,并且能够结合Redis完成前后端分离认证方案;3.支持单点登录(前端同域,后端同Redis+前端不同域,后端不同Redis+两者都不同)

1.demo所用依赖
cn.dev33 sa-token-spring-boot-starter 1.33.0 org.springframework.boot spring-boot-starter-data-redis org.apache.commons commons-pool2 io.springfox springfox-swagger2 2.9.2 io.swagger swagger-annotations io.swagger swagger-models io.springfox springfox-swagger-ui 2.9.2 io.swagger swagger-annotations 1.5.21 io.swagger swagger-models 1.5.21 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-test test org.junit.vintage junit-vintage-engine org.springframework.boot spring-boot-dependencies ${spring-boot.version} pom import
1.登录接口
这里是直接进行username和password比对,然后将id传入登录参数得到token返回——>我们一样可以结合数据库,通过用户名密码得到用户信息,存在就将用户唯一标识传入StpUtil.login()种
// 1.测试登录,浏览器访问: http://localhost:8081/user/doLogin?username=zhang&password=123456@ApiOperation(value = "登录测试")@RequestMapping("doLogin")public SaResult doLogin(String username, String password) {// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对 if ("zhang".equals(username) && "123456".equals(password)) {//1.这里我们可以数据库判断业务后,StpUtil.login()用户的idStpUtil.login(10001,false);SaTokenInfo tokenInfo = StpUtil.getTokenInfo();return SaResult.ok().data(tokenInfo).setMsg("登录成功");}return SaResult.error("登录失败");}

2.查询登录状态
// 2.查询登录状态,浏览器访问: http://localhost:8081/user/isLogin@RequestMapping("isLogin")@ApiOperation(value = "登录状态查询")public String isLogin() {return "当前会话是否登录:" + StpUtil.isLogin();}
3.退出登录
单体架构一般就是删除session中相关信息,结合token的话就是将redis中的token删除,如果是token放在cookie中保存,就删除cookie即可
这里注销的是当前用户
@RequestMapping("logout")@ApiOperation(value = "退出登录")public SaResult logout() {StpUtil.logout();return SaResult.ok();}
1.我们来看下Sa-Token中注销用户的核心代码:
它这里是引入了一个Storage存储器的概念,这个存储器每个用户都会有一个类似Session——>这里是由请求进行封装的
/** * 会话注销 */public void logout() {// 如果连 Token 都没有,那么无需执行任何操作String tokenValue = getTokenValue();if(SaFoxUtil.isEmpty(tokenValue)) {return;}// 如果打开了 Cookie 模式,则把 Cookie 清除掉if(getConfig().getIsReadCookie()){SaCookieConfig cookie = getConfig().getCookie();SaHolder.getResponse().deleteCookie(getTokenName(), cookie.getPath(), cookie.getDomain());}// 从当前 [Storage存储器] 里删除 TokenSaHolder.getStorage().delete(splicingKeyJustCreatedSave());// 清除当前上下文的 [临时有效期check标记]SaHolder.getStorage().delete(SaTokenConsts.TOKEN_ACTIVITY_TIMEOUT_CHECKED_KEY);// 清除这个 Token 的相关信息logoutByTokenValue(tokenValue);}
2.会话注销token
/*** 会话注销,根据指定 Token * * @param tokenValue 指定token*/public void logoutByTokenValue(String tokenValue) {// 1. 清理 token-last-activityclearLastActivity(tokenValue); // 2. 注销 Token-Session deleteTokenSession(tokenValue);// 3. 清理 token -> id 索引String loginId = getLoginIdNotHandle(tokenValue);if(loginId != null) {deleteTokenToIdMapping(tokenValue);}// if. 无效 loginId 立即返回if(isValidLoginId(loginId) == false) {return;}// $$ 发布事件:某某Token注销下线了 SaTokenEventCenter.doLogout(loginType, loginId, tokenValue);// 4. 清理User-Session上的token签名 & 尝试注销User-Session SaSession session = getSessionByLoginId(loginId, false);if(session != null) {session.removeTokenSign(tokenValue); session.logoutByTokenSignCountToZero();}}
3.可以看出来我们的storage就是由request进行封装的,并且可以进行封装kv
public SaStorage getStorage() {return new SaStorageForServlet(SpringMVCUtil.getRequest());}
首先我们赋予用户所有权限
这里我们可以给每个用户调用getPermissionList(Object id,String Type)方法,给指定用户传入权限,重写StpInterface接口即可得到权限
package com.wyhtoken.quicktoken.service;import java.util.ArrayList;
import java.util.List;import org.springframework.stereotype.Component;import cn.dev33.satoken.stp.StpInterface;/*** 自定义权限认证接口扩展,Sa-Token 将从此实现类获取每个账号拥有的权限码 * * @author kong* @since 2022-10-13*/
@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {/*** 1.添加权限集合* @param loginId:账户id* @param loginType:账户类型* @return*/@Overridepublic List getPermissionList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询权限List list = new ArrayList(); list.add("101");list.add("user.add");list.add("user.update");list.add("user.get");// list.add("user.delete");list.add("art.*");return list;}/*** 2.返回指定id的用户权限* @param loginId* @param loginType* @return*/@Overridepublic List getRoleList(Object loginId, String loginType) {// 本list仅做模拟,实际项目中要根据具体业务逻辑来查询角色List list = new ArrayList(); list.add("admin");list.add("super-admin");return list;}}
1.用户所有权限
@RequestMapping("checkPermission")@ApiOperation(value = "用户所有权限")public SaResult getPermissionList() {List permissionList = StpUtil.getPermissionList();return SaResult.ok().setData(permissionList).setMsg("所有权限返回成功");}
2.判断用户是否有某权限
这里我们可以跟上述结合起来一起用,查询用户所有权限后,判断用户是否有某权限返回状态,进行兜底服务
//1.返回boolean
StpUtil.hasPermission("user.addd");
//2.抛出异常
StpUtil.checkPermission("user.add");
3.检验用户是否有指定的所有权限
/*** 4.该账号判断是否有指定的多个权限(必须全部通过)** @return*/@ApiOperation(value = "判断用户是否有所有权限")@RequestMapping("checkAll")public SaResult checkPermissionAnd() {StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");return SaResult.ok().setData("所有指定权限都有");}@ApiOperation(value = "判断用户是否有所有权限其中之一")@RequestMapping("checkOr")public SaResult checkPermissionOr(){StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");return SaResult.ok().setData("所有指定权限有其中之一");}
你会发现调用了getPermissionList(用户的标识)方法,而这个方法是StpInterface的实现类重写的方法,可以根据用户标识得到所有权限
/** * 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可] * @param permissionArray 权限码数组*/public void checkPermissionOr(String... permissionArray){Object loginId = getLoginId();List permissionList = getPermissionList(loginId);for (String permission : permissionArray) {if(hasElement(permissionList, permission)) {// 有的话提前退出return; }}if(permissionArray.length > 0) {throw new NotPermissionException(permissionArray[0], this.loginType).setCode(SaErrorCode.CODE_11051);}
串起来所有东西就能理解了
/*** 获取:指定账号的权限码集合 * @param loginId 指定账号id* @return / */public List getPermissionList(Object loginId) {return SaManager.getStpInterface().getPermissionList(loginId, loginType);}
给指定用户踢出下线
@ApiOperation(value = "踢人下线")@RequestMapping("Kick")public SaResult KickOut(Integer id){StpUtil.kickout(10001);return SaResult.ok().setMsg("踢人下线成功");}
// 此接口加上了 @SaIgnore 可以游客访问 @SaIgnore@RequestMapping("getList")public SaResult getList() {// ... return SaResult.ok(); }
package com.wyhtoken.quicktoken.controller;import cn.dev33.satoken.annotation.*;
import cn.dev33.satoken.util.SaResult;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@Api(tags = "Sa-Token接口测试")
@RestController
@RequestMapping("/api/")
public class ApiController {// 登录校验:只有登录之后才能进入该方法@SaCheckLogin@RequestMapping("info")public String info() {return "查询用户信息";}// 角色校验:必须具有指定角色才能进入该方法@SaCheckRole("super-admin")@RequestMapping("add1")public String add1() {return "用户增加";}// 权限校验:必须具有指定权限才能进入该方法@SaCheckPermission("user-add")@RequestMapping("add2")public String add2() {return "用户增加";}// 二级认证校验:必须二级认证之后才能进入该方法@SaCheckSafe()@RequestMapping("add3")public String add3() {return "用户增加";}// Http Basic 校验:只有通过 Basic 认证后才能进入该方法@SaCheckBasic(account = "sa:123456")@RequestMapping("add4")public String add4() {return "用户增加";}// 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法@SaCheckDisable("comment")@RequestMapping("send")public String send() {return "查询用户信息";}// 注解式鉴权:只要具有其中一个权限即可通过校验@RequestMapping("atJurOr")@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)public SaResult atJurOr() {return SaResult.data("用户信息");}// 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验@RequestMapping("userAdd")@SaCheckPermission(value = "user.add", orRole = "admin")public SaResult userAdd() {return SaResult.data("用户信息");}
}
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {// 注册拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {// 注册 Sa-Token 拦截器,校验规则为 StpUtil.checkLogin() 登录校验。registry.addInterceptor(new SaInterceptor(handle -> StpUtil.checkLogin())).addPathPatterns("/**").excludePathPatterns("/user/doLogin","/swagger-ui.html");}/*** 注册全局过滤器*/@Beanpublic SaServletFilter getSaServletFilter(){return new SaServletFilter().setAuth(obj->{System.out.println("--------进入Sa-Token全局认证--------");//1.登录认证 拦截所有路由,并且排除/user/doLogin用于开放登录SaRouter.match("/**", "/user/doLogin", () -> StpUtil.checkLogin());}).setError(e->{System.out.println("---------- 进入Sa-Token异常处理 -----------");return SaResult.error(e.getMessage());})// 前置函数:在每次认证函数之前执行.setBeforeAuth(r -> {// ---------- 设置一些安全响应头 ----------SaHolder.getResponse()// 服务器名称.setServer("sa-server")// 是否可以在iframe显示视图: DENY=不可以 | SAMEORIGIN=同域下可以 | ALLOW-FROM uri=指定域名下可以.setHeader("X-Frame-Options", "SAMEORIGIN")// 是否启用浏览器默认XSS防护: 0=禁用 | 1=启用 | 1; mode=block 启用, 并在检查到XSS攻击时,停止渲染页面.setHeader("X-XSS-Protection", "1; mode=block")// 禁用浏览器内容嗅探.setHeader("X-Content-Type-Options", "nosniff");});}/*** 重写 Sa-Token 框架内部算法策略*/@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略SaStrategy.me.createToken = (loginId, loginType) -> {return SaFoxUtil.getRandomString(60); // 随机60位长度字符串};}}
@Autowiredpublic void rewriteSaStrategy() {// 重写 Token 生成策略SaStrategy.me.createToken = (loginId, loginType) -> {return SaFoxUtil.getRandomString(60); // 随机60位长度字符串};}
常规 Web 端鉴权方法,一般由 Cookie模式 完成,而 Cookie 有两个特性:
可由后端控制写入。
每次请求自动提交。
这就使得我们在前端代码中,无需任何特殊操作,就能完成鉴权的全部流程(因为整个流程都是后端控制完成的)
而在app、小程序等前后台分离场景中,一般是没有 Cookie 这一功能的,此时大多数人都会一脸懵逼,咋进行鉴权啊?
我们也可以加一个验证是否登录
// 登录接口
@RequestMapping("doLogin")
public SaResult doLogin() {// 第1步,先登录上 StpUtil.login(10001);// 第2步,获取 Token 相关参数 SaTokenInfo tokenInfo = StpUtil.getTokenInfo();// 第3步,返回给前端 return SaResult.data(tokenInfo);
}
Sa-Token默认StpUil.login(id,type)实现了记住我功能
实现原理:
Cookie作为浏览器提供的默认会话跟踪机制,其生命周期有两种形式,分别是:
临时Cookie:有效期为本次会话,只要关闭浏览器窗口,Cookie就会消失。
持久Cookie:有效期为一个具体的时间,在时间未到期之前,即使用户关闭了浏览器Cookie也不会消失。
利用Cookie的此特性,我们便可以轻松实现 [记住我] 模式:
勾选 [记住我] 按钮时:调用StpUtil.login(10001, true),在浏览器写入一个持久Cookie储存 Token,此时用户即使重启浏览器 Token 依然有效。
不勾选 [记住我] 按钮时:调用StpUtil.login(10001, false),在浏览器写入一个临时Cookie储存 Token,此时用户在重启浏览器后 Token 便会消失,导致会话失效。
我们可以设置token的有效时间
// 示例1:
// 指定token有效期(单位: 秒),如下所示token七天有效
StpUtil.login(10001, new SaLoginModel().setTimeout(60 * 60 * 24 * 7));// ----------------------- 示例2:所有参数
// `SaLoginModel`为登录参数Model,其有诸多参数决定登录时的各种逻辑,例如:
StpUtil.login(10001, new SaLoginModel().setDevice("PC") // 此次登录的客户端设备类型, 用于[同端互斥登录]时指定此次登录的设备类型.setIsLastingCookie(true) // 是否为持久Cookie(临时Cookie在浏览器关闭时会自动删除,持久Cookie在重新打开后依然存在).setTimeout(60 * 60 * 24 * 7) // 指定此次登录token的有效期, 单位:秒 (如未指定,自动取全局配置的 timeout 值).setToken("xxxx-xxxx-xxxx-xxxx") // 预定此次登录的生成的Token .setIsWriteHeader(false) // 是否在登录后将 Token 写入到响应头);
1.介绍: 
如果你经常使用腾讯QQ,就会发现它的登录有如下特点:它可以手机电脑同时在线,但是不能在两个手机上同时登录一个账号。
同端互斥登录,指的就是:像腾讯QQ一样,在同一类型设备上只允许单地点登录,在不同类型设备上允许同时在线。
首先在配置文件中,将 isConcurrent 配置为false,然后调用登录等相关接口时声明设备类型即可:
StpUtil.login(用户id标识,"PC");
2.查询当前登录的设备类型
// 返回当前token的登录设备类型
StpUtil.getLoginDevice();
3.获取指定登录设备类型的tokenValue
// 获取指定loginId指定设备类型端的tokenValue
StpUtil.getTokenValueByLoginId(10001, "APP");
// 封禁指定账号
StpUtil.disable(10001, 86400);
参数含义:
参数1:要封禁的账号id。
参数2:封禁时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)
1.我们一般采用先踢掉,再封禁的方法
// 先踢下线
StpUtil.kickout(10001);
// 再封禁账号
StpUtil.disable(10001, 86400);
2.然后我们再下次登录可以验证一下
// 校验指定账号是否已被封禁,如果被封禁则抛出异常 `DisableServiceException`
StpUtil.checkDisable(10001); // 通过校验后,再进行登录:
StpUtil.login(10001);
// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);
1、封禁评价能力:账号A 因为多次虚假好评,被限制订单评价功能。
2、封禁下单能力:账号B 因为多次薅羊毛,被限制下单功能。
3、封禁开店能力:账号C 因为店铺销售假货,被限制开店功能。
参数释义:
参数1:要封禁的账号id。
参数2:针对这个账号,要封禁的服务标识(可以是任意的自定义字符串)。
参数3:要封禁的时间,单位:秒,此为 86400秒 = 1天(此值为 -1 时,代表永久封禁)。
/** 以下示例中:"comment"=评论服务标识、"place-order"=下单服务标识、"open-shop"=开店服务标识*/// 封禁指定用户评论能力,期限为 1天
StpUtil.disable(10001, "comment", 86400);// 在评论接口,校验一下,会抛出异常:`DisableServiceException`,使用 e.getService() 可获取业务标识 `comment`
StpUtil.checkDisable(10001, "comment");// 在下单时,我们校验一下 下单能力,并不会抛出异常,因为我们没有限制其下单功能
StpUtil.checkDisable(10001, "place-order");// 现在我们再将其下单能力封禁一下,期限为 7天
StpUtil.disable(10001, "place-order", 86400 * 7);// 然后在下单接口,我们添加上校验代码,此时用户便会因为下单能力被封禁而无法下单(代码抛出异常)
StpUtil.checkDisable(10001, "place-order");// 但是此时,用户如果调用开店功能的话,还是可以通过,因为我们没有限制其开店能力 (除非我们再调用了封禁开店的代码)
StpUtil.checkDisable(10001, "open-shop");
Sa-Token支持非对称和对称加密两种方式
1.对称加密:AES加密
// 定义秘钥和明文
String key = "123456";
String text = "Sa-Token 一个轻量级java权限认证框架";// 加密
String ciphertext = SaSecureUtil.aesEncrypt(key, text);
System.out.println("AES加密后:" + ciphertext);// 解密
String text2 = SaSecureUtil.aesDecrypt(key, ciphertext);
System.out.println("AES解密后:" + text2);
2.非对称加密:RSA加密
// 定义私钥和公钥
String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAO+wmt01pwm9lHMdq7A8gkEigk0XKMfjv+4IjAFhWCSiTeP7dtlnceFJbkWxvbc7Qo3fCOpwmfcskwUc3VSgyiJkNJDs9ivPbvlt8IU2bZ+PBDxYxSCJFrgouVOpAr8ar/b6gNuYTi1vt3FkGtSjACFb002/68RKUTye8/tdcVilAgMBAAECgYA1COmrSqTUJeuD8Su9ChZ0HROhxR8T45PjMmbwIz7ilDsR1+E7R4VOKPZKW4Kz2VvnklMhtJqMs4MwXWunvxAaUFzQTTg2Fu/WU8Y9ha14OaWZABfChMZlpkmpJW9arKmI22ZuxCEsFGxghTiJQ3tK8npj5IZq5vk+6mFHQ6aJAQJBAPghz91Dpuj+0bOUfOUmzi22obWCBncAD/0CqCLnJlpfOoa9bOcXSusGuSPuKy5KiGyblHMgKI6bq7gcM2DWrGUCQQD3SkOcmia2s/6i7DUEzMKaB0bkkX4Ela/xrfV+A3GzTPv9bIBamu0VIHznuiZbeNeyw7sVo4/GTItq/zn2QJdBAkEA8xHsVoyXTVeShaDIWJKTFyT5dJ1TR++/udKIcuiNIap34tZdgGPI+EM1yoTduBM7YWlnGwA9urW0mj7F9e9WIQJAFjxqSfmeg40512KP/ed/lCQVXtYqU7U2BfBTg8pBfhLtEcOg4wTNTroGITwe2NjL5HovJ2n2sqkNXEio6Ji0QQJAFLW1Kt80qypMqot+mHhS+0KfdOpaKeMWMSR4Ij5VfE63WzETEeWAMQESxzhavN1WOTb3/p6icgcVbgPQBaWhGg==";
String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDvsJrdNacJvZRzHauwPIJBIoJNFyjH47/uCIwBYVgkok3j+3bZZ3HhSW5Fsb23O0KN3wjqcJn3LJMFHN1UoMoiZDSQ7PYrz275bfCFNm2fjwQ8WMUgiRa4KLlTqQK/Gq/2+oDbmE4tb7dxZBrUowAhW9NNv+vESlE8nvP7XXFYpQIDAQAB";// 文本
String text = "Sa-Token 一个轻量级java权限认证框架";// 使用公钥加密
String ciphertext = SaSecureUtil.rsaEncryptByPublic(publicKey, text);
System.out.println("公钥加密后:" + ciphertext);// 使用私钥解密
String text2 = SaSecureUtil.rsaDecryptByPrivate(privateKey, ciphertext);
System.out.println("私钥解密后:" + text2);
实现SaTokenListener接口即可
思路:我们可以每次下线将日志写入文件中,就可以用这个全局监听器
/*** 自定义侦听器的实现 */
@Component
public class MySaTokenListener implements SaTokenListener {/** 每次登录时触发 */@Overridepublic void doLogin(String loginType, Object loginId, String tokenValue, SaLoginModel loginModel) {System.out.println("---------- 自定义侦听器实现 doLogin");}/** 每次注销时触发 */@Overridepublic void doLogout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doLogout");}/** 每次被踢下线时触发 */@Overridepublic void doKickout(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doKickout");}/** 每次被顶下线时触发 */@Overridepublic void doReplaced(String loginType, Object loginId, String tokenValue) {System.out.println("---------- 自定义侦听器实现 doReplaced");}/** 每次被封禁时触发 */@Overridepublic void doDisable(String loginType, Object loginId, String service, int level, long disableTime) {System.out.println("---------- 自定义侦听器实现 doDisable");}/** 每次被解封时触发 */@Overridepublic void doUntieDisable(String loginType, Object loginId, String service) {System.out.println("---------- 自定义侦听器实现 doUntieDisable");}/** 每次二级认证时触发 */@Overridepublic void doOpenSafe(String loginType, String tokenValue, String service, long safeTime) {System.out.println("---------- 自定义侦听器实现 doOpenSafe");}/** 每次退出二级认证时触发 */@Overridepublic void doCloseSafe(String loginType, String tokenValue, String service) {System.out.println("---------- 自定义侦听器实现 doCloseSafe");}/** 每次创建Session时触发 */@Overridepublic void doCreateSession(String id) {System.out.println("---------- 自定义侦听器实现 doCreateSession");}/** 每次注销Session时触发 */@Overridepublic void doLogoutSession(String id) {System.out.println("---------- 自定义侦听器实现 doLogoutSession");}/** 每次Token续期时触发 */@Overridepublic void doRenewTimeout(String tokenValue, Object loginId, long timeout) {System.out.println("---------- 自定义侦听器实现 doRenewTimeout");}
}
上一篇:有哪些关于帮助别人的四字成语