基于 Sa-Token 实现微服务登录鉴权实战
简介
Sa-Token 是一个轻量级 Java 权限认证框架,主要解决:登录认证、权限认证、单点登录、OAuth2.0、分布式Session会话、微服务网关鉴权 等一系列权限相关问题。
官网地址:https://sa-token.cc/
SpringBoot 微服务实战
1、创建项目
在 IDEA 中新建一个 SpringBoot 项目,命名:sa-token-demo-springboot
2、添加依赖
在项目中添加依赖:
注:如果你使用的是 SpringBoot 3.x,只需要将 sa-token-spring-boot-starter 修改为 sa-token-spring-boot3-starter 即可。
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>1.35.0.RC</version>
</dependency>
3、设置配置文件
在 application.yml 文件中增加如下配置,定制性使用框架:
server:
# 端口
port: 8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
sa-token:
# token 名称(同时也是 cookie 名称)
token-name: satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
timeout: 2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
active-timeout: -1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个 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
# 端口
server.port=8081
############## Sa-Token 配置 (文档: https://sa-token.cc) ##############
# token 名称(同时也是 cookie 名称)
sa-token.token-name=satoken
# token 有效期(单位:秒) 默认30天,-1 代表永久有效
sa-token.timeout=2592000
# token 最低活跃频率(单位:秒),如果 token 超过此时间没有访问系统就会被冻结,默认-1 代表不限制,永不冻结
sa-token.active-timeout=-1
# 是否允许同一账号多地同时登录 (为 true 时允许一起登录, 为 false 时新登录挤掉旧登录)
sa-token.is-concurrent=true
# 在多人登录同一账号时,是否共用一个 token (为 true 时所有登录共用一个 token, 为 false 时每次登录新建一个 token)
sa-token.is-share=true
# token 风格(默认可取值:uuid、simple-uuid、random-32、random-64、random-128、tik)
sa-token.token-style=uuid
# 是否输出操作日志
sa-token.is-log=true
4、创建启动类
在项目中新建包 com.xinyi ,在此包内新建主类 SaTokenDemoApplication.java,复制以下代码:
@SpringBootApplication
public class SaTokenDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SaTokenDemoApplication.class, args);
System.out.println("启动成功,Sa-Token 配置如下:" + SaManager.getConfig());
}
}
5、创建测试 controller
@RestController
@RequestMapping("user")
public class UserController {
/**
* 测试登录
* 浏览器访问: http://localhost:8081/user/doLogin?username=admin&password=123456
*
* @param username 用户名
* @param password 密码
* @return 登录结果
*/
@RequestMapping("doLogin")
public String doLogin(String username, String password) {
// 此处仅作模拟示例,真实项目需要从数据库中查询数据进行比对
if ("admin".equals(username) && "123456".equals(password)) {
StpUtil.login(10001);
return "登录成功";
}
return "登录失败";
}
/**
* 查询登录状态
* 浏览器访问: http://localhost:8081/user/isLogin
*
* @return 登录状态
*/
@RequestMapping("isLogin")
public String isLogin() {
return "当前会话是否登录:" + StpUtil.isLogin();
}
}
6、测试运行
项目启动日志如下:
浏览器访问: http://localhost:8081/user/doLogin?username=admin&password=123456
登录成功,返回这个用户的 Token 会话凭证
浏览器访问:http://localhost:8081/user/isLogin
登录成功,返回这个用户的 Token 会话凭证,用户后续的每次请求,都携带上这个 Token,服务器根据 Token 判断此会话是否登录成功。
登录认证
登录访问流程
用户携带用户名和密码调用登录接口,登录成功后,返回该用户Token会话凭证,用户后续的每次请求,都携带上这个Token,服务器根据 Token 判断此会话是否登录成功
API 列表
登录与注销
// 会话登录:参数填写要登录的账号id,建议的数据类型:long | int | String, 不可以传入复杂类型,如:User、Admin 等等
StpUtil.login(Object id);
// 当前会话注销登录
StpUtil.logout();
// 获取当前会话是否已经登录,返回true=已登录,false=未登录
StpUtil.isLogin();
// 检验当前会话是否已经登录, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.checkLogin();
会话查询
// 获取当前会话账号id, 如果未登录,则抛出异常:`NotLoginException`
StpUtil.getLoginId();
// 类似查询API还有:
StpUtil.getLoginIdAsString(); // 获取当前会话账号id, 并转化为`String`类型
StpUtil.getLoginIdAsInt(); // 获取当前会话账号id, 并转化为`int`类型
StpUtil.getLoginIdAsLong(); // 获取当前会话账号id, 并转化为`long`类型
// ---------- 指定未登录情形下返回的默认值 ----------
// 获取当前会话账号id, 如果未登录,则返回 null
StpUtil.getLoginIdDefaultNull();
// 获取当前会话账号id, 如果未登录,则返回默认值 (`defaultValue`可以为任意类型)
StpUtil.getLoginId(T defaultValue);
token 查询
// 获取当前会话的 token 值
StpUtil.getTokenValue();
// 获取当前`StpLogic`的 token 名称
StpUtil.getTokenName();
// 获取指定 token 对应的账号id,如果未登录,则返回 null
StpUtil.getLoginIdByToken(String tokenValue);
// 获取当前会话剩余有效期(单位:s,返回-1代表永久有效)
StpUtil.getTokenTimeout();
// 获取当前会话的 token 信息参数
StpUtil.getTokenInfo();
测试代码
@RestController
@RequestMapping("login")
public class LoginController {
/**
* 测试登录
* 浏览器访问:http://localhost:8081/login/doLogin?name=admin&pwd=123456
*
* @param name 用户名
* @param pwd 密码
* @return 登录结果
*/
@GetMapping("doLogin")
public SaResult doLogin(@RequestParam("name") String name, @RequestParam("pwd") String pwd) {
if ("admin".equals(name) && "12345".equals(pwd)) {
StpUtil.login("10001");
return SaResult.ok(name + " 登录成功");
}
return SaResult.error("登录失败");
}
/**
* 查询登录状态
* 浏览器访问:http://localhost:8081/login/isLogin
*
* @return 结果
*/
@GetMapping("isLogin")
public SaResult isLogin() {
return SaResult.ok("是否登录:" + StpUtil.isLogin());
}
/**
* 查询 Token
* 浏览器访问:http://localhost:8081/login/tokenInfo
*
* @return
*/
@GetMapping("tokenInfo")
public SaResult tokenInfo() {
return SaResult.data(StpUtil.getTokenInfo());
}
/**
* 测试注销
* 浏览器访问:http://localhost:8081/login/logout
*
* @return
*/
@GetMapping("logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok();
}
}
权限认证
1、设计思路
权限认证, 核心逻辑就是验证一个账户是否拥有指定权限
深入到底层数据中,就是每个账号都会拥有一组权限码集合,框架来校验这个集合中是否包含指定的权限码。
2、获取当前账户权限码合集
实现Sa-Token暴露的 StpInterface 接口
@Component
public class StpInterfaceImpl implements StpInterface {
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询权限
List<String> list = new ArrayList<String>();
list.add("101");
list.add("user.add");
list.add("user.update");
list.add("user.get");
// list.add("user.delete");
list.add("art.*");
return list;
}
/**
* 返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 本 list 仅做模拟,实际项目中要根据具体业务逻辑来查询角色
List<String> list = new ArrayList<String>();
list.add("admin");
list.add("super-admin");
return list;
}
}
3、API 列表
权限校验 API
// 获取:当前账号所拥有的权限集合
StpUtil.getPermissionList();
// 判断:当前账号是否含有指定权限, 返回 true 或 false
StpUtil.hasPermission("user.add");
// 校验:当前账号是否含有指定权限, 如果验证未通过,则抛出异常: NotPermissionException
StpUtil.checkPermission("user.add");
// 校验:当前账号是否含有指定权限 [指定多个,必须全部验证通过]
StpUtil.checkPermissionAnd("user.add", "user.delete", "user.get");
// 校验:当前账号是否含有指定权限 [指定多个,只要其一验证通过即可]
StpUtil.checkPermissionOr("user.add", "user.delete", "user.get");
扩展:NotPermissionException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
角色校验 API
// 获取:当前账号所拥有的角色集合
StpUtil.getRoleList();
// 判断:当前账号是否拥有指定角色, 返回 true 或 false
StpUtil.hasRole("super-admin");
// 校验:当前账号是否含有指定角色标识, 如果验证未通过,则抛出异常: NotRoleException
StpUtil.checkRole("super-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,必须全部验证通过]
StpUtil.checkRoleAnd("super-admin", "shop-admin");
// 校验:当前账号是否含有指定角色标识 [指定多个,只要其一验证通过即可]
StpUtil.checkRoleOr("super-admin", "shop-admin");
扩展:NotRoleException 对象可通过 getLoginType() 方法获取具体是哪个 StpLogic 抛出的异常
注销和下线
设计思路
踢人下线,核心操作就是找到指定 loginId 对应的 Token,并设置其失效
强制注销 和 踢人下线 的区别在于:
- 强制注销等价于对方主动调用了注销方法,再次访问会提示:Token无效。
- 踢人下线不会清除Token信息,而是将其打上特定标记,再次访问会提示:Token已被踢下线。
API 列表
强制注销 API
StpUtil.logout(10001); // 强制指定账号注销下线
StpUtil.logout(10001, "PC"); // 强制指定账号指定端注销下线
StpUtil.logoutByTokenValue("token"); // 强制指定 Token 注销下线
踢人下线 API
StpUtil.kickout(10001); // 将指定账号踢下线
StpUtil.kickout(10001, "PC"); // 将指定账号指定端踢下线
StpUtil.kickoutByTokenValue("token"); // 将指定 Token 踢下线
测试代码
@RestController
@RequestMapping("out")
public class KickoutController {
/**
* 测试用户强制注销 浏览器访问:http://localhost:8081/out/doLogout/10002
*
* @param loginId
* @return
*/
@GetMapping("doLogout/{loginId}")
public SaResult doLogout(@PathVariable String loginId) {
StpUtil.logout(loginId);
return SaResult.ok();
}
/**
* 测试用户踢下线 浏览器访问:http://localhost:8081/out/doKickout/10002
*
* @param loginId
* @return
*/
@GetMapping("doKickout/{loginId}")
public SaResult doKickout(@PathVariable String loginId) {
StpUtil.kickout(loginId);
return SaResult.ok();
}
}
注解方式实现鉴权
注解鉴权
- @SaCheckLogin: 登录校验 —— 只有登录之后才能进入该方法。
- @SaCheckRole("admin"): 角色校验 —— 必须具有指定角色标识才能进入该方法。
- @SaCheckPermission("user:add"): 权限校验 —— 必须具有指定权限才能进入该方法。
- @SaCheckSafe: 二级认证校验 —— 必须二级认证之后才能进入该方法。
- @SaCheckBasic: HttpBasic校验 —— 只有通过 Basic 认证后才能进入该方法。
- @SaIgnore:忽略校验 —— 表示被修饰的方法或类无需进行注解鉴权和路由拦截器鉴权。
- @SaCheckDisable("comment"):账号服务封禁校验 —— 校验当前账号指定服务是否被封禁。
Sa-Token 使用全局拦截器完成注解鉴权功能,为了不为项目带来不必要的性能负担,拦截器默认处于关闭状态
因此,为了使用注解鉴权,你必须手动将 Sa-Token 的全局拦截器注册到你项目中
1、注册拦截器
新建配置类SaTokenConfigure.java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}
2、使用注解鉴权
@SaCheckLogin // 作用在类上,表示该类下的任意方法均需登录后访问
@RestController
@RequestMapping("atc")
public class AtCheckController {
/**
* 登录校验:只有登录之后才能进入该方法
*
* @return
*/
@SaCheckLogin
@RequestMapping("info")
public String info() {
return "查询用户信息";
}
/**
* 角色校验:必须具有指定角色才能进入该方法
*
* @return
*/
@SaCheckRole("super-admin")
@RequestMapping("add")
public String add() {
return "用户增加";
}
/**
* 权限校验:必须具有指定权限才能进入该方法
*
* @return
*/
@SaCheckPermission("user-update")
@RequestMapping("add/permission")
public String update() {
return "用户修改";
}
/**
* 校验当前账号是否被封禁 comment 服务,如果已被封禁会抛出异常,无法进入方法
*
* @return
*/
@SaCheckDisable("comment")
@RequestMapping("send")
public String send() {
return "查询用户信息";
}
/**
* 注解式鉴权:只要具有其中一个权限即可通过校验
*
* @return
*/
@RequestMapping("atJurOr")
@SaCheckPermission(value = {"user-add", "user-all", "user-delete"}, mode = SaMode.OR)
public SaResult atJurOr() {
return SaResult.data("用户信息");
}
/**
* 角色权限双重 “or校验”:具备指定权限或者指定角色即可通过校验
*
* 写法一:orRole = "admin",代表需要拥有角色 admin 。
* 写法二:orRole = {"admin", "manager", "staff"},代表具有三个角色其一即可。
* 写法三:orRole = {"admin, manager, staff"},代表必须同时具有三个角色。
* @return
*/
@RequestMapping("userAdd")
@SaCheckPermission(value = "user.add", orRole = "admin")
public SaResult userAdd() {
return SaResult.data("用户信息");
}
/**
* 此接口加上了 @SaIgnore 可以游客访问
* @SaIgnore 修饰方法时代表这个方法可以被游客访问,修饰类时代表这个类中的所有接口都可以游客访问。
* @SaIgnore 具有最高优先级,当 @SaIgnore 和其它鉴权注解一起出现时,其它鉴权注解都将被忽略。
* @return
*/
@SaIgnore
@RequestMapping("getList")
public SaResult getList() {
// ...
return SaResult.ok();
}
/**
* 在 `@SaCheckOr` 中可以指定多个注解,只要当前会话满足其中一个注解即可通过验证,进入方法。
* @return
*/
@SaCheckOr(
login = @SaCheckLogin,
role = @SaCheckRole("admin"),
permission = @SaCheckPermission("user.add"),
safe = @SaCheckSafe("update-password"),
basic = @SaCheckBasic(account = "sa:123456"),
disable = @SaCheckDisable("submit-orders")
)
@RequestMapping("test")
public SaResult test() {
// ...
return SaResult.ok();
}
}
本文由mdnice多平台发布