皇冠新体育APP

IT技术之家

使用sa-token 进行权限控制_satoken使用_蜀黍是个小学生

发表准确时间:2023-08-23 02:43:22 Java 51次 标签:java 后端 皇冠新体育APP:spring boot
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地址