基于springboot和jwt的预约系统
1、依赖
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- mybatis plus -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.3.1</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<!-- mybatis plus generator 代码生成器 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>2.0</version>
</dependency>
<!-- swagger2 -->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.4.0</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.4.0</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.4</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</dependency>
<!-- JWT-->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<!-- 坑!! 不引入下面依赖JWT报错-->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-impl</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.sun.xml.bind</groupId>
<artifactId>jaxb-core</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1.1</version>
</dependency>
<!-- easy excel-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>2.1.1</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi</artifactId>
<version>3.17</version>
</dependency>
<dependency>
<groupId>org.apache.poi</groupId>
<artifactId>poi-ooxml</artifactId>
<version>3.17</version>
</dependency>
<!-- sureness鉴权 -->
<dependency>
<groupId>com.usthe.sureness</groupId>
<artifactId>sureness-core</artifactId>
<version>1.0.3</version>
</dependency>
</dependencies>
2、建表 使用mybatisplus代码生成器
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.DataSourceConfig;
import com.baomidou.mybatisplus.generator.config.GlobalConfig;
import com.baomidou.mybatisplus.generator.config.PackageConfig;
import com.baomidou.mybatisplus.generator.config.StrategyConfig;
import com.baomidou.mybatisplus.generator.config.rules.NamingStrategy;
public class Main {
public static void main(String[] args) {
//创建AutoGenerator对象
AutoGenerator autoGenerator = new AutoGenerator();
//数据源
DataSourceConfig dataSourceConfig = new DataSourceConfig();
dataSourceConfig.setUrl("jdbc:mysql://localhost:3306/yuyue?useUnicode=true&useSSL=false&characterEncoding=UTF-8");
dataSourceConfig.setDriverName("com.mysql.cj.jdbc.Driver");
dataSourceConfig.setUsername("root");
dataSourceConfig.setPassword("123456");
autoGenerator.setDataSource(dataSourceConfig);
//全局配置
GlobalConfig globalConfig = new GlobalConfig();
//System.getProperty("user.dir")工程绝对路径
globalConfig.setOutputDir(System.getProperty("user.dir") + "/src/main/java");
globalConfig.setAuthor("william");
//创建后不打开文件
globalConfig.setOpen(false);
autoGenerator.setGlobalConfig(globalConfig);
// 包信息
PackageConfig packageConfig = new PackageConfig();
packageConfig.setParent("com.william");
//各层存放位置
packageConfig.setController("controller");
packageConfig.setService("service");
packageConfig.setServiceImpl("service.impl");
packageConfig.setEntity("entity");
packageConfig.setMapper("mapper");
autoGenerator.setPackageInfo(packageConfig);
// 配置策略
StrategyConfig strategyConfig = new StrategyConfig();
//entity生成后自动使用lombok注解
strategyConfig.setEntityLombokModel(true);
//开启下划线转驼峰
strategyConfig.setNaming(NamingStrategy.underline_to_camel);
autoGenerator.setStrategy(strategyConfig);
//开始生成
autoGenerator.execute();
}
}
然后在启动类加上@MapperScan("com.william.mapper")
# application.yml
server:
port: 8081
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
url: jdbc:mysql://localhost:3306/yuyue?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=UTF-8
# 环境设置:dev、test、prod
profiles:
active: dev
#mybatis日志
logging:
level:
root: warn
#配置日志级别
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath*:**/mapper/xml/*.xml
type-enums-package: com.william.enums
3、统一结果返回
public interface ResultCode {
public static Integer SUCCESS = 200;
public static Integer ERROR = 400;
}
package com.william.commonUtils;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
@Data
public class R {
private Boolean success;
private Integer code;
private String msg;
private Map<String, Object> data = new HashMap<>();
private R() {
}
public static R ok() {
R r = new R();
r.setCode(ResultCode.SUCCESS);
r.setSuccess(true);
r.setMsg("成功");
return r;
}
public static R error() {
R r = new R();
r.setCode(ResultCode.ERROR);
r.setSuccess(false);
r.setMsg("失败");
return r;
}
public R success(Boolean success) {
this.setSuccess(success);
return this;
}
public R message(String message) {
this.setMsg(message);
return this;
}
public R code(Integer code) {
this.setCode(code);
return this;
}
public R data(String key, Object value) {
this.data.put(key, value);
return this;
}
public R data(Map<String, Object> map) {
this.setData(map);
return this;
}
}
4、全局异常处理
/**
* 自定义异常类
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class MyException extends RuntimeException {
/**
* 状态码
*/
private Integer code;
/**
* 异常信息
*/
private String msg;
}
/**
* 异常处理
*/
@ControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
/**
* 全局异常处理
*
* @param e
* @return
*/
@ExceptionHandler(Exception.class)
@ResponseBody
public R error(Exception e) {
e.printStackTrace();
return R.error().message("执行了全局异常处理:");
}
/**
* 特定异常处理
*
* @param e
* @return
*/
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public R error(ArithmeticException e) {
e.printStackTrace();
return R.error().message("执行了特定异常");
}
/**
* 自定义异常处理
*
* @param e
* @return
*/
@ExceptionHandler(MyException.class)
@ResponseBody
public R error(MyException e) {
e.printStackTrace();
log.error(e.getMsg());
return R.error().code(e.getCode()).message(e.getMsg());
}
}
5、通用枚举
public enum AppointStatusEnum {
/**
* 用户登录状态的枚举类
*/
EFFECTIVE(0,"effective"),
CANCELLED(1,"cancelled"),
EXPIRED(2,"expired");
@EnumValue
private Integer code;
@JsonValue
private String msg;
AppointStatusEnum(Integer code, String msg) {
this.code = code;
this.msg = msg;
}
}
// 修改entity中属性的类型
/**
* 预约状态
*/
private AppointStatusEnum status;
6、字段自动填充
@Component
public class MyMetaObjectHandler implements MetaObjectHandler {
@Override
public void insertFill(MetaObject metaObject) {
//属性名称,不是字段名称
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateTime", new Date(), metaObject);
}
}
/**
* 创建时间
*/
@TableField(fill = FieldFill.INSERT)
private Date createTime;
/**
* 更新时间
*/
@TableField(fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
7、逻辑删除
/**
* 开启逻辑删除
* 未删除:1 已删除:0
*/
@TableLogic(value = "1", delval = "0")
private Integer deleted;
8、编写业务代码
controller
@CrossOrigin
@RestController
@RequestMapping("/xxx")
分页+条件+联表查询
// mybatisplus分页插件配置
@Configuration
@MapperScan("com.william.mapper")
public class MybatisPlusConfig {
// 最新版
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
// VO
@Data
public class PageQueryVO {
/**
* 学号
*/
public String studentId;
/**
* 实验室号
*/
public String roomId;
/**
* 用户名
*/
public String userName;
/**
* 预约状态
*/
public Integer status;
}
// controller
@ApiOperation(value = "分页+条件+联表查询预约信息")
@PostMapping("/getAppointPage/{curPage}/{pageSize}")
public R getAppointPage(@PathVariable Integer curPage, @PathVariable Integer pageSize, @RequestBody(required = false) PageQueryVO pageQueryVO) {
Page<AppointPageVO> page = appointmentService.getAppointPageVO(curPage, pageSize, pageQueryVO);
if (page != null) {
return R.ok().data("page", page).message("查询成功");
} else {
return R.error().message("查询失败");
}
}
// service
public interface IAppointmentService extends IService<Appointment> {
Page<AppointPageVO> getAppointPageVO(Integer curPage, Integer pageSize, PageQueryVO pageQueryVO);
}
// service.impl
@Service
public class AppointmentServiceImpl extends ServiceImpl<AppointmentMapper, Appointment> implements IAppointmentService {
@Override
public Page<AppointPageVO> getAppointPageVO(Integer curPage, Integer pageSize, PageQueryVO pageQueryVO) {
Page<AppointPageVO> page = new Page<>(curPage, pageSize);
return baseMapper.pageList(page, pageQueryVO);
}
}
// mapper
public interface AppointmentMapper extends BaseMapper<Appointment> {
Page<AppointPageVO> pageList(@Param("page") Page<AppointPageVO> page, @Param("pageQueryVO") PageQueryVO pageQueryVO);
}
// mapper.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.william.mapper.AppointmentMapper">
<select id="pageList" resultType="com.william.entity.vo.AppointPageVO">
select a.id,
a.room_id,
a.date,
a.time,
a.status,
u.user_name,
u.student_id
from appointment a, `user` u
<where>
a.user_id = u.user_id
and u.deleted = 1
<if test="pageQueryVO.studentId != null and !(pageQueryVO.studentId).equals('')">and u.student_id=#{pageQueryVO.studentId}</if>
<if test="pageQueryVO.roomId != null and !(pageQueryVO.roomId).equals('')">and a.room_id=#{pageQueryVO.roomId}</if>
<if test="pageQueryVO.status != null and !(pageQueryVO.status).equals('')">and a.status=#{pageQueryVO.status}</if>
</where>
order by a.id desc
</select>
</mapper>
9、swagger2
@Configuration
@EnableSwagger2
public class SwaggerConfig {
//默认文档地址为 http://localhost:port/swagger-ui.html
@Bean
public Docket createRestApi() {
return new Docket(DocumentationType.SWAGGER_2) //指定Api类型为Swagger2
.apiInfo(apiInfo()) //指定文档汇总信息
.select()
.apis(RequestHandlerSelectors
.basePackage("com.william.controller")) //指定controller包路径
.paths(PathSelectors.any()) //指定展示所有controller
.build();
}
private ApiInfo apiInfo() {
//返回一个apiinfo
return new ApiInfoBuilder()
.title("预约系统") //文档页标题
.description("api文档") // 详细信息
.version("1.0.0") // 文档版本号
.termsOfServiceUrl("https://www.baidu.com") //网站地址
.build();
}
}
10、登录功能
MD5
/**
* MD5非对称加密工具类
*/
public final class MD5 {
public static String encrypt(String strSrc) {
try {
char hexChars[] = {'0', '1', '2', '3', '4', '5', '6', '7', '8',
'9', 'a', 'b', 'c', 'd', 'e', 'f'};
byte[] bytes = strSrc.getBytes();
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(bytes);
bytes = md.digest();
int j = bytes.length;
char[] chars = new char[j * 2];
int k = 0;
for (int i = 0; i < bytes.length; i++) {
byte b = bytes[i];
chars[k++] = hexChars[b >>> 4 & 0xf];
chars[k++] = hexChars[b & 0xf];
}
return new String(chars);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
throw new RuntimeException("MD5加密出错!!+" + e);
}
}
}
JwtUtil
public class JwtUtil {
private static String signature = "密钥";
private static String tokenName = "从request.header中取token的名称";
/**
* 根据用户信息生成JwtToken
*
* @param user 用户信息
* @return token
*/
public static String createToken(User user) {
JwtBuilder jwtBuilder = Jwts.builder();
return jwtBuilder
// header
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// payload
.claim("password", user.getPassword())
// userId存在Subject中
.setSubject(user.getUserId().toString())
// 24H过期
.setExpiration(DateUtil.tomorrow())
.setId(UUID.randomUUID().toString())
// signature
.signWith(SignatureAlgorithm.HS256, signature)
.compact();
}
/**
* 解析request.header中的token获取用户id并返回
*
* @param request
* @return
*/
public static String getUserIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader(tokenName);
if (StrUtil.hasEmpty(jwtToken)) {
return "";
}
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(jwtToken);
Claims jwsBody = claimsJws.getBody();
// 加密时Subject中存的是用户id
return jwsBody.getSubject();
}
/**
* 解析token返回body
*/
public static Claims getBodyByJwtToken(String jwtToken) {
JwtParser parser = Jwts.parser();
Jws<Claims> claimsJws = parser.setSigningKey(signature).parseClaimsJws(jwtToken);
// 加密时Subject中存的是用户id
return claimsJws.getBody();
}
}
service.impl
public String login(User user) {
String studentId = user.getStudentId();
String password = user.getPassword();
// 非空校验
if (StrUtil.hasEmpty(studentId) || StrUtil.hasEmpty(password)) {
throw new MyException(ResultCode.ERROR, "登陆失败");
}
// 根据学号查数据
QueryWrapper<User> wrapper = new QueryWrapper<User>();
wrapper.eq("student_id", studentId);
User userInBase = baseMapper.selectOne(wrapper);
// 判断学号是否存在
if (userInBase == null) {
throw new MyException(ResultCode.ERROR, "登陆失败,用户不存在");
}
//判断密码是否符合 前端传来的密码通过MD5加密后和数据库中比较
if (!MD5.encrypt(password).equals(userInBase.getPassword())) {
throw new MyException(ResultCode.ERROR, "登录失败,密码错误");
}
// 登录成功,状态设为在线并返回token
userInBase.setStatus(UserStatusEnum.ACTIVE);
baseMapper.updateById(userInBase);
return JwtUtil.createToken(userInBase);
}
根据token获取用户信息
@ApiOperation(value = "根据token返回用户信息和权限", notes = "token在请求中的header中携带")
@GetMapping("/getUserInfo")
public R getUserInfo(HttpServletRequest request) {
String userId = JwtUtil.getUserIdByJwtToken(request);
User user = userService.getById(userId);
if (user != null) {
user.setStatus(UserStatusEnum.ACTIVE);
userService.updateById(user);
Role role = roleService.getById(user.getRoleId());
LinkedList<String> rolesList = new LinkedList<>();
rolesList.add(role.getRoleKey());
// 前端根据返回的roles动态生成路由
return R.ok().data("userInfo", user).data("roles", rolesList).message("获取用户信息成功");
} else {
return R.error().message("获取用户信息失败");
}
}
11、使用excel导入用户
@Data
public class UserExcel {
/**
* 姓名
*/
@ExcelProperty("姓名")
private String userName;
/**
* 学号
*/
@ExcelProperty("学号")
private String studentId;
/**
* 学院
*/
@ExcelProperty("学院")
private String college;
/**
* 专业班级
*/
@ExcelProperty("专业班级")
private String major;
}
// 有个很重要的点 DemoDataListener 不能被spring管理,要每次读取excel都要new,然后里面用到spring可以构造方法传进去
/**
* easyExcel监听器
* @author william
*/
public class UploadDataListener extends AnalysisEventListener<UserExcel> {
private static final Logger LOGGER = LoggerFactory.getLogger(UploadDataListener.class);
/**
* 每隔10条存储数据库,实际使用中可以3000条,然后清理list ,方便内存回收
*/
private static final int BATCH_COUNT = 10;
List<UserExcel> list = new ArrayList<UserExcel>();
/**
* 假设这个是一个DAO,当然有业务逻辑这个也可以是一个service。当然如果不用存储这个对象没用。
*/
private IUserService userService;
/**
* 如果使用了spring,请使用这个构造方法。每次创建Listener的时候需要把spring管理的类传进来
*/
public UploadDataListener(IUserService userService) {
this.userService = userService;
}
/**
* 这个每一条数据解析都会来调用
*/
@Override
public void invoke(UserExcel data, AnalysisContext context) {
LOGGER.info("解析到一条数据:{}", JSONUtil.toJsonStr(data));
// 判断学号是否是已经存在数据库中的
String studentId = data.getStudentId();
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("student_id", studentId);
User one = userService.getOne(wrapper);
if (one == null){
list.add(data);
} else {
LOGGER.error("学号为{}的数据已存在!", studentId);
}
// 达到BATCH_COUNT了,需要去存储一次数据库,防止数据几万条数据在内存,容易OOM
if (list.size() >= BATCH_COUNT) {
saveData();
// 存储完成清理 list
list.clear();
}
}
/**
* 所有数据解析完成了 都会来调用
*
* @param context
*/
@Override
public void doAfterAllAnalysed(AnalysisContext context) {
// 这里也要保存数据,确保最后遗留的数据也存储到数据库
saveData();
LOGGER.info("所有数据解析完成!");
}
/**
* 加上存储数据库
*/
private void saveData() {
LOGGER.info("{}条数据,开始存储数据库!", list.size());
userService.saveBatch(list);
LOGGER.info("存储数据库成功!");
}
}
@ApiOperation(value = "通过excel导入学生信息")
@PostMapping("/addUserByExcel")
public R addUserByExcel(MultipartFile file) throws IOException {
userService.saveUsers(file, userService);
return R.ok().message("导入成功");
}
public void saveUsers(MultipartFile file, IUserService userService) {
try {
//获取文件输入流
InputStream inputStream = file.getInputStream();
// 调用方法读取文件
EasyExcel.read(inputStream, UserExcel.class, new UploadDataListener(userService)).sheet().doRead();
} catch (IOException e) {
e.printStackTrace();
}
}
12、logback日志
logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="10 seconds">
<!-- 日志级别从低到高分为TRACE < DEBUG < INFO < WARN < ERROR < FATAL,如果设置为WARN,则低于WARN的信息都不会输出 -->
<!-- scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true -->
<!-- scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒。当scan为true时,此属性生效。默认的时间间隔为1分钟。 -->
<!-- debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。 -->
<contextName>logback</contextName>
<!-- name的值是变量的名称,value的值时变量定义的值。通过定义的值会被插入到logger上下文中。定义变量后,可以使“${}”来使用变量。 -->
<property name="log.path" value="log" />
<!-- 彩色日志 -->
<!-- 配置格式变量:CONSOLE_LOG_PATTERN 彩色日志格式 -->
<!-- magenta:洋红 -->
<!-- boldMagenta:粗红-->
<!-- cyan:青色 -->
<!-- white:白色 -->
<!-- magenta:洋红 -->
<property name="CONSOLE_LOG_PATTERN"
value="%yellow(%date{yyyy-MM-dd HH:mm:ss}) |%highlight(%-5level) |%blue(%thread) |%blue(%file:%line) |%green(%logger) |%cyan(%msg%n)"/>
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<!--此日志appender是为开发使用,只配置最底级别,控制台输出的日志级别是大于或等于此级别的日志信息-->
<!-- 例如:如果此处配置了INFO级别,则后面其他位置即使配置了DEBUG级别的日志,也不会被输出 -->
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<level>DEBUG</level>
</filter>
<encoder>
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!-- 设置字符集 -->
<charset>UTF-8</charset>
</encoder>
</appender>
<!--输出到文件-->
<!-- 时间滚动输出 level为 INFO 日志 -->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset>
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 每天日志归档路径以及格式 -->
<fileNamePattern>${log.path}/info/log-info-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录info级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 WARN 日志 -->
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/warn/log-warn-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录warn级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>warn</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 时间滚动输出 level为 ERROR 日志 -->
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<!--日志文件输出格式-->
<encoder>
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
<charset>UTF-8</charset> <!-- 此处设置字符集 -->
</encoder>
<!-- 日志记录器的滚动策略,按日期,按大小记录 -->
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<fileNamePattern>${log.path}/error/log-error-%d{yyyy-MM-dd}.%i.log</fileNamePattern>
<timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
<maxFileSize>100MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
<!-- 此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!--
<logger>用来设置某一个包或者具体的某一个类的日志打印级别、以及指定<appender>。
<logger>仅有一个name属性,
一个可选的level和一个可选的addtivity属性。
name:用来指定受此logger约束的某一个包或者具体的某一个类。
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,
如果未设置此属性,那么当前logger将会继承上级的级别。
-->
<!--
使用mybatis的时候,sql语句是debug下才会打印,而这里我们只配置了info,所以想要查看sql语句的话,有以下两种操作:
第一种把<root level="INFO">改成<root level="DEBUG">这样就会打印sql,不过这样日志那边会出现很多其他消息
第二种就是单独给mapper下目录配置DEBUG模式,代码如下,这样配置sql语句会打印,其他还是正常DEBUG级别:
-->
<!--开发环境:打印控制台-->
<springProfile name="dev">
<!--可以输出项目中的debug日志,包括mybatis的sql日志-->
<logger name="com.william" level="INFO" />
<!--
root节点是必选节点,用来指定最基础的日志输出级别,只有一个level属性
level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF,默认是DEBUG
可以包含零个或多个appender元素。
-->
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="WARN_FILE" />
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
<!--生产环境:输出到文件-->
<springProfile name="pro">
<root level="INFO">
<appender-ref ref="CONSOLE" />
<appender-ref ref="DEBUG_FILE" />
<appender-ref ref="INFO_FILE" />
<appender-ref ref="ERROR_FILE" />
<appender-ref ref="WARN_FILE" />
</root>
</springProfile>
</configuration>
在全局异常处理类上加
@ControllerAdvice
@Slf4j
13、sureness鉴权
根据角色分配资源(RESTapi)
@Component
public class DatabaseAccountProvider implements SurenessAccountProvider {
@Resource
private IUserService userService;
@Override
public SurenessAccount loadAccount(String appId) {
return userService.loadAccount(appId);
}
}
// userService.impl
@Override
public SurenessAccount loadAccount(String appId) {
User user = baseMapper.selectById(appId);
if (user != null) {
DefaultAccount.Builder accountBuilder = DefaultAccount.builder(appId)
.setPassword(user.getPassword())
.setDisabledAccount(user.getDeleted() == 0);
Role role = roleMapper.selectById(user.getRoleId());
if (role != null) {
List<String> roles = new LinkedList<>();
roles.add(role.getRoleKey());
accountBuilder.setOwnRoles(roles);
}
return accountBuilder.build();
} else {
return null;
}
}
public class RefreshExpiredTokenException extends SurenessAuthenticationException {
public RefreshExpiredTokenException(String message) {
super(message);
}
}
public class PasswdSubjectCreator implements SubjectCreate {
private static final String jwtName = "token名字";
@Override
public boolean canSupportSubject(Object context) {
// define which request can be access
if (context instanceof HttpServletRequest) {
String token = ((HttpServletRequest) context).getHeader(jwtName);
return token != null;
} else {
return false;
}
}
@Override
public Subject createSubject(Object context) {
// create PasswordSubject from request
// 从jwt中拿用户名和密码做鉴权
String token = ((HttpServletRequest) context).getHeader(jwtName);
Claims body = JwtUtil.getBodyByJwtToken(token);
String username = body.getSubject();
String password = (String) body.get("password");
String remoteHost = ((HttpServletRequest) context).getRemoteHost();
String requestUri = ((HttpServletRequest) context).getRequestURI();
String requestType = ((HttpServletRequest) context).getMethod();
String targetUri = requestUri.concat("===").concat(requestType).toLowerCase();
return PasswordSubject.builder(username, password)
.setRemoteHost(remoteHost)
.setTargetResource(targetUri)
.build();
}
}
@Configuration
public class SurenessConfiguration {
@Bean
ProcessorManager processorManager(SurenessAccountProvider accountProvider) {
// 处理器processor初始化
List<Processor> processorList = new LinkedList<>();
// 使用了默认的支持NoneSubject的处理器NoneProcessor
NoneProcessor noneProcessor = new NoneProcessor();
processorList.add(noneProcessor);
// 使用了默认的支持JwtSubject的处理器JwtProcessor
JwtProcessor jwtProcessor = new JwtProcessor();
processorList.add(jwtProcessor);
// 使用了默认的支持PasswordSubject的处理器PasswordProcessor
PasswordProcessor passwordProcessor = new PasswordProcessor();
// 这里注意,PasswordProcessor需要对用户账户密码验证,所以其需要账户信息提供者来给他提供想要的账户数据,
// 这里的 SurenessAccountProvider accountProvider bean就是这个账户数据提供源,
// 其实现bean是上面讲到的 DatabaseAccountProvider bean,即数据库实现的账户数据提供者。
passwordProcessor.setAccountProvider(accountProvider);
processorList.add(passwordProcessor);
return new DefaultProcessorManager(processorList);
}
@Bean
TreePathRoleMatcher pathRoleMatcher() {
// 这里的PathTreeProvider databasePathTreeProvider 就是通过数据库来提供资源权限配置信息bean实例
// 下面我们再实例化一个通过文件sureness.yml提供资源权限配置信息的提供者
PathTreeProvider documentPathTreeProvider = new DocumentPathTreeProvider();
// 下面我们再实例化一个通过注解形式@RequiresRoles @WithoutAuth提供资源权限配置信息的提供者
AnnotationPathTreeProvider annotationPathTreeProvider = new AnnotationPathTreeProvider();
annotationPathTreeProvider.setScanPackages(Collections.singletonList("com.william.controller"));
// 设置注解扫描包路径,也就是你提供api的controller路径 annotationPathTreeProvider.setScanPackages(Collections.singletonList("com.usthe.sureness.sample.tom.controller"));
// 开始初始化资源权限匹配器,我们可以把上面三种提供者都加入到匹配器中为其提供资源权限数据,匹配器中的数据就是这三个提供者提供的数据集合。t
DefaultPathRoleMatcher pathRoleMatcher = new DefaultPathRoleMatcher();
pathRoleMatcher.setPathTreeProviderList(Arrays.asList(
documentPathTreeProvider,
annotationPathTreeProvider));
// 使用资源权限配置数据来建立对应的匹配树
pathRoleMatcher.buildTree();
return pathRoleMatcher;
}
@Bean
SubjectFactory subjectFactory() {
// 我们之前知道了可以有各种Processor来处理对应的Jwt,那这Subject怎么得到呢,就需要不同的SubjectCreator来根据请求信息创建对应的Subject对象供之后的流程使用
SubjectFactory subjectFactory = new SurenessSubjectFactory();
// 这里我们注册我们需要的SubjectCreator
subjectFactory.registerSubjectCreator(Arrays.asList(
// 注意! 强制必须首先添加一个 noSubjectCreator
new NoneSubjectServletCreator(),
// 注册用来创建PasswordSubject的creator
new BasicSubjectServletCreator(),
// 注册用来创建JwtSubject的creator
new JwtSubjectServletCreator(),
// 当然你可以自己实现一个自定义的creator,实现SubjectCreate接口即可
new PasswdSubjectCreator()));
return subjectFactory;
}
@Bean
SurenessSecurityManager securityManager(ProcessorManager processorManager,
TreePathRoleMatcher pathRoleMatcher, SubjectFactory subjectFactory) {
// 我们把上面初始化好的配置bean整合到一起初始化surenessSecurityManager
SurenessSecurityManager securityManager = SurenessSecurityManager.getInstance();
// 设置资源权限匹配者
securityManager.setPathRoleMatcher(pathRoleMatcher);
// 设置subject创建工厂
securityManager.setSubjectFactory(subjectFactory);
// 设置处理器processor管理者
securityManager.setProcessorManager(processorManager);
return securityManager;
}
}
过滤器
在启动类中开启@ServletComponentScan
@Order(1)
@WebFilter(filterName = "SurenessFilter", urlPatterns = "/*", asyncSupported = true)
public class SurenessFilter implements Filter {
/** logger **/
private static final Logger logger = LoggerFactory.getLogger(SurenessFilter.class);
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
try {
SubjectSum subject = SurenessSecurityManager.getInstance().checkIn(servletRequest);
// You can consider using SurenessContextHolder to bind subject in threadLocal
// if bind, please remove it when end
if (subject != null) {
SurenessContextHolder.bindSubject(subject);
}
} catch (IncorrectCredentialsException | UnknownAccountException | ExpiredCredentialsException e1) {
logger.debug("this request account info is illegal, {}", e1.getMessage());
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED).body("Username or password is incorrect or expired"), servletResponse);
return;
} catch (DisabledAccountException | ExcessiveAttemptsException e2 ) {
logger.debug("the account is disabled, {}", e2.getMessage());
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED).body("Account is disabled"), servletResponse);
return;
} catch (RefreshExpiredTokenException e4) {
logger.debug("this account credential token is expired, return refresh value");
Map<String, String> refreshTokenMap = Collections.singletonMap("refresh-token", e4.getMessage());
responseWrite(ResponseEntity
.status(HttpStatus.UNAUTHORIZED).body(refreshTokenMap), servletResponse);
return;
} catch (UnauthorizedException e5) {
logger.debug("this account can not access this resource, {}", e5.getMessage());
responseWrite(ResponseEntity
.status(HttpStatus.FORBIDDEN)
.body("This account has no permission to access this resource"), servletResponse);
return;
} catch (RuntimeException e) {
logger.error("other exception happen: ", e);
responseWrite(ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build(),
servletResponse);
return;
}
try {
// if ok, doFilter and add subject in request
filterChain.doFilter(servletRequest, servletResponse);
} finally {
SurenessContextHolder.clear();
}
}
/**
* write response json data
* @param content content
* @param response response
*/
private void responseWrite(ResponseEntity<?> content, ServletResponse response) {
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json;charset=utf-8");
((HttpServletResponse)response).setStatus(content.getStatusCodeValue());
content.getHeaders().forEach((key, value) ->
((HttpServletResponse) response).addHeader(key, value.get(0)));
try (PrintWriter printWriter = response.getWriter()) {
if (content.getBody() != null) {
if (content.getBody() instanceof String) {
printWriter.write(content.getBody().toString());
} else {
ObjectMapper objectMapper = new ObjectMapper();
printWriter.write(objectMapper.writeValueAsString(content.getBody()));
}
} else {
printWriter.flush();
}
} catch (IOException e) {
logger.error("responseWrite response error: ", e);
}
}
}
sureness.yml
## -- sureness.yml document dataSource-- ##
# load api resource which need be protected, config role who can access these resource.
# resources that are not configured are also authenticated and protected by default, but not authorized
# eg: /api/v2/host===post===[role2,role3,role4] means /api/v2/host===post can be access by role2,role3,role4
# eg: /api/v1/getSource3===get===[] means /api/v1/getSource3===get can not be access by any role
resourceRole:
- /room/editRoomInfoById===post===[admin,teacher]
- /room/getRoomList===get===[admin,teacher]
- /user/addUserByExcel===post===[admin,teacher]
- /user/changePassword===post===[student]
- /user/changePasswordById===post===[admin,teacher]
- /user/editUserInfo===post===[admin,teacher]
- /user/getUserList/**===post===[admin,teacher]
- /user/logout===post===[admin,teacher,student]
- /appointment/cancelAppointmentById/**===delete===[admin,teacher,student]
- /appointment/doAppointment===post===[admin,teacher,student]
- /appointment/getAppointPage/**===post===[admin,teacher]
# load api resource which do not need be protected, means them need be excluded.
# these api resource can be access by everyone
excludedResource:
- /user/login===post
- /room/getRoomInfoById/**===get
- /user/getUserInfo===get
- /user/getUserInfoById/*===get
- /appointment/getHistory/**===get
- /appointment/getRecent/**===get
- /**/*.css===get
- /**/*.ico===get
- /**/*.png===get
- /swagger-ui.html===*
- /swagger/**===*
- /swagger-resources/**===*
- /v2/**===*
- /webjars/**===*
- /configuration/**===*
st===[role2,role3,role4] means /api/v2/host===post can be access by role2,role3,role4
# eg: /api/v1/getSource3===get===[] means /api/v1/getSource3===get can not be access by any role
resourceRole:
- /room/editRoomInfoById===post===[admin,teacher]
- /room/getRoomList===get===[admin,teacher]
- /user/addUserByExcel===post===[admin,teacher]
- /user/changePassword===post===[student]
- /user/changePasswordById===post===[admin,teacher]
- /user/editUserInfo===post===[admin,teacher]
- /user/getUserList/**===post===[admin,teacher]
- /user/logout===post===[admin,teacher,student]
- /appointment/cancelAppointmentById/**===delete===[admin,teacher,student]
- /appointment/doAppointment===post===[admin,teacher,student]
- /appointment/getAppointPage/**===post===[admin,teacher]
# load api resource which do not need be protected, means them need be excluded.
# these api resource can be access by everyone
excludedResource:
- /user/login===post
- /room/getRoomInfoById/**===get
- /user/getUserInfo===get
- /user/getUserInfoById/*===get
- /appointment/getHistory/**===get
- /appointment/getRecent/**===get
- /**/*.css===get
- /**/*.ico===get
- /**/*.png===get
- /swagger-ui.html===*
- /swagger/**===*
- /swagger-resources/**===*
- /v2/**===*
- /webjars/**===*
- /configuration/**===*