前言
本文介绍Springboot相关的实用知识,如项目目录介绍、热部署、权限认证。
Springboot项目目录
常用目录划分
-
代码层(java)
- 启动类(XXApplication.java)
- 实体类(entity/domain/pojo)
- 数据传输对象(dto)
数据传输对象(Data Transfer Object)用于封装多个实体类(domain)之间的关系,不破坏原有的实体类结构 - 视图包装对象(vo)
视图包装对象(View Object)用于封装客户端请求的数据,防止部分数据泄露(如:管理员ID),保证数据安全,不破坏原有的实体类结构 - 数据接口访问层(mapper/dao)
- 数据服务接口层(service)
- 数据服务实现层(service.impl)
- 前端控制器层(controller)
- 工具类库(utils)
- 配置类(config)
-
资源层根目录(resources)
- 项目配置文件:application.yml
- 静态资源目录:/static/
用于存放html、css、js、图片等资源 - 视图模板目录:/templates/
用于存放jsp、thymeleaf等模板文件 - mybatis映射文件:/mapper/
- mybatis配置文件:/mapper/config/
代码层根目录命名规则
- indi
- 个体项目,指个人发起,但非自己独自完成的项目,可公开或私有项目,copyright主要属于发起者。
- 包名为“indi.发起者名.项目名.模块名.……”。
- pers
- 个人项目,指个人发起,独自完成,可分享的项目,copyright主要属于个人。
- 包名为“pers.个人名.项目名.模块名.……”。
- priv
- 私有项目,指个人发起,独自完成,非公开的私人使用的项目,copyright属于个人。
- 包名为“priv.个人名.项目名.模块名.……”。
- team :
- 团队项目,指由团队发起,并由该团队开发的项目,copyright属于该团队所有。
- 包名为“team.团队名.项目名.模块名.……”。
- com :
- 公司项目,copyright由项目发起的公司所有。
- 包名为“com.公司名.项目名.模块名.……”。
热部署
什么是热部署
在实际开发中,反复修改类、页面等资源,每次修改后都是需要重新启动才生效,这样每次启动都很麻烦,浪费了大量的时间,热部署就解决了这个问题,热部署可以在我们修改完项目代码后不用重启服务器就能把项目代码部署到服务器中(即修改代码后可以快速生效);
由于Spring Boot应用只是普通的Java应用,所以JVM热交换(hot-swapping)也能开箱即用。不过JVM热交换能替换的字节码有限制,想要更彻底的解决方案可以使用Spring Loaded项目或JRebel。
开启热部署(spring-boot-devtools)
spring-boot-devtools 模块也支持应用快速重启(restart)。
此种方式的特点是作用范围广,系统的任何变动包括配置文件修改、方法名称变化都能覆盖,但是后遗症也非常明显,它是采用文件变化后重启的策略来实现了,主要是节省了我们手动点击重启的时间,提高了实效性,在体验上回稍差。spring-boot-devtools 默认关闭了模版缓存,如果使用这种方式不用单独配置关闭模版缓存。
引入依赖
导入devtools坐标后,当修改代码后,IntelliJ IDEA中使用Ctrl +F9
激活热部署,就可以把修改后的项目部署到服务器中了(目的和重启服务器一样了)
<!-- 支持应用快速重启(restart)-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
</dependency>
自动启动配置
第一步
第二步
修改代码后,鼠标焦点离开idea工具(比如点击window系统任务栏),5s后自动启动热部署
热部署配置
# spring配置
spring:
# 热部署配置
devtools:
restart:
# enabled: false
exclude: application.yml,db/migration/**
- enabled:设置为
false
,代表关闭热部署(全局关闭热部署) - exclude:用于设置不参与热部署的文件或者文件夹,即这些文件或者文件夹修改后,需要重启服务器才能生效(当enabled不设置为false时,才能生效)
配置文件(application.yml)
参数配置
核心依赖
<!-- 读取配置文件-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
自定义参数
# 项目相关配置
project:
# 名称
name: autotestPlatform
# 版本
version: 1.0.0
简单类型注入(依赖注入)
方式一:@Value 直接简单注入:注解读取、加载配置文件中的自定义配置项
package com.aegis.config;
import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
//@ConfigurationProperties(prefix = "project")
@Data
public class ProjectConfig {
/**
* 项目名称
*/
@Value("${project.name}")
private String name;
/**
* 版本
*/
@Value("${project.version}")
private String version;
}
方式二:@ConfigurationProperties(prefix = "xxx")
批量注入:解决了当存在大量自定义配置项时,需要很多个@Value注解一一绑定,不够优雅的问题
注意:
- 类字段名称定义要与配置项名称相匹配;若配置项中名称含-短横线,则相应的类中字段名为驼峰格式
- 添加@Component注解进行实例化
package com.aegis.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "project")
@Data
public class ProjectConfig {
/**
* 项目名称
*/
private String name;
/**
* 版本
*/
private String version;
}
使用时在@Configuration
注解的配置类里面使用@Resource
自动装配
package com.aegis.config;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.*;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spi.service.contexts.SecurityContext;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
import javax.annotation.Resource;
import java.util.ArrayList;
import java.util.List;
/**
* Swagger2的接口配置
*
* @author admin
*/
//表示这个类是一个配置类,会把这个类注入到ioc容器中
@Configuration
//开启swagger2的功能
@EnableSwagger2
public class SwaggerConfig {
/**
* 系统基础配置
*/
@Resource
private ProjectConfig projectConfig;
/** 是否开启swagger */
@Value("${swagger.enabled}")
private boolean enabled;
/** 设置请求的统一前缀 */
@Value("${swagger.pathMapping}")
private String pathMapping;
/**
* 创建API
*/
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2)
// 是否启用Swagger
.enable(enabled)
// 用来创建该API的基本信息,展示在文档的页面中(自定义展示的信息)
.apiInfo(apiInfo())
// 设置哪些接口暴露给Swagger展示
.select()
// 扫描所有有注解的api,用这种方式更灵活
.apis(RequestHandlerSelectors.withMethodAnnotation(ApiOperation.class))
// 扫描指定包中的swagger注解
// .apis(RequestHandlerSelectors.basePackage("com.aegis.project.tool.swagger"))
// 扫描所有 .apis(RequestHandlerSelectors.any())
.paths(PathSelectors.any())
.build()
.pathMapping(pathMapping)
/* 设置安全模式,swagger可以设置访问token */
.securitySchemes(securitySchemes())
.securityContexts(securityContexts());
}
/**
* 添加摘要信息
*/
private ApiInfo apiInfo() {
// 用ApiInfoBuilder进行定制
return new ApiInfoBuilder()
// 设置标题
.title("标题:自动化测试平台系统_接口文档")
// 描述
.description("描述:用于管理自动化测试平台系统信息,具体包括XXX,XXX模块...")
// 作者信息
.contact(new Contact(projectConfig.getName(), null, null))
// 版本
.version("版本号:" + projectConfig.getVersion())
.build();
}
/**
* 安全模式,这里指定token通过Authorization头请求头传递
*/
private List<ApiKey> securitySchemes() {
List<ApiKey> apiKeyList = new ArrayList<ApiKey>();
apiKeyList.add(new ApiKey("Authorization", "Authorization", "header"));
return apiKeyList;
}
/**
* 安全上下文
*/
private List<SecurityContext> securityContexts() {
List<SecurityContext> securityContexts = new ArrayList<>();
securityContexts.add(
SecurityContext.builder()
.securityReferences(defaultAuth())
.forPaths(PathSelectors.regex("^(?!auth).*$"))
.build());
return securityContexts;
}
/**
* 默认的安全上引用
*/
private List<SecurityReference> defaultAuth() {
AuthorizationScope authorizationScope = new AuthorizationScope("global", "accessEverything");
AuthorizationScope[] authorizationScopes = new AuthorizationScope[1];
authorizationScopes[0] = authorizationScope;
List<SecurityReference> securityReferences = new ArrayList<>();
securityReferences.add(new SecurityReference("Authorization", authorizationScopes));
return securityReferences;
}
}
端口设置
# tomcat配置
server:
# TODO 配置启动端口
port: 80
tomcat:
uri-encoding: UTF-8
多环境配置(多套配置中心)
- 主配置文件中指定启动环境
定义一个application.yml里面写上spring.profiles.active=test 设置默认启动test文件 - 根据需要环境的多少,定义环境配置文件(格式application-环境名.yaml)
- 设置开发环境
定义application-dev.yml - 设置测试环境
定义application-test.yml - 设置生产环境
定义application-prod.yml
- 设置开发环境
- 使用
java -jar xx.jar --spring.profiles.active=dev
设置运行启动dev环境(xx.jar为打包的包名)
案例:多环境端口配置
第一步:application.yml 设置启动环境为test
# spring配置
spring:
# 指定启动环境
profiles:
active: test
第二步:配置多个环境,比如开发环境dev和测试环境test
application-dev.yml 设置dev环境端口为81
# tomcat配置
server:
# TODO 配置启动端口
port: 81
application-test.yml设置test环境端口为82
# tomcat配置
server:
# TODO 配置启动端口
port: 82
第三步:启动验证效果
对象映射器
应用场景
- 对象序列化:【对象的状态信息转换为可以存储或传输的形式的过程【Json数据】】
- 反序列化:【Json字符串进行反向操纵、生成对应的实体类等信息】
- 指定前端显示日期格式
- 【业务分析:后端存入的时间数据往往是 2021-05-27 08:00:00 具体数据、一般前端显示不需要整个数据、所以需要对时间数据进行固定显示。 前后端都可解决此问题。 前端一般拆分每个对应数据(时分秒等)做拼接、后端则可通过工具进行传输对应格式数据】
- 前端传入Long类型数据确实精度问题解决
- 【业务分析:当长整型数据传给前端(通常主键雪花算法生成)、Js处理数据返回时、会出现精度缺失(后几位变为0)、这里一般也分为前后端解决】
- 前端解决:利用bignumber.js 按千分位格式话数据(不会有精度问题、不会有溢出问题)
- 后端解决:通常主键雪花算法生成ID呈64位、如果用Long传输、则会出现问题。那么我们可以采取兼容性最强的String类型进行定义数据类型。
- 择中解决:利用Jackson提供的ObjectMapper将数据转化成String类型
创建工具类(JacksonObjectMapper)
package com.aegis.common;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;
import java.math.BigInteger;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
/**
* 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
* 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
* 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
*/
public class JacksonObjectMapper extends ObjectMapper {
public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
public static final String DEFAULT_TIME_FORMAT = "HH-mm-ss";
public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH-mm-ss";
public JacksonObjectMapper(){
super();
//收到未知属性时不报异常
this.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES,false);
//反序列化时,属性不存在的兼容处理
this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
SimpleModule simpleModule = new SimpleModule()
.addDeserializer(LocalDate.class,new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addDeserializer(LocalTime.class,new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addDeserializer(LocalDateTime.class,new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
.addSerializer(BigInteger.class, ToStringSerializer.instance)
.addSerializer(Long.class, ToStringSerializer.instance)
.addSerializer(LocalDate.class,new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
.addSerializer(LocalTime.class,new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
.addSerializer(LocalDateTime.class,new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)));
//注册功能模块 例如,可以添加自定义序列化器和反序列化器
this.registerModule(simpleModule);
}
}
配置扩展mvc框架的消息转换器
package com.aegis.config;
import cn.dev33.satoken.interceptor.SaRouteInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.aegis.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
import java.util.List;
@Slf4j
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
/**
* 注册Sa-Token的注解拦截器
* @param registry
*/
@Override
protected void addInterceptors(InterceptorRegistry registry) {
// 注册Sa-Token的注解拦截器,打开注解式鉴权功能,并排除不需要注解鉴权的接口地址 (与登录拦截器无关)
registry.addInterceptor(new SaRouteInterceptor((req,res,handler)->{
// 登录认证 -- 拦截所有路由,并排除/user/login 用于开放登录
SaRouter.match("/**","/user/login",r-> StpUtil.checkLogin());
// 角色认证
SaRouter.match("/admin/**",r->StpUtil.checkRoleOr("admin","super-admin"));
// 权限认证(不同模块认证不同权限)
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/account/**", r -> StpUtil.checkPermission("account"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
})).addPathPatterns("/**");
}
/**
* 设置静态资源映射
* @param registry
*/
@Override
protected void addResourceHandlers(ResourceHandlerRegistry registry) {
// 静态资源映射
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
}
效果
返回日期数据变化
权限认证(sa-token)
sa-token是一个轻量级 Java权限认证框架,主要解决:
登录认证、权限认证、Session会话、单点登录、OAuth2.0、微服务网关鉴权
等一系列权限相关问题。简化了我们开发权限管理的业务逻辑。
更多了解参考:sa-token官方文档
核心依赖
<!-- Sa-Token 权限认证,在线文档:https://sa-token.cc -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot-starter</artifactId>
</dependency>
<!-- Sa-Token 整合 Redis (使用jackson序列化方式) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-dao-redis-jackson</artifactId>
</dependency>
<!-- Sa-Token插件:权限缓存与业务缓存分离 -->
<!-- <dependency>-->
<!-- <groupId>cn.dev33</groupId>-->
<!-- <artifactId>sa-token-alone-redis</artifactId>-->
<!-- </dependency>-->
<!-- 提供Redis连接池 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
数据库设计(RBAC模型)
RBAC 是基于角色的访问控制(Role-Based Access Control )在RBAC中,权限与角色相关联,用户通过成为适当角色的成员而得到这些角色的权限。这就极大地简化了权限的管理。这样管理都是层级相互依赖的,权限赋予给角色,而把角色又赋予用户,这样的权限设计很清楚,管理起来很方便。
# 用户表
DROP TABLE IF EXISTS auth_user;
CREATE TABLE auth_user(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
updated_time timestamp COMMENT '修改时间' ,
username VARCHAR(255) COMMENT '用户名' ,
password VARCHAR(255) COMMENT '密码' ,
email VARCHAR(255) COMMENT '邮箱' ,
phone INTEGER COMMENT '手机号' ,
is_deleted VARCHAR(1) DEFAULT 0 COMMENT '是否删除;0:未删除,1:已删除' ,
is_enable VARCHAR(1) DEFAULT 0 COMMENT '是否启用;0:未启用,1:启用' ,
PRIMARY KEY (id)
) COMMENT = '用户信息';
# 角色表
DROP TABLE IF EXISTS auth_role;
CREATE TABLE auth_role(
id INT NOT NULL COMMENT '唯一标识' ,
created_time DATETIME DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '角色名称' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '角色';
# 权限表
DROP TABLE IF EXISTS auth_permit;
CREATE TABLE auth_permit(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '权限名称' ,
url VARCHAR(255) COMMENT '授权路径' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '权限';
# 部门表
DROP TABLE IF EXISTS auth_org;
CREATE TABLE auth_org(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
name VARCHAR(255) COMMENT '部门名称' ,
remark VARCHAR(255) COMMENT '备注' ,
PRIMARY KEY (id)
) COMMENT = '部门机构';
# 用户角色表
DROP TABLE IF EXISTS auth_user_role;
CREATE TABLE auth_user_role(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
user_id INT COMMENT '用户id' ,
role_id INT COMMENT '角色id' ,
PRIMARY KEY (id)
) COMMENT = '用户角色表';
# 角色权限表
DROP TABLE IF EXISTS auth_role_permit;
CREATE TABLE auth_role_permit(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
role_id INT COMMENT '角色ID' ,
permit_id INT COMMENT '权限ID' ,
PRIMARY KEY (id)
) COMMENT = '角色权限表';
# 部门用户表
DROP TABLE IF EXISTS auth_org_user;
CREATE TABLE auth_org_user(
id INT NOT NULL COMMENT '唯一标识' ,
created_time timestamp DEFAULT now() COMMENT '创建时间' ,
org_id INT COMMENT '部门id' ,
user_id INT COMMENT '用户id' ,
PRIMARY KEY (id)
) COMMENT = '部门用户表';
Sa-Token配置(yml文件)
# Sa-Token配置
sa-token:
# token名称 (同时也是cookie名称)
token-name: Authorization
# 自定义token前缀: Token前缀 与 Token值 之间必须有一个空格
token-prefix: Bearer
# token有效期,单位s 默认30天, -1代表永不过期
timeout: 2592000
# 是否尝试从 header 里读取 Token
is-read-head: true
# 是否开启自动续签
auto-renew: true
# 临时有效期,单位s,例如将其配置为 1800 (30分钟),代表用户如果30分钟无操作,则此Token会立即过期
activity-timeout: -1
# 是否允许同一账号并发登录 (为true时允许一起登录, 为false时同端互斥)
is-concurrent: true
# 在多人登录同一账号时,是否共用一个token (为true时所有登录共用一个token, 为false时每次登录新建一个token)
is-share: true
# token风格,如uuid、random-128
token-style: uuid
# 是否输出操作日志
is-log: false
# # 配置 Sa-Token 单独使用的 Redis 连接
# alone-redis:
# # Redis数据库索引(默认为0)
# database: 0
# # Redis服务器地址
# host: 127.0.0.1
# # Redis服务器连接端口
# port: 6379
# # Redis服务器连接密码(默认为空)
# password: 123456
# # 连接超时时间
# timeout: 10s
当sa-tooken集成redis时,需要配置redis
# spring配置
spring:
# TODO redis配置
redis:
# Redis数据库索引(默认为0)
database: 0
# Redis服务器地址
host: 127.0.0.1
# Redis服务器连接端口
port: 16380
# Redis服务器连接密码(默认为空)
# password:
# 连接超时时间
timeout: 10s
lettuce:
pool:
# 连接池最大连接数
max-active: 200
# 连接池最大阻塞等待时间(使用负值表示没有限制)
max-wait: -1ms
# 连接池中的最大空闲连接
max-idle: 10
# 连接池中的最小空闲连接
min-idle: 0
授权认证异常处理器
package com.aegis.common;
import cn.dev33.satoken.exception.DisableLoginException;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.exception.NotPermissionException;
import cn.dev33.satoken.exception.NotRoleException;
import com.aegis.common.api.R;
import com.aegis.common.api.ResultCode;
import io.swagger.annotations.ApiModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.NotReadablePropertyException;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolationException;
import java.sql.SQLIntegrityConstraintViolationException;
@ApiModel(value = "全局统一异常处理")
@RestControllerAdvice
@ResponseBody
@Slf4j
public class GlobalExceptionHandler {
/**
自定义业务异常处理
*/
@ExceptionHandler(BusinessException.class)
public R<String> handleBusinessExceptionHandler(BusinessException ex){
log.error("BusinessException异常:{}", ex.getMessage());
return R.fail(ResultCode.FAILED,ex.getMessage());
}
// @ExceptionHandler
@ExceptionHandler({NotLoginException.class, NotRoleException.class, NotPermissionException.class, DisableLoginException.class})
@ResponseStatus(HttpStatus.OK)
public R handleTokenException(Exception e, HttpServletRequest request, HttpServletResponse response) {
// 打印堆栈,以供调试
System.out.println("全局异常---------------");
// 如果是未登录异常
if(e instanceof NotLoginException){
return R.fail(ResultCode.UNAUTHORIZED, "未登录或登录已过期,请重新登录~");
}else if (e instanceof NotRoleException){
return R.fail(ResultCode.UNAUTHORIZED, "无此角色:"+ ((NotRoleException) e).getRole());
}else if (e instanceof NotPermissionException){
return R.fail(ResultCode.UNAUTHORIZED, "无此权限:"+ ((NotPermissionException) e).getCode());
}else if (e instanceof DisableLoginException){
return R.fail(ResultCode.UNAUTHORIZED, "账号被封禁:"+ + ((DisableLoginException) e).getDisableTime() + "秒后解封");
}
return R.fail(e.getMessage());
}
/**
* 请求参数异常处理
* @param ex
* @return
*/
@ExceptionHandler({MethodArgumentNotValidException.class})
@ResponseStatus(HttpStatus.OK)
public R<MethodArgumentNotValidException> handleMethodArgumentNotValidException(MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
StringBuilder sb = new StringBuilder("");
for (FieldError fieldError : bindingResult.getFieldErrors()) {
sb.append(fieldError.getField()).append(":").append(fieldError.getDefaultMessage());
}
String msg = sb.toString();
return R.fail(ResultCode.VALIDATE_FAILED, msg);
}
/**
* 参数格式异常处理
* @param ex
* @return
*/
@ExceptionHandler({ConstraintViolationException.class})
@ResponseStatus(HttpStatus.OK)
public R<ConstraintViolationException> handleConstraintViolationException(ConstraintViolationException ex) {
return R.fail(ResultCode.VALIDATE_FAILED, ex.getMessage());
}
/**
* 集合类型的请求参数异常处理
* @param ex
* @return
*/
@ExceptionHandler({NotReadablePropertyException.class})
@ResponseStatus(HttpStatus.OK)
public R<NotReadablePropertyException> handleNotReadablePropertyException(NotReadablePropertyException ex) {
StringBuilder sb =new StringBuilder(ex.getPropertyName());
String propertyName = sb.substring(sb.indexOf("[") + 1,sb.indexOf("]"));
int index = Integer.parseInt(propertyName)+1;
return R.fail(ResultCode.VALIDATE_FAILED,"第"+index+"条数据存在问题");
}
@ExceptionHandler(SQLIntegrityConstraintViolationException.class)
public R<String> handleSQLIntegrityConstraintViolationException(SQLIntegrityConstraintViolationException ex){
log.error(ex.getMessage());
if(ex.getMessage().contains("Duplicate entry")){
String[] split = ex.getMessage().split(" ");
String msg = split[2] + "已存在";
return R.fail(msg);
}
return R.fail("未知错误");
}
}
自定义token拦截器(TokenInterceptor)
实现效果
- token自动续期
- 当用户一直在操作页面请求服务器时,token应该是需要一直有效的,不能那个页面点着点着就告诉用户需要重新登录吧,除非是用户长时间没有请求服务器了,才需要重新登录。
- 当token的长久有效期到期后,必定过期,无法继续使用;若想续签,需手动调用
StpUtil.renewTimeout(100)
方法;自定义拦截器预期效果是不管调用哪个接口,token都会续签
- token定期刷新
- 如果token长时间续期或者token的有效期很长,token值一直不变的话可能不太安全,所以需要加个定期刷新token的功能。同样在拦截器中实现,获取token的创建时间,当创建时间距离当前时间超过了两个小时,就生成一个新的token,并设置到响应头中
如果是前后端分离的项目(不支持Cookie 功能),每次请求后主动获取更新后的token似乎并不优雅,待研究??
package com.aegis.common;
import cn.dev33.satoken.stp.StpUtil;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* 自定义token拦截器( token定期刷新 和 token续期)
*/
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
response.setHeader("Access-Control-Allow-Origin",request.getHeader("Origin"));
response.setHeader("Access-Control-Allow-Credentials","true");
response.setHeader("Referrer-Policy","no-referrer");
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// token定期刷新(如果是前后端分离的项目,不支持Cookie 功能,每次请求后主动获取更新后的token似乎并不优雅,待研究??)
// 获取当前token(这个token获取的是请求头的token,也可以用 request 获取)
String tokenValue = StpUtil.getTokenValue();
// 根据token获取用户id(这里如果找不到id直接返回null,不会报错)
String loginUserId = (String) StpUtil.getLoginIdByToken(tokenValue);
//判断token的创建时间是否大于2小时,如果是的话则需要刷新token
long time = System.currentTimeMillis()-StpUtil.getSession().getCreateTime();
long hour = time/1000/(60*60);
if(hour>2){
// 如果在login时,存在在session中存储用户信息的行为,则需在退出登录前取出用户信息,重新登录后在重新存储用户信息到session
// 要生成新的token的话,要先退出再重新登录
StpUtil.logout();
StpUtil.login(loginUserId);
}
// token续期
if(StpUtil.getTokenTimeout()!=-1){
StpUtil.renewTimeout(3600);
}
return true;
}
}
注册Sa-Token拦截器
根据实际业务需求,进行路由认证配置
package com.aegis.config;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.interceptor.SaRouteInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.aegis.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 注册Sa-Token的拦截器
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// // 注册token拦截器:实现token定期刷新、token续期
// registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**")
// .excludePathPatterns("/sso/login","/sso/getCode","/upload/**");
// 注册Sa-Token的注解拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
// 注册Sa-Token的路由拦截器
registry.addInterceptor(new SaRouteInterceptor((req, res, handler)->{
// 登录认证 -- 拦截所有路由,并排除/sso/** 用于开放登录
SaRouter
// 拦截的 path 列表,可以写多个 */
.match("/**")
// 排除掉的 path 列表,可以写多个
.notMatch("/sso/**")
// 要执行的校验动作,可以写完整的 lambda 表达式
.check(r-> StpUtil.checkLogin());
// 角色认证
SaRouter.match("/admin/**",r->StpUtil.checkRoleOr("admin","super-admin"));
// 权限认证(不同模块认证不同权限)
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/account/**", r -> StpUtil.checkPermission("account"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
})).addPathPatterns("/**");
}
/**
* 设置静态资源映射
* @param registry
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 静态资源映射
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展mvc框架的消息转换器
* @param converters
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600)
.exposedHeaders();
}
}
业务代码编写
Mapper层
UserMapper
package com.aegis.mapper;
import com.aegis.entity.Permission;
import com.aegis.entity.Role;
import com.aegis.entity.User;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import java.util.List;
/**
* <p>
* 用户信息 Mapper 接口
* </p>
*
* @author chenjing
* @since 2022-11-22 14:21:10
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
List<Role> getRoleById(@Param("userId") Integer id);
List<Permission> getPermissionByUserId(@Param("userId") Integer id);
}
UserMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.aegis.mapper.UserMapper">
<!-- 通用查询映射结果 -->
<resultMap id="BaseResultMap" type="com.aegis.entity.User">
<id column="id" property="id" />
<result column="created_time" property="createdTime" />
<result column="updated_time" property="updatedTime" />
<result column="username" property="username" />
<result column="password" property="password" />
<result column="email" property="email" />
<result column="phone" property="phone" />
<result column="is_deleted" property="isDeleted" />
<result column="is_enable" property="isEnable" />
</resultMap>
<!-- 通用查询结果列 -->
<sql id="Base_Column_List">
id, created_time, updated_time, username, password, email, phone, is_deleted, is_enable
</sql>
<select id="getRoleById" resultType="com.aegis.entity.Role">
select auth_role.id,auth_role.name
from auth_user_role
left join auth_role on auth_user_role.role_id = auth_role.id
where user_id = #{userId}
</select>
<select id="getPermissionByUserId" resultType="com.aegis.entity.Permission">
select auth_permission.id,auth_permission.name
from auth_user_role
left join auth_role_permission on auth_user_role.role_id = auth_role_permission.role_id
left join auth_role on auth_user_role.role_id = auth_role.id
left join auth_permission on auth_role_permission.permission_id = auth_permission.id
where user_id = #{userId}
<!-- <if test="userType != null and userType != ''">-->
<!-- AND auth_role.name = #{userType}-->
<!-- </if>-->
</select>
</mapper>
自定义权限验证接口扩展(StpInterfaceImpl)
package com.aegis.service.impl;
import cn.dev33.satoken.stp.StpInterface;
import com.aegis.entity.Permission;
import com.aegis.entity.Role;
import com.aegis.mapper.UserMapper;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.stream.Collectors;
/**
* 自定义权限验证接口扩展
*/
@Component // 打开此注解,保证此类被springboot扫描,即可完成sa-token的自定义权限验证扩展
public class StpInterfaceImpl implements StpInterface {
@Resource
private UserMapper userMapper;
/**
* 返回一个账号所拥有的权限码集合
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
// 实际项目中要根据具体业务逻辑来查询权限
List<Permission> permission = userMapper.getPermissionByUserId(Integer.valueOf((String) loginId));
List<String> permissions = permission.stream().map(Permission::getName).collect(Collectors.toList());
return permissions;
}
/**
* 返回一个账号所拥有的角色标识集合
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
// 实际项目中要根据具体业务逻辑来查询角色
List<Role> role = userMapper.getRoleById(Integer.valueOf((String) loginId));
List<String> roles = role.stream().map(Role::getName).collect(Collectors.toList());
return roles;
}
}
控制层(LoginController)
package com.aegis.controller;
import cn.dev33.satoken.stp.StpUtil;
import com.aegis.common.api.R;
import com.aegis.entity.User;
import com.aegis.service.IUserService;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.validation.constraints.NotNull;
import java.util.HashMap;
@Validated
@RestController
@RequestMapping("/sso")
@Api(value = "LoginController",tags = "登录注册模块")
public class LoginController {
@Resource
private IUserService userService;
/**
* 登录:
* 多个终端登录可以写不同的登录接口,分别设置登录的设备。
* 也可以写一个统一的接口,在 request 中设置登录终端标识,根据这个标识来设置登录的设备。
* 退出登录同理
* */
@ApiOperation(value = "登录接口", notes = "使用账号密码登录用户")
@PostMapping("/login")
public R<HashMap<Object,Object>> login(@NotNull String name, @NotNull String password, String code, HttpServletRequest request){
LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(StringUtils.isNotBlank(name),User::getUsername,name);
// queryWrapper.eq(name != null ,User::getUsername,name);
User user = userService.getOne(queryWrapper);
if(user==null){
return R.fail("用户不存在!");
}
if (!"1".equals(user.getIsEnable())) {
return R.fail("用户未激活,请联系管理员!");
}
if(!password.equals(user.getPassword())){
return R.fail("用户名密码不正确!");
}
// sa-token登录
StpUtil.login(user.getId());
String token = StpUtil.getTokenValue();
HashMap<Object,Object> res = new HashMap<>();
res.put("info",user);
res.put("token",token);
return R.success(res);
}
// /** 登录 */
// @PostMapping("/login2")
// public R login2(String userName, String password, String code,HttpServletRequest request){
// //保存登录日志
// SysLog sysLog = new SysLog(IpUtil.getIpAddress(request),"用户登录","login");
// sysLog.setId(IdUtil.getSnowflakeNextIdStr());
// sysLog.setCreateBy(userName);
// sysLog.setState("登录成功");
// try {
// /*code = code.toUpperCase();
// Object verCode = redisUtil.get(BaseConstant.verCode+code);
// if (Objects.isNull(verCode)) {
// return R.error("验证码已失效,请重新输入");
// }
// redisUtil.del(BaseConstant.verCode+code);
// password = RSAUtil.decrypt(password); //密码私钥解密*/
// SysSafe safe = sysSafeService.list().get(0);
// SysUser user = passwordErrorNum(userName, password,safe);
// sysUserService.setDataScope(user); // 设置用户的数据范围查询条件
// StpUtil.login(user.getId());
// String tokenValue = StpUtil.getTokenValue();
// int i = safe.getIdleTimeSetting();
// //如果系统闲置时间为0,设置token和session永不过期
// if (i==0){
// SaTokenDao saTokenDao = SaManager.getSaTokenDao();
// SaTokenConfig config = SaManager.getConfig();
// saTokenDao.updateSessionTimeout(StpUtil.getSession().getId(),-1);
// saTokenDao.updateTimeout(BaseConstant.tokenCachePrefix+tokenValue,-1);
// saTokenDao.updateTimeout(BaseConstant.cachePrefix+"last-activity:"+tokenValue,-1);
// config.setActivityTimeout(-1);
// }
// sysLog.setInfo(userName+"登录成功");
// StpUtil.getSession().set("user",user);
// sysLogService.save(sysLog);
// return R.success(tokenValue);
// } catch (ExceptionVo e) {
// sysLog.setInfo(e.getMessage());
// sysLog.setState("登录失败");
// sysLogService.save(sysLog);
// return R.error(e.getCode(),e.getMessage());
// }catch (Exception e) {
// sysLog.setInfo(BaseConstant.UNKNOWN_EXCEPTION);
// sysLog.setState("登录失败");
// sysLogService.save(sysLog);
// e.printStackTrace();
// return R.error(BaseConstant.UNKNOWN_EXCEPTION);
// }
// }
//
// @PostMapping("/test")
// public R test(){
// Map<String,Object> map = new HashMap<>();
// SaSession session = StpUtil.getSession();
// //获取用户信息
// map.put("user",session.get("user"));
// //获取权限集合
// map.put("permission",StpUtil.getPermissionList());
// //获取token信息
// map.put("tokenInfo",StpUtil.getTokenInfo());
// return R.success(map);
// }
//
// /** 获取验证码 */
// @GetMapping(value = "/getCode")
// public void getCode(HttpServletResponse response) {
// try {
// response.setHeader("Pragma", "No-cache");
// response.setHeader("Cache-Control", "no-cache");
// response.setDateHeader("Expires", 0);
// response.setContentType("image/jpeg");
//
// RandomGenerator randomGenerator = new RandomGenerator(BaseConstant.captcha, 4);
// LineCaptcha lineCaptcha = CaptchaUtil.createLineCaptcha(100, 42,4,50);
// lineCaptcha.setGenerator(randomGenerator);
// BufferedImage image = lineCaptcha.getImage();
// OutputStream out = response.getOutputStream();
// redisUtil.set(BaseConstant.verCode+lineCaptcha.getCode(), lineCaptcha.getCode(), 60);
// ImageIO.write(image, "png", out);
// } catch (Exception e) {
// e.printStackTrace();
// }
// }
//
//
//
// /** 退出登录 */
// @DeleteMapping("/logout")
// public R logout(HttpServletRequest request){
// //退出
// StpUtil.logout();
// //根据token退出
// //String token = request.getHeader(BaseConstant.tokenHeader);
// //StpUtil.logoutByTokenValue(token);
// //根据用户id和登录设备退出
// //StpUtil.logout(StpUtil.getLoginId(),"WEB");
// return R.success("退出登录成功");
// }
//
// /**
// * 产生public key
// */
// @GetMapping("/getKey")
// public R getKey() {
// String publicKey = RSAUtil.getPublicKey();
// System.out.println(publicKey);
// return R.success(publicKey);
// }
//
// /**
// * 判断账号是否锁定
// */
// private boolean lockedUser(long currentTime,String userName){
// boolean flag = false;
// if (redisUtil.hasKey(BaseConstant.ERROR_COUNT+userName)){
// long loginTime = Long.parseLong(redisUtil.hget(BaseConstant.ERROR_COUNT+userName, "loginTime").toString());
// int i = Integer.parseInt(redisUtil.hget(BaseConstant.ERROR_COUNT+userName,"errorNum").toString());
// if (i >= ERROR_COUNT && currentTime < loginTime){
// Duration between = LocalDateTimeUtil.between(LocalDateTimeUtil.of(currentTime), LocalDateTimeUtil.of(loginTime));
// throw new ExceptionVo(1004,"账号锁定中,还没到允许登录的时间,请"+between.toMinutes()+"分钟后再尝试");
// }else{
// flag = true;
// }
// }
// return flag;
// }
//
// /**
// * 密码错误次数验证
// */
// private SysUser passwordErrorNum(String userName,String password,SysSafe sysSafe) throws InvalidKeySpecException, NoSuchAlgorithmException {
// //查询用户
// SysUser user = sysUserService.getUserByName(userName);
// if (null == user){
// throw new ExceptionVo(1001,"用户不存在");
// }
// //根据前端输入的密码(明文),和加密的密码、盐值进行比较,判断输入的密码是否正确
// boolean authenticate = EncryptionUtil.authenticate(password, user.getPassword(), user.getSalt());
// if (authenticate) {
// //密码正确错误次数清零
// redisUtil.del(BaseConstant.ERROR_COUNT+userName);
// } else {
// long currentTime = System.currentTimeMillis();
// //判断账号是否锁定
// boolean flag = lockedUser(currentTime, userName);
//
// //错误3次,锁定15分钟后才可登陆 允许时间加上定义的登陆时间(毫秒)
// String str = "15";
// long timeStamp = System.currentTimeMillis()+900000;
// //密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
// if (sysSafe.getPwdLoginLimit()==1){
// ERROR_COUNT = 5;
// str = "30";
// timeStamp = System.currentTimeMillis()+1800000;
// }
// //密码登录限制(0:连续错3次,锁定账号15分钟。1:连续错5次,锁定账号30分钟)
// if (redisUtil.hasKey(BaseConstant.ERROR_COUNT+userName)){
// int i = Integer.parseInt(redisUtil.hget(BaseConstant.ERROR_COUNT+userName,"errorNum").toString());
// if (flag && i==ERROR_COUNT){
// redisUtil.hset(BaseConstant.ERROR_COUNT+userName,"errorNum",1);
// }else {
// redisUtil.hincr(BaseConstant.ERROR_COUNT+userName,"errorNum",1);
// }
// redisUtil.hset(BaseConstant.ERROR_COUNT+userName,"loginTime",timeStamp);
// }else {
// Map<String,Object> map = new HashMap<>();
// map.put("errorNum",1);
// map.put("loginTime",timeStamp);
// redisUtil.hmset(BaseConstant.ERROR_COUNT+userName, map, -1);
// }
// int i = Integer.parseInt(redisUtil.hget(BaseConstant.ERROR_COUNT+userName,"errorNum").toString());
// if (i==ERROR_COUNT){
// throw new ExceptionVo(1004,"您的密码已错误"+ERROR_COUNT+"次,现已被锁定,请"+str+"分钟后再尝试");
// }
// throw new ExceptionVo(1000,"密码错误,总登录次数"+ERROR_COUNT+"次,剩余次数: " + (ERROR_COUNT-i));
// }
// return user;
// }
}
注解鉴权
通过注解来实现角色权限认证或者是菜单权限认证等。比如某个接口只有管理员角色才能访问,或者必须具有指定权限才能进入该方法。
注意:要使用注解鉴权,需要注册satoken自带的注解拦截器(SaAnnotationInterceptor)才能生效
AccountController
@SaCheckPermission("account:add")
@PostMapping("/save")
@ApiOperation("添加数据")
@ApiImplicitParams({@ApiImplicitParam(value = "token", paramType = "header", name = "token", required = true)})
public R<Account> save(@Validated(ValidationGroups.Register.class) @RequestBody Account account){
boolean flag = accountService.save(account);
if(flag){
return R.success(account);
}else{
return R.fail("添加数据失败");
}
}
效果验证
登录–>获取token
redis存储token
当应用服务重启时,无需重新登录获取token(登录时会自动存储到redis服务器),使用原先的token即可
未登录–>登录校验
登录成功,无权限–>权限校验
登录成功,无细分权限–>接口业务处理
登录成功,无模块权限–>接口业务处理
登录成功,有权限–>接口业务处理
参考资料
【SaToken使用】springboot+redis+satoken权限认证
无token放行swagger路径
src/main/java/com/aegis/config/WebMvcConfig.java
核心配置
// 排除swagger相关
.excludePathPatterns("/swagger**/**")
.excludePathPatterns("/webjars/**")
// swagger2.0为V2,swagger3.0为V3
.excludePathPatterns("/v2/**")
.excludePathPatterns("/doc.html");
整体配置
package com.aegis.config;
import cn.dev33.satoken.interceptor.SaAnnotationInterceptor;
import cn.dev33.satoken.interceptor.SaRouteInterceptor;
import cn.dev33.satoken.router.SaRouter;
import cn.dev33.satoken.stp.StpUtil;
import com.aegis.common.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
@Slf4j
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* 注册Sa-Token的拦截器
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
// // 注册token拦截器:实现token定期刷新、token续期
// registry.addInterceptor(new TokenInterceptor()).addPathPatterns("/**")
// .excludePathPatterns("/sso/login","/sso/getCode","/upload/**");
// 注册Sa-Token的注解拦截器
registry.addInterceptor(new SaAnnotationInterceptor()).addPathPatterns("/**");
// 注册Sa-Token的路由拦截器
registry.addInterceptor(new SaRouteInterceptor((req, res, handler)->{
// 登录认证 -- 拦截所有路由,并排除/sso/** 用于开放登录
SaRouter
// 拦截的 path 列表,可以写多个 */
.match("/**")
// 排除掉的 path 列表,可以写多个
.notMatch("/sso/**")
// 要执行的校验动作,可以写完整的 lambda 表达式
.check(r-> StpUtil.checkLogin());
// 角色认证
SaRouter.match("/admin/**",r->StpUtil.checkRoleOr("admin","super-admin"));
// 权限认证(不同模块认证不同权限)
// SaRouter.match("/user/**", r -> StpUtil.checkPermission("user"));
SaRouter.match("/account/**", r -> StpUtil.checkPermission("account"));
// SaRouter.match("/admin/**", r -> StpUtil.checkPermission("admin"));
// SaRouter.match("/orders/**", r -> StpUtil.checkPermission("orders"));
// SaRouter.match("/notice/**", r -> StpUtil.checkPermission("notice"));
}))
.addPathPatterns("/**")
// 排除swagger相关
.excludePathPatterns("/swagger**/**")
.excludePathPatterns("/webjars/**")
// swagger2.0为V2,swagger3.0为V3
.excludePathPatterns("/v2/**")
.excludePathPatterns("/doc.html");;
}
/**
* 设置静态资源映射
*/
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 静态资源映射
log.info("开始进行静态资源映射...");
registry.addResourceHandler("/backend/**").addResourceLocations("classpath:/backend/");
registry.addResourceHandler("/front/**").addResourceLocations("classpath:/front/");
}
/**
* 扩展mvc框架的消息转换器
*/
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
log.info("扩展消息转换器...");
//创建消息转换器对象
MappingJackson2HttpMessageConverter messageConverter = new MappingJackson2HttpMessageConverter();
//设置对象转换器,底层使用Jackson将Java对象转为json
messageConverter.setObjectMapper(new JacksonObjectMapper());
//将上面的消息转换器对象追加到mvc框架的转换器集合中
converters.add(0,messageConverter);
}
/**
* 允许跨域调用的过滤器
*/
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
//允许所有域名进行跨域调用
config.addAllowedOriginPattern("*");
//允许跨越发送cookie
config.setAllowCredentials(true);
//放行全部原始头信息
config.addAllowedHeader("*");
//允许所有请求方法跨域调用
config.addAllowedMethod("*");
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT")
.maxAge(3600)
.exposedHeaders();
}
}
配置后可正常访问 http://localhost/swagger-ui.html,无需token登录验证
遗留问题:后端服务控制台会出现根路径“/”,token登录授权失败错误日志
常用工具类封装