Java 权限认证框架功能 测试 / 对比 / 迁移。
对比以下框架的常见功能,为项目技术栈迁移提供代码示例
- Sa-Token
- Apache Shiro
- Spring Security
- JWT
注意事项
- 因个人精力&能力有限,本篇只展示部分常见功能的对比,也欢迎大家一起贡献案例,提交pr。
- 代码案例仓库:https://gitee.com/sa-tokens/auth-framework-function-test
- 注:本篇主要展示一些常见功能不同框架的实现差异,而非每个框架的所含功能点对比。
依赖引入
xml
<!-- Sa-Token 权限认证, 在线文档:https://sa-token.com/ -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.39.0</version>
</dependency>会话登录 & 会话状态查询
测试 Controller
java
@RestController
@RequestMapping("/acc/")
public class LoginController {
@Autowired
SysUserDao sysUserDao;
// 测试登录
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
// 校验
SysUser user = sysUserDao.findByUsername(username);
if(user == null) {
return AjaxJson.getError("用户不存在");
}
if(!user.getPassword().equals(password)) {
return AjaxJson.getError("密码错误");
}
// 登录
StpUtil.login(user.getId());
StpUtil.getSession().set("user", user);
return AjaxJson.getSuccess("登录成功");
}
// 查询登录状态
@RequestMapping("isLogin")
public AjaxJson isLogin() {
if(StpUtil.isLogin()) {
return AjaxJson.getSuccess("已登录,账号id:" + StpUtil.getLoginId());
}
return AjaxJson.getError("未登录");
}
}会话注销
java
@RequestMapping("logout")
public AjaxJson logout() {
StpUtil.logout();
return AjaxJson.getSuccess("注销成功");
}账号密码登录(MD5 加 salt)
测试 Controller
java
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
// 校验
SysUser user = sysUserDao.findByUsername(username);
if(user == null) {
return AjaxJson.getError("用户不存在");
}
String salt = "abc";
if(!user.getPassword().equals(SaSecureUtil.md5(salt + password))) {
return AjaxJson.getError("密码错误");
}
// 登录
StpUtil.login(user.getId());
StpUtil.getSession().set("user", user);
return AjaxJson.getSuccess("登录成功");
}从上下文获取当前登录 User 信息
java
// 从上下文获取当前登录 User 信息
@RequestMapping("getCurrUser")
public AjaxJson getCurrUser() {
return AjaxJson.getSuccess()
.set("id", StpUtil.getLoginId())
.set("user", StpUtil.getSession().get("user"));
}从会话上下文上存取值
java
// 测试从从会话上下文存取值
@RequestMapping("testSession")
public AjaxJson test() {
SaSession session = StpUtil.getSession();
System.out.println("从 session 上取值:" + session.get("name"));
session.set("name", "zhang");
System.out.println("从 session 上取值:" + session.get("name"));
return AjaxJson.getSuccess();
}角色认证 & 权限认证
自定义 StpInterface 实现类
java
@Component
public class StpInterfaceImpl implements StpInterface {
// 加载角色信息
@Override
public List<String> getRoleList(Object loginId, String loginType) {
return Arrays.asList("admin", "super-admin", "ceo");
}
// 加载权限信息
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
return Arrays.asList("user:add", "user:delete", "user:update");
}
}测试 Controller
java
@RestController
@RequestMapping("/jur/")
public class JurController {
// 角色判断 ---- http://localhost:8082/jur/assertRole
@RequestMapping("assertRole")
public AjaxJson assertRole() {
// is 模式,返回 true 或 false
System.out.println("单个角色判断:" + StpUtil.hasRole("admin"));
System.out.println("多个角色判断(and):" + StpUtil.hasRoleAnd("admin", "dev-admin"));
System.out.println("多个角色判断(or):" + StpUtil.hasRoleOr("admin", "dev-admin"));
// check 模式,无角色时抛出异常
StpUtil.checkRole("admin"); // 单个 check
StpUtil.checkRoleAnd("admin", "dev-admin"); // 多个 check (and)
StpUtil.checkRoleOr("admin", "dev-admin"); // 多个 check (or)
return AjaxJson.getSuccess();
}
// 权限判断 ---- http://localhost:8082/jur/assertPermission
@RequestMapping("assertPermission")
public AjaxJson assertPermission() {
// is 模式,返回 true 或 false
System.out.println("单个权限判断:" + StpUtil.hasPermission("user:add"));
System.out.println("多个权限判断(and):" + StpUtil.hasPermissionAnd("user:add", "user:delete22"));
System.out.println("多个权限判断(or):" + StpUtil.hasPermissionOr("user:add", "user:delete22"));
// check 模式,无权限时抛出异常
StpUtil.checkPermission("user:add"); // 单个 check
StpUtil.checkPermissionAnd("user:add", "user:delete22"); // 多个 check (and)
StpUtil.checkPermissionOr("user:add", "user:delete22"); // 多个 check (or)
return AjaxJson.getSuccess();
}
}注解鉴权
SaTokenConfigure 配置注解拦截器
java
@Configuration
public class SaTokenConfigure implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
}
}测试 Controller
java
@RestController
@RequestMapping("/at-check/")
public class AtCheckController {
// 登录校验
@SaCheckLogin
@RequestMapping("checkLogin")
public AjaxJson checkLogin() {
return AjaxJson.getSuccess();
}
// 角色校验
@SaCheckRole("admin")
@RequestMapping("checkRole")
public AjaxJson checkRole() {
return AjaxJson.getSuccess();
}
// 权限校验
@SaCheckPermission("user:add")
@RequestMapping("checkPermission")
public AjaxJson checkPermission() {
return AjaxJson.getSuccess();
}
// 忽略认证校验
@SaIgnore
@SaCheckLogin
@RequestMapping("ignoreCheck")
public AjaxJson ignoreCheck() {
return AjaxJson.getSuccess();
}
}路由拦截鉴权
SaTokenConfigure 配置
java
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 注册 Sa-Token 拦截器打开注解鉴权功能
registry.addInterceptor(new SaInterceptor(handle -> {
SaRouter.match("/route-check/getInfo1").stop(); // 不拦截
SaRouter.match("/route-check/getInfo2").check(r -> StpUtil.checkLogin()); // 需要登录
SaRouter.match("/route-check/getInfo3").check(r -> StpUtil.checkRole("admin2")); // 需要角色
SaRouter.match("/route-check/getInfo4").check(r -> StpUtil.checkPermission("user:add3")); // 需要权限
})).addPathPatterns("/**");
}鉴权未通过的处理方案
定义全局异常处理类
java
@RestControllerAdvice
public class GlobalException {
@ExceptionHandler(NotLoginException.class)
public AjaxJson handlerException(NotLoginException e) {
return AjaxJson.get(401, "未登录");
}
@ExceptionHandler(NotRoleException.class)
public AjaxJson handlerException(NotRoleException e) {
return AjaxJson.get(403, "缺少角色:" + e.getRole());
}
@ExceptionHandler(NotPermissionException.class)
public AjaxJson handlerException(NotPermissionException e) {
return AjaxJson.get(403, "缺少权限:" + e.getPermission());
}
}和 Thymeleaf 集成
pom.xml 依赖
xml
<!-- 在 thymeleaf 标签中使用 Sa-Token -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dialect-thymeleaf</artifactId>
<version>1.39.0</version>
</dependency>SaTokenConfigure 增加配置 Sa-Token 标签方言对象
java
// Sa-Token 标签方言 (Thymeleaf版)
@Bean
public SaTokenDialect getSaTokenDialect() {
return new SaTokenDialect();
}新建 ThymeleafConfigure 注入全局变量
java
@Configuration
public class ThymeleafConfigure {
// 为 Thymeleaf 注入全局变量,以便在页面中调用 Sa-Token 的方法
@Autowired
public void configureThymeleafStaticVars(ThymeleafViewResolver viewResolver) {
viewResolver.addStaticVariable("stp", StpUtil.stpLogic);
}
}新建 Controller
java
@Controller
public class HomeController {
@RequestMapping("/")
public Object index(HttpServletRequest request) {
request.setAttribute("isLogin", StpUtil.isLogin());
return new ModelAndView("index.html");
}
}新建 templates/index.html
html
<!DOCTYPE html>
<html lang="zh" xmlns:sa="http://www.thymeleaf.org/extras/sa-token">
<head>
<title>Sa-Token 集成 Thymeleaf 标签方言</title>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, minimum-scale=1.0, maximum-scale=1.0, user-scalable=no">
</head>
<body>
<div class="view-box" style="padding: 30px;">
<h2>Sa-Token 集成 Thymeleaf 标签方言 —— 测试页面</h2>
<p>当前是否登录:<span th:text="${stp.isLogin()}"></span></p>
<p>
<a href="login" target="_blank">登录</a>
<a href="logout" target="_blank">注销</a>
</p>
<p>登录之后才能显示:<span sa:login>value</span></p>
<p>不登录才能显示:<span sa:notLogin>value</span></p>
<p>具有角色 admin 才能显示:<span sa:hasRole="admin">value</span></p>
<p>同时具备多个角色才能显示:<span sa:hasRoleAnd="admin, ceo, cto">value</span></p>
<p>只要具有其中一个角色就能显示:<span sa:hasRoleOr="admin, ceo, cto">value</span></p>
<p>不具有角色 admin 才能显示:<span sa:notRole="admin">value</span></p>
<p>具有权限 user-add 才能显示:<span sa:hasPermission="user-add">value</span></p>
<p>同时具备多个权限才能显示:<span sa:hasPermissionAnd="user-add, user-delete, user-get">value</span></p>
<p>只要具有其中一个权限就能显示:<span sa:hasPermissionOr="user-add, user-delete, user-get">value</span></p>
<p>不具有权限 user-add 才能显示:<span sa:notPermission="user-add">value</span></p>
<p th:if="${stp.isLogin()}">
从SaSession中取值:
<span th:text="${stp.getSession().get('name', '')}"></span>
</p>
</div>
</body>
</html>前后端分离
1、在登录时,将 token 信息返回到前端
java
// 测试登录
@RequestMapping("doLogin")
public AjaxJson doLogin(String username, String password) {
// 校验
SysUser user = sysUserDao.findByUsername(username);
// user 信息校验代码不再赘述 ...
// 登录
StpUtil.login(user.getId());
StpUtil.getSession().set("user", user);
return AjaxJson.getSuccess("登录成功").set("satoken", StpUtil.getTokenValue()); // ⚠️ 关键代码
}2、前端改造
- 1、在登录请求时,将返回的 token 保存到本地
localStorage.setItem('satoken', res.satoken)。 - 2、在后续每次请求中,读取本地保存的 satoken 塞到请求 header 中
js
const header = {};
if(localStorage.satoken) {
header.satoken = localStorage.satoken;
}
// 后续提交请求...集成 Redis
pom.xml 引入依赖
xml
<!-- Sa-Token 整合 RedisTemplate -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-template</artifactId>
<version>1.39.0</version>
</dependency>
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>application.yml 新增连接配置
yaml
spring:
data:
# 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其它代码照旧

