1. springboot项目创建
项目创建
前置条件
- 配置java环境
- 配置maven环境
在IDEA中创建一个springboot项目,创建的时候不选择任何的pom依赖
pom.xml文件示例:
这里建议使用2.6以下的springboot版本,不然后面会有swagger的冲突,虽然有其它的方式解决,但是还是选2.6以下版本更简单方便。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.bwss</groupId>
<artifactId>ai-management-platform</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
项目配置
引入web依赖
<!-- 引入web相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
修改application.properties后缀为application.yml,添加启动的端口号
server:
port: 8288
启动设置
package com.maple.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author lh
**/
@RestController
public class TestController {
@GetMapping(value = "test")
public String test() {
return "项目启动测试";
}
}
项目启动成功,在浏览器输入http://127.0.0.1:8288/test地址进行测试。
2. swagger2的knife4j配置
引入Knife4j的依赖包
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>2.0.9</version>
</dependency>
Swagger配置
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.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2WebMvc;
/**
* @Description swagger配置
* @Author LH
**/
@Configuration
@EnableSwagger2WebMvc
public class Knife4jConfig
{
@Bean(value = "knife4j")
public Docket knife4j()
{
return new Docket(DocumentationType.SWAGGER_2)
.apiInfo(new ApiInfoBuilder().title("AI管理平台接口").description("AI管理平台接口")
.termsOfServiceUrl("http://127.0.0.1:8288/doc.html").version("1.0").build())
//分组名称
.groupName("AI管理平台接口").select()
//这里指定Controller扫描包路径
.apis(RequestHandlerSelectors.basePackage("com.bwss")).paths(PathSelectors.any()).build();
}
}
浏览器访问http://127.0.0.1:8288/doc.html
3. 集成Mybatis Plus
引入相应的pom依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.7</version>
<relativePath/>
</parent>
<groupId>com.bwss</groupId>
<artifactId>ai-management-platform</artifactId>
<version>0.0.1-SNAPSHOT</version>
<description>AI管理平台</description>
<properties>
<java.version>1.8</java.version>
<lombok-version>1.18.12</lombok-version>
<druid-version>1.2.11</druid-version>
<mybatis-plus-version>3.5.2</mybatis-plus-version>
<velocity-version>2.3</velocity-version>
<fastjson-version>2.0.7</fastjson-version>
<mysql-connector-version>8.0.29</mysql-connector-version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<!-- 引入web相关 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--使用Mysql数据库-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql-connector-version}</version>
</dependency>
<!--使用阿里巴巴druid数据库连接池-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid-version}</version>
</dependency>
<!-- mybatis-plus的依赖 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus-version}</version>
</dependency>
<!-- mybatis-plus的自动生成代码插件 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus-version}</version>
</dependency>
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity-engine-core</artifactId>
<version>${velocity-version}</version>
</dependency>
<!--Lombok管理Getter/Setter/log等-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>${lombok-version}</version>
</dependency>
<!-- 处理JSON -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson-version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
数据库创建
创建用户表
CREATE TABLE `user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID',
`account` varchar(30) DEFAULT NULL COMMENT '用户账号',
`user_name` varchar(30) DEFAULT NULL COMMENT '用户姓名',
`nick_name` varchar(30) DEFAULT NULL COMMENT '用户昵称',
`user_type` varchar(2) DEFAULT '00' COMMENT '用户类型(00系统用户,01小程序用户)',
`email` varchar(50) DEFAULT '' COMMENT '用户邮箱',
`phone` varchar(11) DEFAULT '' COMMENT '手机号码',
`sex` char(1) DEFAULT '0' COMMENT '用户性别(0男 1女 2未知)',
`avatar` varchar(100) DEFAULT '' COMMENT '头像地址',
`salt` varchar(32) DEFAULT NULL COMMENT '用户加密盐值',
`password` varchar(100) DEFAULT '' COMMENT '密码',
`status` char(1) DEFAULT '0' COMMENT '帐号状态(0正常 1停用)',
`create_id` bigint(20) DEFAULT NULL COMMENT '创建人id',
`create_name` varchar(64) DEFAULT '' COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_id` bigint(20) DEFAULT NULL COMMENT '更新人id',
`update_name` varchar(64) DEFAULT '' COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`delete_flag` tinyint(1) DEFAULT '0' COMMENT '删除标志',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8 COMMENT='用户信息表';
配置项目的application.yml文件
server:
port: 8288
spring:
application:
name: ai
# 配置数据库连接
datasource:
url: jdbc:mysql://127.0.0.1:3306/maple?useUnicode=yes&characterEncoding=UTF-8&useSSL=false&serverTimezone=UTC
driverClassName: com.mysql.cj.jdbc.Driver
username: root
password: 123456
# 配置druid数据库连接池
type: com.alibaba.druid.pool.DruidDataSource
druid:
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 'x'
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
filters: stat,wall
# 通过connectProperties属性来打开mergeSql功能;慢SQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000
useGlobalDataSourceStat: true
# 配置mybatis-plus的xml和bean的目录
mybatis-plus:
mapper-locations: classpath:mapper/*.xml
configuration:
map-underscore-to-camel-case: true
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
#逻辑删除配置
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0
配置文件配置
package com.demo.config;
import com.alibaba.druid.support.http.StatViewServlet;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.web.servlet.ServletRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.servlet.Servlet;
import java.util.Date;
/**
* Mybatis plus配置
*/
@EnableTransactionManagement
@Configuration
@MapperScan("com.demo.mapper")
public class MybatisPlusConfig implements MetaObjectHandler {
/**
* 新增时,自动填充数据
*/
@Override
public void insertFill(MetaObject metaObject) {
this.setFieldValByName("isDeleted", false, metaObject);
this.setFieldValByName("createId", 1L, metaObject);
this.setFieldValByName("createName", "占位符", metaObject);
this.setFieldValByName("createTime", new Date(), metaObject);
this.setFieldValByName("updateId", 1L, metaObject);
this.setFieldValByName("updateName", "占位符", metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
/**
* 修改时,自动填充数据
*/
@Override
public void updateFill(MetaObject metaObject) {
this.setFieldValByName("updateId", 1L, metaObject);
this.setFieldValByName("updateName", "占位符", metaObject);
this.setFieldValByName("updateTime", new Date(), metaObject);
}
/**
* 配置druid监控服务器
*
* @return 返回监控注册的servlet对象
*/
@Bean
public ServletRegistrationBean<Servlet> statViewServlet() {
ServletRegistrationBean<Servlet> servletRegistrationBean = new ServletRegistrationBean<>(new StatViewServlet(), "/druid/*");
// 添加IP白名单,如果不需要黑名单、白名单的功能,注释掉即可
servletRegistrationBean.addInitParameter("allow", "127.0.0.1");
// 添加IP黑名单,当白名单和黑名单重复时,黑名单优先级更高
servletRegistrationBean.addInitParameter("deny", "127.0.0.1");
// 添加控制台管理用户
servletRegistrationBean.addInitParameter("loginUsername", "druid");
servletRegistrationBean.addInitParameter("loginPassword", "123456");
// 是否能够重置数据
servletRegistrationBean.addInitParameter("resetEnable", "false");
return servletRegistrationBean;
}
/**
* 分页插件。如果你不配置,分页插件将不生效
*/
@Bean
public MybatisPlusInterceptor paginationInterceptor()
{
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
CustomPaginationInterceptor paginationInnerInterceptor = new CustomPaginationInterceptor();
// 设置最大单页限制数量,默认 500 条,-1 不受限制
paginationInnerInterceptor.setMaxLimit(-1L);
// 分页合理化
paginationInnerInterceptor.setOverflow(true);
interceptor.addInnerInterceptor(paginationInnerInterceptor);
return interceptor;
}
}
配置分页查询,当超过页面数时,跳到最后一页:
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
/**
* @Description mybatis-plus自定义分页查询
* @Author LH
* @Date 2024/2/6 15:31
**/
public class CustomPaginationInterceptor extends PaginationInnerInterceptor {
@Override
protected void handlerOverflow(IPage<?> page) {
//原来的逻辑是超出范围返回第一页
//page.setCurrent(1L);
//修改成返回最后一页
page.setCurrent(page.getPages());
}
}
创建系统基类BaseEntity.java
,所有的实体类都继承该对象,用于存放系统字段
package com.demo.config.bean;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
import java.util.Date;
@Data
public class BaseEntity implements Serializable {
@TableId(type = IdType.AUTO)
protected Long id;
@ApiModelProperty("创建人id")
@TableField(value = "create_id", fill = FieldFill.INSERT)
private Long createId;
@ApiModelProperty("创建人名称")
@TableField(value = "create_name", fill = FieldFill.INSERT)
private String createName;
@ApiModelProperty("创建时间")
@TableField(value = "create_time", fill = FieldFill.INSERT)
private Date createTime;
@ApiModelProperty("更新人id")
@TableField(value = "update_id", fill = FieldFill.INSERT_UPDATE)
private Long updateId;
@ApiModelProperty("更新人名称")
@TableField(value = "update_name", fill = FieldFill.INSERT_UPDATE)
private String updateName;
@ApiModelProperty("更新时间")
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
private Date updateTime;
}
创建代码生成器Generator.java
package com.bwss.aimanagementplatform.generator;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.bwss.aimanagementplatform.entity.BaseEntity;
/**
* @Description 代码生成类
* @Author LH
**/
public class Generator
{
public static void main(String[] args)
{
// 设置作者
String auth = "lh";
// 设置父包名
String packageName = "com.bwss.aimanagementplatform";
// 设置父包模块名
String moduleName = "";
// 指定输出目录
String path = "generator";
String url = "jdbc:mysql://127.0.0.1:3306/ai?useUnicode=true&useSSL=false&characterEncoding=utf8";
String username = "root";
String password = "xxxx";
// 设置需要生成的表名
String table = "user";
// 设置过滤表前缀
String[] tablePrefix =
{ "usc_", "sys_" };
generateTest(auth, packageName, path, moduleName, url, username, password, table, tablePrefix);
}
private static void generateTest(String auth, String packageName, String path, String moduleName, String url,
String username, String password, String table, String[] tablePrefix)
{
FastAutoGenerator.create(url, username, password).globalConfig(builder -> builder.author(auth)
// 开启 swagger 模式
.enableSwagger().outputDir(path))
.packageConfig(builder -> builder.parent(packageName).moduleName(moduleName)
// 设置mapperXml生成路径
//.pathInfo(Collections.singletonMap(OutputFile.xml, path))
)
.strategyConfig(builder -> builder.addInclude(table).addTablePrefix(tablePrefix).entityBuilder()
.superClass(BaseEntity.class).enableLombok().logicDeleteColumnName("delete_flag")
.controllerBuilder().enableRestStyle())
.execute();
}
}
运行代码,可以在目录生成对应的表文件代码
阿里巴巴的Druid数据库管理
浏览器访问http://127.0.0.1:8288/druid/login.html
可以看到sql的执行情况。
4. 后端返回统一结果包装
封装一个统一返回类
public class BwResult<D>
{
/**
* 0 为成功,1为失败
*/
public static final String SUCCESS = "0";
public static final String FAIL = "1";
private String code;
private D data;
private String msg;
private BwResult(String code)
{
this.code = code;
}
public static <T> BwResult<T> get(String code)
{
return new BwResult<>(code);
}
public static <T> BwResult<T> success()
{
return new BwResult<T>(SUCCESS).setMsg("操作成功");
}
public static <T> BwResult<T> success(T data)
{
return new BwResult<T>(SUCCESS).setMsg("操作成功").setData(data);
}
public static <T> BwResult<T> success(String msg)
{
return new BwResult<T>(SUCCESS).setMsg(msg);
}
public static <T> BwResult<T> success(String msg, T data)
{
return new BwResult<T>(SUCCESS).setMsg(msg).setData(data);
}
public static <T> BwResult<T> fail()
{
return new BwResult<T>(FAIL).setMsg("操作失败");
}
public static <T> BwResult<T> fail(String msg)
{
return new BwResult<T>(FAIL).setMsg(msg);
}
public static <T> BwResult<T> fail(String msg, T data)
{
return new BwResult<T>(FAIL).setMsg(msg).setData(data);
}
public static <T> BwResult<T> fail(T data)
{
return new BwResult<T>(FAIL).setMsg("操作失败").setData(data);
}
public D getData()
{
return this.data;
}
public BwResult<D> setData(D data)
{
this.data = data;
return this;
}
public String getCode()
{
return code;
}
public BwResult<D> setCode(String code)
{
this.code = code;
return this;
}
public String getMsg()
{
return msg;
}
public BwResult<D> setMsg(String msg)
{
this.msg = msg;
return this;
}
/**
* 重载空参for 跨服务调用实例化该类用的构造函数
*/
private BwResult()
{
}
}
controller调用测试
import com.bwss.aimanagementplatform.entity.BwResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description 统一返回测试类
* @Author LH
**/
@RestController
public class TestResultController
{
@GetMapping("/testResultJsonSuccess")
public BwResult<?> testResultJsonSuccess()
{
return BwResult.success();
}
@GetMapping("/testResultJsonError")
public BwResult<?> testResultJsonError()
{
return BwResult.fail();
}
}
5. 后端返回统一异常处理
编写未知异常测试类
import lombok.Data;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @Description
* @Author LH
**/
@RestController
@RequiredArgsConstructor
public class TestErrorResultController {
/**
* 模拟系统空指针异常
*/
@GetMapping("/testResultError")
public Test testResultError() {
Test test = null;
test.setName("抛个空指针");
return test;
}
@Data
static class Test {
private String name;
}
}
浏览器请求处理不了类似的未知异常, 因此对未知异常统一编写一个异常拦截处理类,所有抛出去的未知异常都先经过这个类处理,再返回给前端。
/**
* @Description 异常信息统一处理
* @Author LH
**/
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice
{
/**
* 系统异常
* @param e 异常信息
* @return R
*/
@ExceptionHandler(Exception.class)
@Order(99)
public BwResult exception(Exception e)
{
log.error("系统异常信息 ex={}", e.getMessage(), e);
return BwResult.fail("未知异常,请联系管理员");
}
}
前端再次请求测试
上面是处理未知异常的,但是对于代码中可能出现并且需要处理的异常,使用自定义异常类来进行处理
import lombok.Data;
import lombok.EqualsAndHashCode;
/**
* @Description 自定义异常
* @Author LH
**/
@EqualsAndHashCode(callSuper = true)
@Data
public class CustomException extends RuntimeException
{
private final String code = "1";
private final String errorMsg;
public CustomException(String errorMsg)
{
this.errorMsg = errorMsg;
}
}
异常拦截类里添加
import com.bwss.aimanagementplatform.entity.BwResult;
import com.bwss.aimanagementplatform.exception.CustomException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.annotation.Order;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* @Description 异常信息统一处理
* @Author LH
**/
@Slf4j
@RestControllerAdvice
public class ExceptionAdvice
{
/**
* 系统异常
* @param e 异常信息
* @return R
*/
@ExceptionHandler(Exception.class)
@Order(99)
public BwResult<?> exception(Exception e)
{
log.error("系统异常信息 ex={}", e.getMessage(), e);
return BwResult.fail("未知异常,请联系管理员");
}
/**
* 自定义异常处理
* @param e 异常信息
* @return 返回结果
*/
@ExceptionHandler(CustomException.class)
public BwResult<?> mapleBaseException(CustomException e)
{
log.error("自定义异常信息 ex={}", e.getMessage(), e);
return BwResult.fail(e.getErrorMsg());
}
}
编写测试类
/**
* 模拟系统空指针异常
*/
@GetMapping("/testResultError")
public Test testResultError() {
Test test = new Test();
if (test.getName() == null) {
throw new CustomException("姓名为空!");
}
return test;
}
前端调用测试
6. SpringBoot集成日志打印Logback
在resources目录下创建文件logback-spring.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration scan="true" scanPeriod="60 seconds">
<springProperty scope="context" name="spring.application.name" source="spring.application.name"/>
<!-- 定义参数 -->
<property name="log.lever" value="debug"/>
<property name="log.maxHistory" value="365"/>
<property name="log.filePath" value="logs"/>
<property name="log.pattern" value="%-12(%d{MM-dd HH:mm:ss}) %c [%L] | %msg%n"/>
<!-- 控制台设置 -->
<appender name="consoleAppender" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<!--格式化输出:%d:表示日期 %thread:表示线程名 %-5level:级别从左显示5个字符宽度 %msg:日志消息 %n:是换行符-->
<pattern>%boldMagenta%red(%d{yyyy-MM-dd HH:mm:ss}) %green([%thread]) %highlight(%-5level) %yellow(%logger) - %cyan(%msg%n)</pattern>
<charset>UTF-8</charset>
</encoder>
</appender>
<!-- DEBUG -->
<appender name="debugAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/${spring.application.name}_debug.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/debug/${spring.application.name}_debug.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<MaxHistory>${log.maxHistory}</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- INFO -->
<appender name="infoAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/${spring.application.name}_info.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/info/${spring.application.name}_info.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<MaxHistory>${log.maxHistory}</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- ERROR -->
<appender name="errorAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 文件路径 -->
<file>${log.filePath}/${spring.application.name}_error.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!-- 文件名称 -->
<fileNamePattern>${log.filePath}/error/${spring.application.name}_error.%d{yyyy-MM-dd}.log.gz
</fileNamePattern>
<!-- 文件最大保存历史数量 -->
<MaxHistory>${log.maxHistory}</MaxHistory>
</rollingPolicy>
<encoder>
<pattern>${log.pattern}</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 上线后如果要查看错误日志,可以把level=info改为level=debug -->
<root level="info">
<appender-ref ref="consoleAppender"/>
<appender-ref ref="debugAppender"/>
<appender-ref ref="infoAppender"/>
<appender-ref ref="errorAppender"/>
</root>
</configuration>
启动查看
7. 启动banner图片生成
8. springboot集成redis
添加redis的pom依赖
<!-- 引入redis依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引入redis连接池的依赖 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
配置文件配置
spring:
redis:
database: 0
host: 127.0.0.1
port: 6379
password:
timeout: 5000
lettuce:
pool:
max-active: 32
max-wait: -1
max-idle: 16
min-idle: 8
编写配置类
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @author LH
**/
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
}
编写redis工具类RedisUtil.java
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import org.apache.commons.lang3.BooleanUtils;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
/**
* Redis常用的一些操作
*
* @author LH
*/
@Component
public class RedisUtil {
@Resource
private RedisTemplate<String, Object> redisTemplate;
/**
* 写入缓存
*/
public boolean set(final String key, Object value) {
boolean result = false;
try {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 写入缓存设置时效时间
*/
public boolean set(final String key, Object value, Long expireTime) {
boolean result = false;
try {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
operations.set(key, value);
redisTemplate.expire(key, expireTime, TimeUnit.SECONDS);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 更新缓存
*/
public boolean getAndSet(final String key, String value) {
boolean result = false;
try {
redisTemplate.opsForValue().getAndSet(key, value);
result = true;
} catch (Exception e) {
e.printStackTrace();
}
return result;
}
/**
* 批量删除对应的value
*/
public void remove(final String... keys) {
for (String key : keys) {
remove(key);
}
}
/**
* 批量删除key
*/
public void removePattern(final String pattern) {
Set<String> keys = redisTemplate.keys(pattern);
if (CollectionUtils.isNotEmpty(keys)) {
redisTemplate.delete(keys);
}
}
/**
* 删除对应的value
*/
public void remove(final String key) {
if (exists(key)) {
redisTemplate.delete(key);
}
}
/**
* 判断缓存中是否有对应的value
*/
public boolean exists(final String key) {
Boolean isExists = redisTemplate.hasKey(key);
return BooleanUtils.isTrue(isExists);
}
/**
* 读取缓存
*/
public Object get(final String key) {
ValueOperations<String, Object> operations = redisTemplate.opsForValue();
return operations.get(key);
}
/**
* 哈希 添加
*/
public void hmSet(String key, Object hashKey, Object value) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
hash.put(key, hashKey, value);
}
/**
* 哈希获取数据
*/
public Object hmGet(String key, Object hashKey) {
HashOperations<String, Object, Object> hash = redisTemplate.opsForHash();
return hash.get(key, hashKey);
}
/**
* 列表添加
*/
public void lPush(String k, Object v) {
ListOperations<String, Object> list = redisTemplate.opsForList();
list.rightPush(k, v);
}
/**
* 列表获取
*/
public List<Object> lRange(String k, long l, long l1) {
ListOperations<String, Object> list = redisTemplate.opsForList();
return list.range(k, l, l1);
}
/**
* 集合添加
*/
public void addSet(String key, Object value) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
set.add(key, value);
}
/**
* 删除集合下的所有值
*/
public void removeSetAll(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
Set<Object> objectSet = set.members(key);
if (objectSet != null && !objectSet.isEmpty()) {
for (Object o : objectSet) {
set.remove(key, o);
}
}
}
/**
* 判断set集合里面是否包含某个元素
*/
public Boolean isMember(String key, Object member) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.isMember(key, member);
}
/**
* 集合获取
*/
public Set<Object> setMembers(String key) {
SetOperations<String, Object> set = redisTemplate.opsForSet();
return set.members(key);
}
/**
* 有序集合添加
*/
public void zAdd(String key, Object value, double source) {
ZSetOperations<String, Object> zset = redisTemplate.opsForZSet();
zset.add(key, value, source);
}
/**
* 有序集合获取指定范围的数据
*/
public Set<Object> rangeByScore(String key, double source, double source1) {
ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
return zSet.rangeByScore(key, source, source1);
}
/**
* 有序集合升序获取
*/
public Set<Object> range(String key, Long source, Long source1) {
ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
return zSet.range(key, source, source1);
}
/**
* 有序集合降序获取
*/
public Set<Object> reverseRange(String key, Long source, Long source1) {
ZSetOperations<String, Object> zSet = redisTemplate.opsForZSet();
return zSet.reverseRange(key, source, source1);
}
}
redis Key过期处理事件
修改redis的配置文件
找到notify-keyspace-events
看一下notify-keyspace-events Ex
是否被注释(默认是注释),放开注释即可。
修改RedisConfig.java
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
/**
* @Description redis配置
* @Author LH
**/
@Configuration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
public class RedisConfig
{
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory)
{
RedisTemplate<Object, Object> template = new RedisTemplate<>();
//使用fastjson序列化
FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
// value值的序列化采用fastJsonRedisSerializer
template.setValueSerializer(fastJsonRedisSerializer);
template.setHashValueSerializer(fastJsonRedisSerializer);
// key的序列化采用StringRedisSerializer
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean(StringRedisTemplate.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory)
{
StringRedisTemplate template = new StringRedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
/**
* 开启监听redis Key过期事件
*/
@Bean
public RedisMessageListenerContainer redisMessageListenerContainer(RedisConnectionFactory connectionFactory)
{
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
}
定义过期监听器RedisKeyExpireListener
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.connection.Message;
import org.springframework.data.redis.listener.KeyExpirationEventMessageListener;
import org.springframework.data.redis.listener.RedisMessageListenerContainer;
/**
* @Description redis过期监听器
* @Author LH
**/
@Slf4j
public class RedisKeyExpireListener extends KeyExpirationEventMessageListener
{
public RedisKeyExpireListener(RedisMessageListenerContainer listenerContainer)
{
super(listenerContainer);
}
@Override
public void onMessage(Message message, byte[] pattern)
{
String expireKey = message.toString();
// 根据过期的key处理对应的业务逻辑
log.info(expireKey + "已过期-------------------");
}
}
9. springBoot集成用户登录功能
首先知道什么是JWT
简单来讲,就是一种认证机制,让后台知道该请求是来自于受信任的客户端。
优点不讲,讲下缺点
- 安全性没法保证:jwt的payload并没有加密,只是用Base64编码而已,不能存储敏感数据。
- 无法中途废弃:一旦签发了一个jwt,在到期之前始终都是有效的,如果用户信息发生更新了,只能等旧的jwt过期后重新签发新的jwt。
- 续签问题:当签发的jwt保存在客户端,客户端一直在操作页面,按道理应该一直为客户端续长有效时间,否则当jwt有效期到了就会导致用户需要重新登录。
缺陷应对
- 在使用的过程中,只储存常用的无敏感数据,比如用户ID,用户角色等。
- 通过和redis配合使用,将token返回时,同步保存redis,通过控制token在redis的有效期来进行控制。
- 通过统计redis有效数据,对在线用户进行统计或强制下线等操作。
用户名密码登录
基于Token的身份验证流程
1、客户端使用用户名或密码请求登录;
2、服务端收到请求,去验证用户名与密码;
3、验证成功后,服务端会使用JWT签发一个Token,保存到Redis中,同时再把这个Token发送给客户端;
4、客户端收到Token以后可以把它存储起来,比如放在Cookie里或者Local Storage里;
5、客户端每次向服务端请求资源的时候需要在请求Header里面带着服务端签发的Token;
6、服务端收到请求,然后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据。验证失败,返回失败原因。
引入pom依赖
<!-- 引入JWT相关 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
创建全局常量类GlobalConfig.java
/**
* @author LH
*/
public class GlobalConfig {
private GlobalConfig() {
}
/**
* 用户储存在redis中的过期时间 12 小时
*/
public static final long EXPIRE_TIME = 60 * 60 * 12L;
/**
* 生成token的私钥
*/
public static final String SECRET = "xxxx";
/**
* 前端传递token的header名称
*/
public static final String TOKEN_NAME = "Authorization";
/**
* 用户登录token保存在redis的key值
*
* @param account 用户登录帐号
* @return token保存在redis的key
*/
public static String getRedisUserKey(String account) {
return "ADMIN:" + account;
}
}
创建保存的jwt的类TokenBean.java
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.RequiredArgsConstructor;
/**
* @author LH
*/
@Data
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
public class TokenBean {
/**
* 用户ID
*/
private Long userId;
/**
* 用户账号
*/
private String account;
/**
* 用户类型
*/
private String userType;
}
创建JwtUtil.java
工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.bwss.aimanagementplatform.config.GlobalConfig;
import com.bwss.aimanagementplatform.entity.TokenBean;
import com.bwss.aimanagementplatform.exception.CustomException;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.util.Objects;
/**
* @Description Jwt常用操作
* @Author LH
**/
public class JwtUtil
{
private static final String ACCOUNT = "account";
private static final String USER_ID = "userId";
private static final String USER_TYPE = "userType";
/**
* 校验token是否正确
*
* @param token 密钥
* @return 是否正确
*/
public static boolean verify(String token, String account)
{
try
{
Algorithm algorithm = Algorithm.HMAC256(GlobalConfig.SECRET);
JWTVerifier verifier = JWT.require(algorithm).withClaim(ACCOUNT, account).build();
verifier.verify(token);
return true;
} catch (Exception exception)
{
return false;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户登录帐号
*/
public static String getAccount()
{
try
{
DecodedJWT jwt = getJwt();
if (jwt == null)
{
return null;
}
return jwt.getClaim(ACCOUNT).asString();
} catch (JWTDecodeException e)
{
return null;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户登录帐号
*/
public static String getAccount(String token)
{
try
{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(ACCOUNT).asString();
} catch (JWTDecodeException e)
{
return null;
}
}
public static Long getUserId()
{
try
{
DecodedJWT jwt = getJwt();
if (jwt == null)
{
return null;
}
return jwt.getClaim(USER_ID).asLong();
} catch (JWTDecodeException e)
{
return null;
}
}
/**
* 获得token中的信息无需secret解密也能获得
*
* @return token中包含的用户ID
*/
public static Long getUserId(String token)
{
try
{
DecodedJWT jwt = JWT.decode(token);
return jwt.getClaim(USER_ID).asLong();
} catch (JWTDecodeException e)
{
return null;
}
}
public static TokenBean getTokenMsg()
{
TokenBean tokenBean = new TokenBean();
try
{
DecodedJWT jwt = getJwt();
if (jwt == null)
{
return tokenBean;
}
tokenBean.setUserId(jwt.getClaim(USER_ID).asLong());
tokenBean.setAccount(jwt.getClaim(ACCOUNT).asString());
return tokenBean;
} catch (JWTDecodeException e)
{
return tokenBean;
}
}
private static DecodedJWT getJwt()
{
ServletRequestAttributes servletRequestAttributes = (ServletRequestAttributes) RequestContextHolder
.getRequestAttributes();
if (Objects.isNull(servletRequestAttributes))
{
throw new CustomException("请求参数有误,请重试");
}
HttpServletRequest request = servletRequestAttributes.getRequest();
String authorization = request.getHeader(GlobalConfig.TOKEN_NAME);
if (authorization == null)
{
return null;
}
return JWT.decode(authorization);
}
/**
* 校验token是否有效
*
* @param token token信息
* @return 返回结果
*/
public static boolean verifyToken(String token)
{
try
{
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(GlobalConfig.SECRET)).build();
DecodedJWT jwt = verifier.verify(token);
jwt.getClaims();
return true;
} catch (Exception e)
{
e.printStackTrace();
return false;
}
}
/**
* 创建token
*
* @param tokenBean token保存的信息
* @return token
*/
public static String createToken(TokenBean tokenBean)
{
Algorithm algorithm = Algorithm.HMAC256(GlobalConfig.SECRET);
return JWT.create().withClaim(USER_ID, tokenBean.getUserId()).withClaim(ACCOUNT, tokenBean.getAccount())
.withClaim(USER_TYPE, tokenBean.getUserType()).sign(algorithm);
}
}
创建异常枚举类ErrorCode.java
import lombok.AllArgsConstructor;
import lombok.Getter;
/**
* @Description
* @Author LH
**/
@AllArgsConstructor
@Getter
public enum ErrorCode {
COMMON_ERROR("9998", "未知异常,请联系管理员"),
NO_TOKEN("1001", "用户未登录"),
TOKEN_EXPIRE("1002", "登陆超时,请重新登录"),
TOKEN_EXCHANGE("1003", "账号在其他地方登录,账号被踢出"),
USER_LOGIN_ERROR("2001", "用户名或密码错误"),
USER_STATUS_ERROR("2002", "用户已被停用,请联系管理员");
private final String code;
private final String msg;
}
在Application.java
启动项上添加注解@ServletComponentScan
SpringBootApplication 上使用@ServletComponentScan 注解后
Servlet可以直接通过@WebServlet注解自动注册
Filter可以直接通过@WebFilter注解自动注册
Listener可以直接通过@WebListener 注解自动注册
JwtFilter
创建登录拦截类
import com.alibaba.fastjson.JSON;
import com.bwss.aimanagementplatform.config.GlobalConfig;
import com.bwss.aimanagementplatform.entity.BwResult;
import com.bwss.aimanagementplatform.enums.ErrorCode;
import com.bwss.aimanagementplatform.util.JwtUtil;
import com.bwss.aimanagementplatform.util.RedisUtil;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
/**
* @Description 判断用户登录token
* @Author LH
**/
@WebFilter(filterName = "jwtFilter", urlPatterns =
{ "/*" })
@AllArgsConstructor
@Order(1)
public class JwtFilter implements Filter
{
private final List<String> excludedUrlList;
@Override
public void init(FilterConfig filterConfig)
{
excludedUrlList.addAll(Arrays.asList("/sso/login", "/sso/logout", "/example/*", "/webjars/**", "/swagger/**",
"/v2/api-docs", "/doc.html", "/swagger-ui.html", "/swagger-resources/**", "/swagger-resources"));
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException
{
String url = ((HttpServletRequest) request).getRequestURI();
boolean isMatch = false;
for (String excludedUrl : excludedUrlList)
{
if (Pattern.matches(excludedUrl.replace("*", ".*"), url))
{
isMatch = true;
break;
}
}
if (isMatch)
{
chain.doFilter(request, response);
} else
{
HttpServletRequest httpServletRequest = (HttpServletRequest) request;
HttpServletResponse httpServletResponse = (HttpServletResponse) response;
//处理跨域问题,跨域的请求首先会发一个options类型的请求
if (httpServletRequest.getMethod().equals(HttpMethod.OPTIONS.name()))
{
chain.doFilter(request, response);
}
BeanFactory factory = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
RedisUtil redisService = (RedisUtil) factory.getBean("redisUtil");
String account;
String authorization = httpServletRequest.getHeader(GlobalConfig.TOKEN_NAME);
// 判断token是否存在,不存在代表未登录
if (StringUtils.isEmpty(authorization))
{
writeRsp(httpServletResponse, ErrorCode.NO_TOKEN);
return;
} else
{
account = JwtUtil.getAccount(authorization);
String token = (String) redisService.get(GlobalConfig.getRedisUserKey(account));
// 判断token是否存在,不存在代表登陆超时
if (StringUtils.isEmpty(token))
{
writeRsp(httpServletResponse, ErrorCode.TOKEN_EXPIRE);
return;
} else
{
// 判断token是否相等,不相等代表在其他地方登录
if (!token.equalsIgnoreCase(authorization))
{
writeRsp(httpServletResponse, ErrorCode.TOKEN_EXCHANGE);
return;
}
}
}
// 保存redis,每次调用成功都刷新过期时间
redisService.set(GlobalConfig.getRedisUserKey(account), authorization, GlobalConfig.EXPIRE_TIME);
chain.doFilter(httpServletRequest, httpServletResponse);
}
}
@Override
public void destroy()
{
Filter.super.destroy();
}
private void writeRsp(HttpServletResponse response, ErrorCode errorCode)
{
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.setHeader("content-type", "application/json;charset=UTF-8");
try
{
response.getWriter().println(JSON.toJSON(BwResult.fail(errorCode.getCode(), errorCode.getMsg())));
} catch (IOException e)
{
e.printStackTrace();
}
}
}
创建md5加密类
import com.bwss.aimanagementplatform.config.GlobalConfig;
import com.bwss.aimanagementplatform.exception.CustomException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.DigestUtils;
import org.springframework.util.StringUtils;
/**
* @Description MD5撒盐加密 及MD5加密
* @Author LH
**/
@Slf4j
public class Md5Util
{
private Md5Util()
{
}
/**
* 密码加密处理
*
* @param password 密码明文
* @param salt 盐
* @return 加密后密文
*/
public static String encrypt(String password, String salt)
{
if (StringUtils.isEmpty(password) || StringUtils.isEmpty(salt))
{
log.error("密码加密失败原因: password and salt cannot be empty");
throw new CustomException("请求参数有误,请重试");
}
return DigestUtils.md5DigestAsHex((salt + password).getBytes());
}
/**
* 校验密码
*
* @param target 待校验密码
* @param source 原密码
* @param salt 加密原密码的盐
*/
public static boolean verifyPassword(String target, String source, String salt)
{
if (StringUtils.isEmpty(target) || StringUtils.isEmpty(source) || StringUtils.isEmpty(salt))
{
log.info("校验密码失败,原因 target ={}, source ={}, salt ={}", target, source, salt);
return false;
}
String targetEncryptPwd = encrypt(target, salt);
return targetEncryptPwd.equals(source);
}
public static void main(String[] args)
{
log.info(encrypt("密码", GlobalConfig.SECRET));
}
}
用户登录相关
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
/**
* @Description 登录请求对象
* @Author LH
**/
@Data
@ApiModel(value = "用户登录请求对象", description = "用户中心-用户登录请求对象")
public class LoginQuery
{
@ApiModelProperty(value = "登录账号")
private String account;
@ApiModelProperty(value = "登录密码")
private String password;
}
返回的用户信息对象UserModel.java
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Builder;
import lombok.RequiredArgsConstructor;
/**
* @Description 用户信息VO
* @Author LH
**/
@Data
@Builder
@RequiredArgsConstructor
@AllArgsConstructor
@ApiModel(value = "用户视图对象", description = "用户中心-用户信息")
public class UserVo
{
@ApiModelProperty(value = "用户ID")
private Long id;
@ApiModelProperty(value = "用户账号")
private String account;
@ApiModelProperty(value = "用户姓名")
private String userName;
@ApiModelProperty(value = "用户类型")
private String userType;
@ApiModelProperty(value = "用户邮箱")
private String email;
@ApiModelProperty(value = "手机号码")
private String phone;
@ApiModelProperty(value = "用户性别")
private String sex;
@ApiModelProperty(value = "头像地址")
private String avatar;
@ApiModelProperty(value = "帐号状态")
private String status;
@ApiModelProperty(value = "备注")
private String remark;
@ApiModelProperty(value = "用户验证Token")
private String token;
}
controller
import com.bwss.aimanagementplatform.entity.vo.LoginQuery;
import com.bwss.aimanagementplatform.entity.vo.UserVo;
import com.bwss.aimanagementplatform.service.IBwUserService;
import com.bwss.aimanagementplatform.util.JwtUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.AllArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
/**
* @Description 系统登录Controller
* @Author LH
**/
@Api(tags = "管理系统-系统登录操作")
@RestController
@AllArgsConstructor
@RequestMapping(value = "/sso")
public class LoginController
{
@Autowired
private IBwUserService iBwUserService;
@ApiOperation(value = "用户登录")
@PostMapping("/login")
public UserVo login(@RequestBody LoginQuery req)
{
return iBwUserService.login(req);
}
@ApiOperation(value = "用户退出登录")
@GetMapping("/logout")
public void logout()
{
iBwUserService.logout();
}
@ApiOperation(value = "获取登录用户信息")
@GetMapping("/getUserId")
public String getUserId()
{
return "当前登录用户的ID为" + JwtUtil.getUserId();
}
}
service
import com.bwss.aimanagementplatform.entity.BwUser;
import com.baomidou.mybatisplus.extension.service.IService;
import com.bwss.aimanagementplatform.entity.vo.LoginQuery;
import com.bwss.aimanagementplatform.entity.vo.UserVo;
/**
* 用户信息表 服务类
* @author lh
*/
public interface IBwUserService extends IService<BwUser>
{
/**
* 用户登录
*
* @param req 用户信息
* @return 用户登录信息
*/
UserVo login(LoginQuery req);
/**
* 退出系统,清除用户token
*/
void logout();
}
serviceImpl
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.bwss.aimanagementplatform.config.GlobalConfig;
import com.bwss.aimanagementplatform.entity.BwUser;
import com.bwss.aimanagementplatform.entity.TokenBean;
import com.bwss.aimanagementplatform.entity.vo.LoginQuery;
import com.bwss.aimanagementplatform.entity.vo.UserVo;
import com.bwss.aimanagementplatform.enums.ErrorCode;
import com.bwss.aimanagementplatform.exception.CustomException;
import com.bwss.aimanagementplatform.mapper.BwUserMapper;
import com.bwss.aimanagementplatform.service.IBwUserService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.bwss.aimanagementplatform.util.JwtUtil;
import com.bwss.aimanagementplatform.util.Md5Util;
import com.bwss.aimanagementplatform.util.RedisUtil;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.Objects;
/**
* 用户信息表 服务实现类
* @author lh
*/
@Service
public class BwUserServiceImpl extends ServiceImpl<BwUserMapper, BwUser> implements IBwUserService
{
@Autowired
private BwUserMapper bwUserMapper;
@Autowired
private RedisUtil redisUtil;
@Override
public UserVo login(LoginQuery req)
{
BwUser user = bwUserMapper
.selectOne(Wrappers.lambdaQuery(BwUser.class).eq(BwUser::getAccount, req.getAccount()).last("LIMIT 1"));
if (Objects.isNull(user))
{
throw new CustomException(ErrorCode.USER_LOGIN_ERROR.getMsg());
}
if ("1".equals(user.getStatus()))
{
throw new CustomException(ErrorCode.USER_STATUS_ERROR.getMsg());
}
if (!Md5Util.verifyPassword(req.getPassword(), user.getPassword(), user.getSalt()))
{
throw new CustomException(ErrorCode.USER_LOGIN_ERROR.getMsg());
}
TokenBean tokenBean = TokenBean.builder().userId(user.getId()).userType(user.getUserType())
.account(user.getUserName()).build();
UserVo userVo = new UserVo();
BeanUtils.copyProperties(user, userVo);
String token;
try
{
token = JwtUtil.createToken(tokenBean);
} catch (Exception e)
{
log.error(e.getMessage());
throw new CustomException(ErrorCode.COMMON_ERROR.getMsg());
}
userVo.setToken(token);
redisUtil.set(GlobalConfig.getRedisUserKey(user.getAccount()), token);
return userVo;
}
@Override
public void logout()
{
redisUtil.remove(GlobalConfig.getRedisUserKey(JwtUtil.getAccount()));
}
}
到此,用户登录功能实现。
10. springBoot处理请求跨域问题
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpHeaders;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/**
* @Description 跨域拦截
* @Author LH
**/
@WebFilter(filterName = "corsFilter", urlPatterns = "/*")
@Order(0)
public class CorsFilter implements Filter
{
private static final String HEADER_ORIGIN = "Origin";
private static final String METHOD_OPTIONS = "OPTIONS";
@Override
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
if (request.getHeader(HEADER_ORIGIN) != null) {
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_ORIGIN, request.getHeader(HEADER_ORIGIN));
}
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_CREDENTIALS, "true");
// 如果允许所有请求方式,用*
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_METHODS, "GET, POST, PUT, DELETE, OPTIONS");
response.setHeader(HttpHeaders.ACCESS_CONTROL_MAX_AGE, "3600");
// 如果允许所有header,用*
response.setHeader(HttpHeaders.ACCESS_CONTROL_ALLOW_HEADERS, "Authorization, Content-Type, Accept, X-Requested-With, remember-me");
response.setHeader(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Disposition");
if (METHOD_OPTIONS.equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
return;
}
chain.doFilter(req, res);
}
}