使用sa-token 进行权限控制_satoken使用_蜀黍是个小学生
sa-token...
使用sa-token 进行权限控制
支持路由鉴权+注解鉴权
框架地址://sa-token.dev33.cn/
项目整体思路:本项目采用RBAC(基于角色的权限访问控制)用户关联多个角色,角色关联菜单/权限。sys_menu 表中通过type字段区别是菜单还是权限。通过当前登录用户角色获取对应的菜单集合和权限集合返给前端,前端使用menu表中url 或者 code码来校验当前页面按钮等相关权限。后端采用请求路由来校验权限。
项目包含功能:
- 多数据源knifej 3.0文档redis项目初始化sql 在resources目录下。登录、注册、退出、获取登录状态、获取菜单列表、获取权限码列表等权限使用闭合回路。
一、在 springboot 项目中使用sa-token
1.1 依赖引入
<properties>
<sa-token.version>1.29.0</sa-token.version>
<commons-pool2.version>2.7.0</commons-pool2.version>
<fastjson.version>1.2.56</fastjson.version>
<knife4j.version>3.0.3</knife4j.version>
<dynamic.datasource.version>3.0.0</dynamic.datasource.version>
<mybatis-plus.version>3.5.0</mybatis-plus.version>
</properties>
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!--集成redis-->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis</artifactId>
<version>${sa-token.version}</version>
</dependency>
<!-- 集成jwt-->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-jwt</artifactId>
<version>${sa-token.version}</version>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>${knife4j.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>${commons-pool2.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!--多数据源-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot-starter</artifactId>
<version>${dynamic.datasource.version}</version>
</dependency>
1.2 表结构
1.2.1 用户表
CREATE TABLE `sys_user` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(128) NOT NULL COMMENT '用户名称',
`nickname` varchar(128) DEFAULT NULL COMMENT '昵称',
`password` varchar(128) NOT NULL COMMENT '密码',
`salt` varchar(32) NOT NULL COMMENT '盐',
`logged` datetime NOT NULL COMMENT '最后登录时间',
`deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='系统用户表';
1.2.2 菜单/权限表
菜單授权表实用type字符串鉴别-是菜單還是授权(是主菜單還是下侧菜單…一系列)CREATE TABLE `sys_menu` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`pid` int(11) NOT NULL COMMENT '父级ID',
`title` varchar(128) NOT NULL COMMENT '文本内容',
`url` varchar(512) DEFAULT NULL COMMENT '链接的url',
`icon` varchar(0) DEFAULT NULL COMMENT '菜单的icon',
`code` varchar(125) NOT NULL COMMENT '权限标识符:对于后台控制类定义。示例:user:list',
`type` int(1) NOT NULL DEFAULT '0' COMMENT '权限类型:1- 目录 | 2 - 菜单-主菜单 | 3 - 按钮 | 5-左侧菜单',
`ord` int(11) NOT NULL DEFAULT '0' COMMENT '菜单 排序 数值越大越靠前',
`status` int(1) NOT NULL DEFAULT '0' COMMENT '状态: 0-正常 | 1-封禁 | 2-正常且禁止封禁',
`deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_pid` (`pid`),
KEY `idx_url` (`url`(191))
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8mb4 COMMENT='系统菜单/权限表';
1.2.3 角色表
主演表安全使用code 主演码实行主演较验。CREATE TABLE `sys_role` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) NOT NULL COMMENT '名称',
`code` varchar(125) NOT NULL COMMENT 'Key值',
`remark` varchar(100) DEFAULT NULL COMMENT '角色描述',
`deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否删除 0-否 1-是',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_name` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8mb4 COMMENT='角色表';
1.2.4 用户-角色 关系表
CREATE TABLE `sys_user_role_mapping` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`userId` int(11) NOT NULL COMMENT '用户编号',
`roleId` int(11) NOT NULL COMMENT '角色编号',
`deleted` int(1) NOT NULL DEFAULT '0' COMMENT ' 是否删除 0-否 1-是',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_userId` (`userId`),
KEY `idx_roleId` (`roleId`)
) ENGINE=InnoDB AUTO_INCREMENT=498 DEFAULT CHARSET=utf8mb4 COMMENT='用户-角色 关系表';
1.2.5 角色-权限 关系表
CREATE TABLE `sys_role_menu_mapping` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`roleId` int(11) NOT NULL COMMENT '角色id',
`menuId` int(11) NOT NULL COMMENT '权限/菜单id',
`deleted` int(1) NOT NULL DEFAULT '0' COMMENT '是否 删除 0-否 1-是',
`createTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updateTime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
KEY `idx_roleId` (`roleId`),
KEY `idx_menuId` (`menuId`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COMMENT='角色-权限 关系表';
1.2.6 表之间关系
1.3 springboot yml文件配置
spring:
datasource:
dynamic:
hikari:
minimum-idle: 4
maximum-pool-size: 4
connection-init-sql: SELECT 1
connection-test-query: SELECT 1
datasource:
master:
url: jdbc:mysql://localhost:3306/sa_token?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
slave:
url: jdbc:mysql://localhost:3306/sa_token?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
source:
url: jdbc:mysql://localhost:3306/sa_token?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=Asia/Shanghai&zeroDateTimeBehavior=CONVERT_TO_NULL
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
# redis配置
redis:
database: 1
host: localhost
port: 6379
password: 123456
timeout: 30s
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
#sa-tken配置
sa-token:
token-name: Authorization
timeout: 86400 #一天
activity-timeout: -1 #(指定时间内无操作就视为token过期) 单位: 秒,-1表示永不过期
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时新登录挤掉旧登录)
is-concurrent: true
token-session-check-login: false
is-share: false
token-style: uuid
is-log: true # 是否输出操作日志
is-read-head: true
auto-renew: true #自动续签
jwt-secret-key: 9c+CuEVDxD+Fl1LApkBCIA==
knife4j:
enable: true
1.4 token 配置
@Configuration
public class TokenConfigure {
@Bean
public StpLogic getStpLoginJwt() {
return new StpLogicJwtForStyle();
}
}
1.4 权限校验-通过路由校验
@Override
public void addInterceptors(InterceptorRegistry registry) {
//开启路由鉴权
registry.addInterceptor(new SaRouteInterceptor((request, response, handler) -> {
log.info(">>> 请求完整路径:{}", request.getUrl());
//登录拦截 鉴权
SaRouter.match("/**")
.notMatch(notLogin())
.check(StpUtil::checkLogin);
// 产品权限、订单管理、财务管理、资料管理、数据中心 权限
SaRouter.match(authPermission())
.notMatch(notAuth())
.check(() -> StpUtil.checkRole(RoleEnum.AUTH.getCode()));
}));
}
public static List<String> notLogin() {
List<String> allows = new ArrayList<>();
allows.add("//user/**");
//swagger资源放行
allows.add("/v2/**");
allows.add("/v3/**");
allows.add("/swagger-ui.html/**");
allows.add("/swagger-ui/**");
allows.add("/doc.html/**");
allows.add("/api-docs-ext/**");
allows.add("/swagger-resources/**");
allows.add("/webjars/**");
allows.add("/favicon.ico");
allows.add("/error");
return allows;
}
/**
* 订单管理、财务管理、资料管理、数据中心 权限
*
* @return
*/
public static List<String> authPermission() {
List<String> allows = new ArrayList<>();
//产品权限
return allows;
}
/**
* 不鉴权 但是需要登录认证
* @return
*/
public static List<String> notAuth() {
List<String> allows = new ArrayList<>();
return allows;
}
1.5 swagger-Knife4j 3.0 配置
@Configuration
@EnableOpenApi
@EnableKnife4j
@ConditionalOnProperty(
name = {"knife4j.enable"},
havingValue = "true"
)
public class Knife4jConfig {
@Bean
public Docket docket() {
return new Docket(DocumentationType.OAS_30)
.useDefaultResponseMessages(false)
.groupName("1.0版本")
.select()
//.paths(PathSelectors.any())
.apis(RequestHandlerSelectors.basePackage("com.example.auth.rest"))
.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
.build()
.directModelSubstitute(Timestamp.class, Long.class)
.apiInfo(apiInfo())
.ignoredParameterTypes(HttpServletRequest.class, HttpServletResponse.class)
.globalResponses(HttpMethod.GET, getResponseMessage())
.globalResponses(HttpMethod.POST, getResponseMessage());
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("sa-token文档")
.description("sa-token文档")
.version("1.0.0")
.termsOfServiceUrl("//localhost:9010/doc.html")
.build();
}
private List<Response> getResponseMessage() {
List<Response> resMsgList = Arrays.asList(
new ResponseBuilder().code("200").description("成功").build(),
new ResponseBuilder().code("401").description("无效token").build(),
new ResponseBuilder().code("403").description("没有权限操作,请后台添加相应权限").build(),
new ResponseBuilder().code("500").description("系统异常").build(),
new ResponseBuilder().code("1001").description("业务异常").build(),
new ResponseBuilder().code("1002").description("操作太频繁,请稍后再试").build());
return resMsgList;
}
}
1.6 自定义权限验证接口扩展 实现 StpInterface
@Component
public class RoleAndMenuImpl implements StpInterface {
@Autowired
private IRoleAndMenuService IRoleAndMenuService;
/**
* 返回一个账号所拥有的权限码集合
*
* @param userId
* @param userType
* @return
*/
@Override
public List<String> getPermissionList(Object userId, String userType) {
List<SysMenu> menus = IRoleAndMenuService.getMenuOrPermissionList(Convert.toInt(userId), null);
return menus.stream().map(SysMenu::getCode).collect(toList());
}
/**
* 返回一个账号所拥有的角色标识集合
*
* @param userId
* @param userType
* @return
*/
@Override
public List<String> getRoleList(Object userId, String userType) {
List<SysRole> roles = IRoleAndMenuService.getRoleList(Convert.toInt(userId));
return roles.stream().map(SysRole::getCode).collect(toList());
}
}
二、全局处理
2.1 全局返回结果封装
/**
* 统一结果返回
*
* @param <T>
*/
@ApiModel("接口返回数据")
public class Result<T> implements Serializable {
private static final long serialVersionUID = 170853201891282512L;
@ApiModelProperty(value = "返回码")
private int code;
@ApiModelProperty(value = "信息")
private String msg;
@ApiModelProperty(value = "返回数据")
private T data;
private static final int SUCCESS_CODE = 0;
private static final String SUCCESS_MSG = "success";
private static final int ERROR_CODE = 500;
private static final String ERROR_MSG = "error";
public Result(int code, String msg, T data) {
this.code = code;
this.msg = msg;
this.data = data;
}
public Result() {
this.code = SUCCESS_CODE;
}
public static <T> Result<T> ok() {
return of(SUCCESS_CODE, SUCCESS_MSG, null);
}
public static <T> Result<T> ok(T data) {
return of(SUCCESS_CODE, SUCCESS_MSG, data);
}
public static <T> Result<T> fail() {
return of(ERROR_CODE, ERROR_MSG, null);
}
public static <T> Result<T> fail(int code, String msg) {
return of(code, msg, null);
}
public static <T> Result<T> fail(BusinessErrorCode code) {
return of(code.getCode(), code.getErrMessage(), null);
}
public static <T> Result<T> fail(int code, String msg, T data) {
return new Result(code, msg, data);
}
private static <T> Result<T> of(int code, String msg, T data) {
return new Result(code, msg, data);
}
public static <T> Result<T> fail(BusinessErrorCode codeData, T data) {
return new Result(codeData.getCode(), codeData.getErrMessage(), data);
}
public int getCode() {
return code;
}
public String getMsg() {
return msg;
}
public T getData() {
return data;
}
public void setData(T data) {
this.data = data;
}
}
2.2 全局异常处理
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 未登录
*
* @param e
* @return
*/
@ExceptionHandler(value = NotLoginException.class)
public Result<ServiceError> notLoginExceptionHandler(NotLoginException e) {
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.NO_HAVE_LOGIN_USER);
}
/**
* 角色异常
*
* @param e
* @return
*/
@ExceptionHandler(value = {NotRoleException.class, NotPermissionException.class})
public Result<ServiceError> notRoleExceptionHandler(Exception e) {
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.NOT_PERMISSION_ERROR);
}
@ExceptionHandler(value = ConstraintViolationException.class)
public Result<List<String>> constraintViolationHandler(ConstraintViolationException e) {
Set<ConstraintViolation<?>> violations = e.getConstraintViolations();
LinkedList<String> errors = Lists.newLinkedList();
if (!CollectionUtils.isEmpty(violations)) {
violations.forEach(constraintViolation -> errors.add(constraintViolation.getMessage()));
}
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.ARGS_VALID_ERROR.getCode(), BusinessErrorCode.ARGS_VALID_ERROR.getErrMessage(), errors);
}
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result<List<String>> methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
List<ObjectError> allErrors = e.getBindingResult().getAllErrors();
LinkedList<String> errors = Lists.newLinkedList();
if (!CollectionUtils.isEmpty(allErrors)) {
allErrors.forEach(objectError -> errors.add(objectError.getDefaultMessage()));
}
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.ARGS_VALID_ERROR.getCode(), BusinessErrorCode.ARGS_VALID_ERROR.getErrMessage(), errors);
}
/**
* 处理空指针的异常
*
* @param e 参数
* @return 返回异常信息
*/
@ExceptionHandler(value = NullPointerException.class)
public Result<ServiceError> exceptionHandler(NullPointerException e) {
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.SYS_ERROR);
}
/**
* 处理其他异常
*
* @param e 参数
* @return 返回异常信息
*/
@ExceptionHandler(value = Exception.class)
public Result<ServiceError> exceptionHandler(Exception e) {
errorFixedPosition(e);
log.error("_> 错误原因:");
log.error("_> {}", e.getMessage());
log.error("=============================错误打印完毕=============================");
return Result.fail(BusinessErrorCode.SYS_ERROR);
}
/**
* 定位错误发生的位置
*
* @param e 错误参数
*/
private void errorFixedPosition(Exception e) {
final StackTraceElement stackTrace = e.getStackTrace()[0];
final String className = stackTrace.getClassName();
final int lineNumber = stackTrace.getLineNumber();
final String methodName = stackTrace.getMethodName();
e.printStackTrace();
log.error("=============================错误信息如下=============================");
log.error("_> 异常定位:");
log.error("_> 类[{}] ==> 方法[{}] ==> 所在行[{}]\n", className, methodName, lineNumber);
}
}
三、项目 demo 地址
??????? gitee地址
皇冠新体育APP相关的文章
- 皇冠新体育APP:猿创征文|【C#编程指南】 文件系统和注册表编程指南与实战_会敲键盘的肘子
- RabbitMQ 使用_naki_bb
- 皇冠新体育APP:K8S集群部署前后端分离的微服务项目_u011663693_k8s部署微服务
- SpringBoot使用Nacos进行服务注册发现与配置管理_akenseren_nacos-discovery-spring-boot-starter
- 【汇编语言】栈区与SS:SP寄存器_Charon_cc_sp寄存器
- [BMZCTF-pwn] 12-csaw-ctf-2016-quals hungman_石氏是时试
- 【DBA100人】网联客CEO隋海峰:把握好职业生涯最重要的两个5年_OceanBase数据库官方
- java--swing界面实现注册登录(用文本文件存储数据)_一粒程序米_swing登陆后保存为临时文档
- C语言Visual Studio 2022的基本操作_你挡我发光了_visual studio2022 输入return输入r后再输入e
- 皇冠新体育APP:Spring Cloud微服务总结笔记_躺平君~从安卓到全栈
- C语言之文件操作_小白菜00_c语言文件操作
- WordPress REST API 内容注入/权限提升漏洞_huckers
- 记一次使用nacos2踩到的坑_linyb极客之路_nacos2.1.2
- 皇冠新体育APP:springcloud微服务国际化_左边的天堂_springcloud 国际化
- 皇冠新体育APP:在Unity中集成皇冠新体育APP开发与运行_我喜欢就喜欢_unity python
- 数据挖掘与机器学习课程总结_飞今天也很开心