❤️springboot+vue前后端微服务
一、创建父工程
1、创建springboot工程stu_parent
在idea开发工具中,使用 Spring Initializr 快速初始化一个 Spring Boot 模块
2、点击next
3、developer tools选择lombok,简化代码
4、web选择spring web
5、完成后的目录
这个图不对,需要等会出现如下第二图
等会是这样
6、删除src文件夹(父工程不写controller,service,dao,交给子模块处理)
7、修改
修改版本为 :2.3.0.RELEASE,artifactId 节点后面添加 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.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<packaging>pom</packaging>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer1024</name>
<description>20230407MyServer1024</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</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>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
8、配置阿里云,让maven下载依赖更快一些,放到buil下边,这里是配置阿里云的代码。
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
9、父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.3.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<packaging>pom</packaging>
<modules>
<module>service</module>
<module>common</module>
</modules>
<version>0.0.1-SNAPSHOT</version>
<name>20230407MyServer1024</name>
<description>20230407MyServer1024</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.1</mybatis-plus.version>
<velocity.version>1.7</velocity.version>
<swagger.version>2.9.2</swagger.version>
<aliyun.oss.version>3.1.0</aliyun.oss.version>
<jodatime.version>2.10.6</jodatime.version>
<commons-fileupload.version>1.3.1</commons-fileupload.version>
<commons-io.version>2.6</commons-io.version>
<commons-lang.version>3.9</commons-lang.version>
<httpclient.version>4.5.1</httpclient.version>
<jwt.version>0.9.1</jwt.version>
<aliyun-java-sdk-core.version>4.3.3</aliyun-java-sdk-core.version>
<aliyun-java-sdk-kms.version>2.10.1</aliyun-java-sdk-kms.version>
<aliyun-java-sdk-vod.version>2.15.10</aliyun-java-sdk-vod.version>
<aliyun-sdk-vod-upload.version>1.4.11</aliyun-sdk-vod-upload.version>
<fastjson.version>1.2.28</fastjson.version>
<gson.version>2.8.2</gson.version>
<json.version>20170516</json.version>
<commons-dbutils.version>1.6</commons-dbutils.version>
<canal.client.version>1.1.4</canal.client.version>
<docker.image.prefix>zx</docker.image.prefix>
<alibaba.easyexcel.version>2.2.6</alibaba.easyexcel.version>
<apache.xmlbeans.version>3.1.0</apache.xmlbeans.version>
<cloud-alibaba.version>2.2.1.RELEASE</cloud-alibaba.version>
<spring-cloud-starter-gateway>2.2.0.RELEASE</spring-cloud-starter-gateway>
<com.github.axet.version>0.0.9</com.github.axet.version>
<cn.hutool.version>5.3.3</cn.hutool.version>
</properties>
<!-- <dependencies>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-starter-web</artifactId>-->
<!-- </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>-->
<!-- </dependencies>-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
<version>${com.github.axet.version}</version>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${cn.hutool.version}</version>
</dependency>
<!--Spring Cloud-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/com.alibaba.cloud/spring-cloud-alibaba-dependencies -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${cloud-alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<!--mybatis-plus 持久层-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<!-- <dependency>-->
<!-- <groupId>org.apache.velocity</groupId>-->
<!-- <artifactId>velocity-engine-core</artifactId>-->
<!-- <version>${velocity.version}</version>-->
<!-- </dependency>-->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
<version>${velocity.version}</version>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>${swagger.version}</version>
</dependency>
<!--swagger ui-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>${swagger.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun.oss</groupId>
<artifactId>aliyun-sdk-oss</artifactId>
<version>${aliyun.oss.version}</version>
</dependency>
<!--日期时间工具-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
<version>${jodatime.version}</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>${commons-fileupload.version}</version>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons-io.version}</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang.version}</version>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
<version>${httpclient.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jwt.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-core</artifactId>
<version>${aliyun-java-sdk-core.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-kms</artifactId>
<version>${aliyun-java-sdk-kms.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-java-sdk-vod</artifactId>
<version>${aliyun-java-sdk-vod.version}</version>
</dependency>
<dependency>
<groupId>com.aliyun</groupId>
<artifactId>aliyun-sdk-vod-upload</artifactId>
<version>${aliyun-sdk-vod-upload.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>${json.version}</version>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
<version>${gson.version}</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>${commons-dbutils.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.otter</groupId>
<artifactId>canal.client</artifactId>
<version>${canal.client.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>easyexcel</artifactId>
<version>${alibaba.easyexcel.version}</version>
</dependency>
<dependency>
<groupId>org.apache.xmlbeans</groupId>
<artifactId>xmlbeans</artifactId>
<version>${apache.xmlbeans.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<!-- <build>-->
<!-- <plugins>-->
<!-- <plugin>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
<!-- <configuration>-->
<!-- <excludes>-->
<!-- <exclude>-->
<!-- <groupId>org.projectlombok</groupId>-->
<!-- <artifactId>lombok</artifactId>-->
<!-- </exclude>-->
<!-- </excludes>-->
<!-- </configuration>-->
<!-- </plugin>-->
<!-- </plugins>-->
<!-- </build>-->
<repositories>
<repository>
<id>nexus-aliyun</id>
<name>nexus-aliyun</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>public</id>
<name>aliyun nexus</name>
<url>http://maven.aliyun.com/nexus/content/groups/public/</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
二、创建子模块service
1、新建module
在idea2023版本里,右键点击项目新增一个,NEW ->Module,但是在新版的IDEA里新建项目时看到的是Maven Archetype而不是maven,找不到在哪新建常规的maven项目
2、点击创建
3、新建完成
4、添加模块类型是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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service</artifactId>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
5、添加项目需要的依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service</artifactId>
<packaging>pom</packaging>
<modules>
<module>service-acl</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.stu</groupId>
<artifactId>service-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</artifactId>
</dependency>
</dependencies>
</project>
6、删掉src目录
7、service模块的pom配置
注意service-base是后边配置swagger时候加的,没有配置service-base模块这里不加service-base依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service</artifactId>
<packaging>pom</packaging>
<modules>
<module>service-acl</module>
</modules>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.stu</groupId>
<artifactId>service-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!-- 日期时间工具 -->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
</dependency>
<!--httpclient-->
<dependency>
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpclient</artifactId>
</dependency>
<!--commons-io-->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
</dependency>
<!--json-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
</dependency>
<dependency>
<groupId>com.google.code.gson</groupId>
<artifactId>gson</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>
</project>
三、搭建service-acl模块
1、新建module
注意名称,parent选择service
2、完成
3、代码生成器
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.generator.FastAutoGenerator;
import com.baomidou.mybatisplus.generator.config.OutputFile;
import com.baomidou.mybatisplus.generator.fill.Column;
import java.util.Collections;
/******************************
* 用途说明:
* 作者姓名:公众号:程序员小明1024
* 创建时间: 2023-06-23 14:52
******************************/
public class CodeGenerator {
public static void main(String[] args) {
generate();
}
private static void generate() {
String projectPath = System.getProperty("user.dir");
FastAutoGenerator.create("jdbc:mysql://localhost:3306/myServer?serverTimezone=Asia/Shanghai", "root", "study")
.globalConfig(builder -> {
builder.author("公众号 小明的学习圈子") // 设置作者
.enableSwagger() // 开启 swagger 模式
.fileOverride() // 覆盖已生成文件
.outputDir(projectPath + "/service/service-acl//src/main/java"); // 指定输出目录
})
.packageConfig(builder -> {
builder.parent("com.stu.service") // 设置父包名
.moduleName(null) // 设置父包模块名
.pathInfo(Collections.singletonMap(OutputFile.mapperXml, projectPath + "/src/main/resources/mapper")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.entityBuilder().enableLombok()
.addTableFills(new Column("gmt_create", FieldFill.INSERT))
.addTableFills(new Column("gmt_modified", FieldFill.INSERT_UPDATE));;
// builder.mapperBuilder().enableMapperAnnotation().build();
builder.controllerBuilder().enableHyphenStyle() // 开启驼峰转连字符
.enableRestStyle(); // 开启生成@RestController 控制器
builder.addInclude("acl_user") // 设置需要生成的表名
.addTablePrefix("acl_", "sys_"); // 设置过滤表前缀
})
// .templateEngine(new FreemarkerTemplateEngine()) // 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
4、service-acl的依赖
注意这里要手动加上packaging为jar
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>service</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service-acl</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>
四、创建common、service-base,配置Swagger2
1、创建common模块
在父模块下创建module模块common
2、添加依赖,并添加packaging为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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>MyServer</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>common</artifactId>
<packaging>pom</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis-plus-->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
</dependency>
<!--lombok用来简化实体类:需要安装lombok插件-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<scope>test</scope>
</dependency>
<!--swagger-->
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
</dependency>
<!-- spring boot redis缓存引入 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- lecttuce 缓存连接池-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
</dependency>
</dependencies>
</project>
3、删除src
4、在common下面创建子模块service-base
5、完成
6、配置swagger2
在模块service-base中,创建swagger2的配置类
创建包com.stu.service.base.config,创建类Swagger2Config
package com.stu.service.base.config;
import com.google.common.base.Predicates;
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.service.ApiInfo;
import springfox.documentation.service.Contact;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
/**
* @author javaclimb
* swagger2 配置文件
*/
@Configuration
@EnableSwagger2
public class Swagger2Config {
@Bean
public Docket webApiConfig() {
return new Docket(DocumentationType.SWAGGER_2)
// 分组名
.groupName("WebApi")
.apiInfo(apiInfo())
.select()
// .paths(Predicates.not(PathSelectors.regex("/admin/.*")))
.paths(Predicates.not(PathSelectors.regex("/error.*")))
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("Java攀登网API文档")
.description("描述服务端的 api 接口定义")
.version("1.0")
.contact(new Contact("小明的学习圈子", "https://www.stucoding.com/", "xxxx.@xxx.com"))
.build();
}
}
7、使用Swagger
定义在类上:@Api
定义在方法上:@ApiOperation
定义在参数上:@ApiParam
在启动类加上
@ComponentScan(basePackages = "com.stu")
在service.pom里添加依赖
<dependency>
<groupId>com.stu</groupId>
<artifactId>service_base</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
8、启动Application
地址是固定的,端口修改为自己的就行
访问:http://localhost:9110/swagger-ui.html
五、定义统一的返回数据格式
1、项目数据格式
项目中我们会将响应封装成json返回,一般我们会将所有接口的数据格式统一, 使前端(iOS Android, Web)对数据的操作更一致、轻松。
一般情况下,统一返回数据格式没有固定的格式,只要能描述清楚返回的数据状态以及要返回的具体数据就可以。但是一般会包含状态码、返回消息、数据这几部分内容
定义统一格式:
{
"success": 布尔, //响应是否成功
"code": 数字, //响应码
"message": 字符串, //返回消息
"data": {} //返回数据,放在java对象中
}
2、在common模块下创建子模块common-utils
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>common-utils</artifactId>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</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>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
</dependency>
<!-- velocity 模板引擎, Mybatis Plus 代码生成器需要 -->
<dependency>
<groupId>org.apache.velocity</groupId>
<artifactId>velocity</artifactId>
</dependency>
<!--mysql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
3、创建包com.stu.service.base.result,创建枚举类ResultCodeEnum.java
package com.stu.base.result;
import lombok.Getter;
import lombok.ToString;
/**
* 前后端数据交换状态码
*/
@Getter
@ToString
public enum ResultCodeEnum {
SUCCESS(true, 20000, "成功"),
UNKNOWN_REASON(false, 20001, "未知错误"),
UPDATE_ERROR(false, 20002, "更新失败"),
BAD_SQL_GRAMMAR(false, 21001, "sql 语法错误"),
JSON_PARSE_ERROR(false, 21002, "json 解析异常"),
PARAM_ERROR(false, 21003, "参数不正确"),
FILE_UPLOAD_ERROR(false, 21004, "文件上传错误"),
FILE_DELETE_ERROR(false, 21005, "文件刪除错误"),
EXCEL_DATA_IMPORT_ERROR(false, 21006, "Excel 数据导入错误"),
VIDEO_UPLOAD_ALIYUN_ERROR(false, 22001, "视频上传至阿里云失败"),
VIDEO_UPLOAD_TOMCAT_ERROR(false, 22002, "视频上传至业务服务器失败"),
VIDEO_DELETE_ALIYUN_ERROR(false, 22003, "阿里云视频文件删除失败"),
FETCH_VIDEO_UPLOADAUTH_ERROR(false, 22004, "获取上传地址和凭证失败"),
REFRESH_VIDEO_UPLOADAUTH_ERROR(false, 22005, "刷新上传地址和凭证失败"),
FETCH_PLAYAUTH_ERROR(false, 22006, "获取播放凭证失败"),
URL_ENCODE_ERROR(false, 23001, "URL编码失败"),
ILLEGAL_CALLBACK_REQUEST_ERROR(false, 23002, "非法回调请求"),
FETCH_ACCESSTOKEN_FAILD(false, 23003, "获取 accessToken 失败"),
FETCH_USERINFO_ERROR(false, 23004, "获取用户信息失败"),
LOGIN_ERROR(false, 23005, "登录失败"),
COMMENT_EMPTY(false, 24006, "评论内容必须填写"),
PAY_RUN(false, 25000, "支付中"),
PAY_UNIFIEDORDER_ERROR(false, 25001, "统一下单错误"),
PAY_ORDERQUERY_ERROR(false, 25002, "查询支付结果错误"),
ORDER_EXIST_ERROR(false, 25003, "课程已购买"),
GATEWAY_ERROR(false, 26000, "服务不能访问"),
CODE_ERROR(false, 28000, "验证码错误"),
LOGIN_PHONE_ERROR(false, 28009, "手机号码不正确"),
LOGIN_MOBILE_ERROR(false, 28001, "账号不正确"),
LOGIN_PASSWORD_ERROR(false, 28008, "密码不正确"),
LOGIN_DISABLED_ERROR(false, 28002, "该用户已被禁用"),
REGISTER_MOBLE_ERROR(false, 28003, "手机号已被注册"),
LOGIN_AUTH(false, 28004, "需要登录"),
LOGIN_ACL(false, 28005, "没有权限"),
SMS_SEND_ERROR(false, 28006, "短信发送失败"),
SMS_SEND_ERROR_BUSINESS_LIMIT_CONTROL(false, 28007, "短信发送过于频繁"),
DIVIDE_ZERO(false, 29001, "除零错误"),
DATA_NULL(false, 30001, "数据不存在!"),
DATA_EXITS(false, 30002, "数据已存在!"),
DATA_NODE_EXITS(false, 30003, "该章节下存在视频课程,请先删除视频课程!");
private final Boolean success;
private final Integer code;
private final String message;
ResultCodeEnum(Boolean success, Integer code, String message) {
this.success = success;
this.code = code;
this.message = message;
}
}
4、创建R.java
package com.stu.base.result;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/**
- 全局统一返回结果
*/
@Data
@ApiModel(value = "全局统一返回结果")
public class R {
@ApiModelProperty(value = "是否成功")
private Boolean success;
@ApiModelProperty(value = "返回码")
private Integer code;
@ApiModelProperty(value = "返回消息")
private String message;
@ApiModelProperty(value = "返回数据")
private Map<String, Object> data = new HashMap<>();
public R() {
}
public static R ok() {
R r = new R();
r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
r.setCode(ResultCodeEnum.SUCCESS.getCode());
r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
return r;
}
public static R error() {
R r = new R();
r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
return r;
}
public static R setResult(ResultCodeEnum resultCodeEnum) {
R r = new R();
r.setSuccess(resultCodeEnum.getSuccess());
r.setCode(resultCodeEnum.getCode());
r.setMessage(resultCodeEnum.getMessage());
return r;
}
public R success(Boolean success) {
this.setSuccess(success);
return this;
}
public R message(String message) {
this.setMessage(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;
}
}
5、service-base里引入common-utils
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>service-base</artifactId>
<packaging>jar</packaging>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>com.stu</groupId>
<artifactId>common_utils</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
</project>
6、使用
以findAllTeacher1访问为例
package com.stu.service.controller;
import com.stu.base.result.R;
import com.stu.service.entity.User;
import com.stu.service.service.IUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
/**
* <p>
* 用户表 前端控制器
* </p>
*
* @author 公众号 小明的学习圈子
* @since 2023-10-25
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Autowired
private IUserService teacherService;
//查询讲师表所有数据
@GetMapping("findAll")
public List<User> findAllTeacher(){
List<User> list = teacherService.list();
return list;
}
@GetMapping("findAll1")
public R findAllTeacher1(){
List<User> list = teacherService.list();
return R.ok().data("list",list);
}
}
五、代码生成器优化
上边的代码生成器没有定义统一的返回数据格式,也没有定义常用的正常改查。上边的CodeGenerator不变,新增一个controller.java.vm文件。
1、在resources下新建templates文件夹,新建controller.java.vm文件
package ${package.Controller};
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import com.stu.base.result.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.web.bind.annotation.PathVariable;
import ${package.Service}.${table.serviceName};
import ${package.Entity}.${entity};
#if(${restControllerStyle})
import org.springframework.web.bind.annotation.RestController;
#else
import org.springframework.stereotype.Controller;
#end
#if(${superControllerClassPackage})
import ${superControllerClassPackage};
#end
/**
* <p>
* $!{table.comment}
* </p>
*
* @author ${author}
* @since ${date}
*/
#if(${restControllerStyle})
@RestController
#else
@Controller
#end
@RequestMapping("#if(${package.ModuleName})/${package.ModuleName}#end/#if(${controllerMappingHyphenStyle})${controllerMappingHyphen}#else${table.entityPath}#end")
#if(${kotlin})
class ${table.controllerName}#if(${superControllerClass}) : ${superControllerClass}()#end
#else
#if(${superControllerClass})
public class ${table.controllerName} extends ${superControllerClass} {
#else
public class ${table.controllerName} {
#end
@Resource
private ${table.serviceName} ${table.entityPath}Service;
/***********************************
* 用途说明:新增或者更新
* 更多内容:公众号 小明的学习圈子 https://www.stucoding.com/
* @param ${table.entityPath}
* 返回值说明:
* @return R
***********************************/
@PostMapping("/saveOrUpdate")
public R saveOrUpdate(@RequestBody ${entity} ${table.entityPath}){
boolean result= ${table.entityPath}Service.saveOrUpdate(${table.entityPath});
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:新增
* @param ${table.entityPath}
* 返回值说明:
* @return R
***********************************/
@PostMapping("add")
public R add(@RequestBody ${entity} ${table.entityPath}){
boolean result= ${table.entityPath}Service.save(${table.entityPath});
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:删除
* @param id
* 返回值说明:
* @return R
***********************************/
@DeleteMapping("/del/{id}")
public R delete(@PathVariable String id){
boolean result= ${table.entityPath}Service.removeById(id);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:批量删除
* @param ids
* 返回值说明:
* @return R
***********************************/
@PostMapping("/del/batch")
public R deleteBatch(@RequestBody List<String> ids){
boolean result= ${table.entityPath}Service.removeByIds(ids);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:取得所有对象
* @param:
* 返回值说明:
* @return R
***********************************/
@GetMapping
public R findAll(){
return R.ok().data("data", ${table.entityPath}Service.list());
}
/***********************************
* 用途说明:根据id取得对象
* @param id
* 返回值说明:
* @return R
***********************************/
@GetMapping("/{id}")
public R findOne(@PathVariable String id){
return R.ok().data("data", ${table.entityPath}Service.getById(id));
}
/***********************************
* 用途说明:分页
* @param pageNum pageSize ${table.entityPath}
* 返回值说明:
* @return R
***********************************/
@PostMapping("/page")
public R findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestBody ${entity} ${table.entityPath}){
QueryWrapper<${entity}> queryWrapper=new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return R.ok().data("data", ${table.entityPath}Service.page(new Page<>(pageNum,pageSize),queryWrapper));
}
}
#end
2、运行controller.java.vm生成代码如下
package com.stu.service.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.List;
import com.stu.base.result.*;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import org.springframework.web.bind.annotation.PathVariable;
import com.stu.service.service.IUserService;
import com.stu.service.entity.User;
import org.springframework.web.bind.annotation.RestController;
/**
* <p>
* 用户表
* </p>
*
* @author 公众号 小明的学习圈子
* @since 2023-10-26
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private IUserService userService;
/***********************************
* 用途说明:新增或者更新
* 更多内容:公众号 小明的学习圈子 https://www.stucoding.com/
* @param user
* 返回值说明:
* @return R
***********************************/
@PostMapping("/saveOrUpdate")
public R saveOrUpdate(@RequestBody User user){
boolean result= userService.saveOrUpdate(user);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:新增
* @param user
* 返回值说明:
* @return R
***********************************/
@PostMapping("add")
public R add(@RequestBody User user){
boolean result= userService.save(user);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:删除
* @param id
* 返回值说明:
* @return R
***********************************/
@DeleteMapping("/del/{id}")
public R delete(@PathVariable String id){
boolean result= userService.removeById(id);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:批量删除
* @param ids
* 返回值说明:
* @return R
***********************************/
@PostMapping("/del/batch")
public R deleteBatch(@RequestBody List<String> ids){
boolean result= userService.removeByIds(ids);
if(result){
return R.ok().message(ResultCodeEnum.SUCCESS.getMessage());
}else{
return R.error().message(ResultCodeEnum.UPDATE_ERROR.getMessage());
}
}
/***********************************
* 用途说明:取得所有对象
* @param:
* 返回值说明:
* @return R
***********************************/
@GetMapping
public R findAll(){
return R.ok().data("data", userService.list());
}
/***********************************
* 用途说明:根据id取得对象
* @param id
* 返回值说明:
* @return R
***********************************/
@GetMapping("/{id}")
public R findOne(@PathVariable String id){
return R.ok().data("data", userService.getById(id));
}
/***********************************
* 用途说明:分页
* @param pageNum pageSize user
* 返回值说明:
* @return R
***********************************/
@PostMapping("/page")
public R findPage(@RequestParam Integer pageNum,
@RequestParam Integer pageSize,
@RequestBody User user){
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
queryWrapper.orderByDesc("id");
return R.ok().data("data", userService.page(new Page<>(pageNum,pageSize),queryWrapper));
}
}
六、统一异常处理
我们想让异常结果也显示为统一的返回结果对象,并且统一处理系统的异常信息,那么需要统一异常处理。
1、创建统一异常处理器
在service_base中创建统一异常处理类GlobalExceptionHandler.java
package com.stu.service.base.handler;
import com.stu.base.result.R;
import com.stu.base.utils.ExceptionUtils;
import com.stu.service.base.exception.CustomException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
/******************************
* 用途说明:统一异常处理
* 作者姓名: Administrator
* 创建时间: 2022-04-17 23:32
******************************/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
/***********************************
* 用途说明:自定义异常
* 返回值说明: com.stu.service.base.result.R
***********************************/
@ExceptionHandler(CustomException.class)
@ResponseBody
public R customException(CustomException e){
// e.printStackTrace();
log.error(e.getMessage());
log.error(ExceptionUtils.getMessage(e));
return R.error().message(e.getMessage()).code(e.getCode());
}
/***********************************
* 用途说明:数字异常
* 返回值说明: com.stu.service.base.result.R
***********************************/
@ExceptionHandler(ArithmeticException.class)
@ResponseBody
public R arithmeticException(Exception e){
log.error(ExceptionUtils.getMessage(e));
return R.error().message(e.getMessage());
}
/***********************************
* 用途说明:全局异常
* 返回值说明: com.stu.service.base.result.R
***********************************/
@ExceptionHandler(Exception.class)
@ResponseBody
public R error(Exception e){
log.error(ExceptionUtils.getMessage(e));
return R.error().message(e.getMessage());
}
}
2、自定义异常CustomException
package com.stu.service.base.exception;
import com.stu.base.result.ResultCodeEnum;
import lombok.Data;
/******************************
* 用途说明:自定义异常
* 作者姓名: Administrator
* 创建时间: 2022-04-18 0:00
******************************/
@Data
public class CustomException extends RuntimeException{
private Integer code;
public CustomException(ResultCodeEnum resultCodeEnum){
super(resultCodeEnum.getMessage());
//this.message = resultCodeEnum.getMessage();
this.code = resultCodeEnum.getCode();
}
}
3、定义ExceptionUtils
package com.stu.base.utils;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/******************************
* 用途说明:
* 作者姓名: Administrator
* 创建时间: 2022-04-18 22:26
******************************/
public class ExceptionUtils {
/***********************************
* 用途说明:
* 返回值说明: java.lang.String
***********************************/
public static String getMessage(Exception e) {
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
// 将出错的栈信息输出到printWriter中
e.printStackTrace(pw);
pw.flush();
sw.flush();
} finally {
if (sw != null) {
try {
sw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (pw != null) {
pw.close();
}
}
return sw.toString();
}
}
4、使用
public List<JSONObject> getMenu(String userName) {
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户动态获取菜单
return permissionService.selectPermissionByUserId(user.getId());
}
七、统一日志处理
1、配置日志级别
日志记录器(Logger)的行为是分等级的。如下表所示:
分为:OFF、FATAL、ERROR、WARN、INFO、DEBUG、ALL
默认情况下,spring boot从控制台打印出来的日志级别只有INFO及以上级别,可以配置日志级别
# 设置日志级别
logging:
level:
root: WARN
这种方式只能将日志打印在控制台上。
spring boot内部使用Logback作为日志实现的框架。
配置logback日志
注释掉application.yml中的日志配置
resources 中创建 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="E:/log/stu" />
<!--控制台日志格式:彩色日志-->
<!-- 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)"/>
<!--文件日志格式-->
<property name="FILE_LOG_PATTERN"
value="%date{yyyy-MM-dd HH:mm:ss} |%-5level |%thread |%file:%line |%logger |%msg%n" />
<!--编码-->
<property name="ENCODING"
value="UTF-8" />
<!--输出到控制台-->
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<filter class="ch.qos.logback.classic.filter.ThresholdFilter">
<!--日志级别-->
<level>DEBUG</level>
</filter>
<encoder>
<!--日志格式-->
<Pattern>${CONSOLE_LOG_PATTERN}</Pattern>
<!--日志字符集-->
<charset>${ENCODING}</charset>
</encoder>
</appender>
<!--输出到文件-->
<appender name="INFO_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!--日志过滤器:此日志文件只记录INFO级别的-->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_info.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</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>500MB</maxFileSize>
</timeBasedFileNamingAndTriggeringPolicy>
<!--日志文件保留天数-->
<maxHistory>15</maxHistory>
</rollingPolicy>
</appender>
<appender name="WARN_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器:此日志文件只记录WARN级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_warn.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</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>
</appender>
<appender name="ERROR_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<!-- 日志过滤器:此日志文件只记录ERROR级别的 -->
<filter class="ch.qos.logback.classic.filter.LevelFilter">
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
<!-- 正在记录的日志文件的路径及文件名 -->
<file>${log.path}/log_error.log</file>
<encoder>
<pattern>${FILE_LOG_PATTERN}</pattern>
<charset>${ENCODING}</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>
</appender>
<!--开发环境-->
<springProfile name="dev">
<!--可以灵活设置此处,从而控制日志的输出-->
<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="ERROR">
<appender-ref ref="ERROR_FILE" />
</root>
</springProfile>
</configuration>
2、将错误日志输出到文件
GlobalExceptionHandler.java 中
类上添加注解
@Slf4j
异常输出语句
log.error(e.getMessage());
3、将日志堆栈信息输出到文件
定义工具类,上边已经定义了ExceptionUtils
package com.stu.base.utils;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.StringWriter;
/******************************
* 用途说明:
* 作者姓名: Administrator
* 创建时间: 2022-04-18 22:26
******************************/
public class ExceptionUtils {
/***********************************
* 用途说明:
* 返回值说明: java.lang.String
***********************************/
public static String getMessage(Exception e) {
StringWriter sw = null;
PrintWriter pw = null;
try {
sw = new StringWriter();
pw = new PrintWriter(sw);
// 将出错的栈信息输出到printWriter中
e.printStackTrace(pw);
pw.flush();
sw.flush();
} finally {
if (sw != null) {
try {
sw.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
if (pw != null) {
pw.close();
}
}
return sw.toString();
}
}
调用
log.error(ExceptionUtil.getMessage(e));
八、整合Spring Security
1、在common下创建spring-security模块
2、在spring-security引入相关依赖
<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>com.stu</groupId>
<artifactId>common</artifactId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<artifactId>spring-security</artifactId>
<packaging>jar</packaging>
<dependencies>
<!-- Spring Security依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>com.stu</groupId>
<artifactId>service-base</artifactId>
<version>0.0.1-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>com.github.axet</groupId>
<artifactId>kaptcha</artifactId>
</dependency>
<!-- hutool工具类-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
</dependency>
</dependencies>
</project>
3、在service-acl引入spring-security依赖
<dependencies>
<dependency>
<groupId>com.stu</groupId>
<artifactId>spring-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
</dependencies>
4、springsecurity目录结构
5、验证码认证
首先我们来解决用户认证问题,分为首次登陆,和二次认证。
- 首次登录认证:用户名、密码和验证码完成登录
- 二次token认证:请求头携带Jwt进行身份认证
使用用户名密码来登录的,然后我们还想添加图片验证码,security的所有过滤器都是没有图片验证码的,如果你想用自带的UsernamePasswordAuthenticationFilter,那么我们就在这过滤器之前添加一个图片验证码过滤器。当然了我们也可以通过自定义过滤器继承UsernamePasswordAuthenticationFilter,然后自己把验证码验证逻辑和认证逻辑写在一起,这也是一种解决方式。 我们这次解决方式是在UsernamePasswordAuthenticationFilter之前自定义一个图片过滤器CaptchaFilter,提前校验验证码是否正确,这样我们就可以使用UsernamePasswordAuthenticationFilter了,然后登录正常或失败我们都可以通过对应的Handler来返回我们特定格式的封装结果数据。
5.1、这在之前先写Redis的工具类和配置
package com.stu.security.utils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ZSetOperations;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Component
public class RedisUtil {
@Autowired
private RedisTemplate redisTemplate;
/**
* 指定缓存失效时间
*
* @param key 键
* @param time 时间(秒)
* @return
*/
public boolean expire(String key, long time) {
try {
if (time > 0) {
redisTemplate.expire(key, time, TimeUnit.SECONDS);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据key 获取过期时间
*
* @param key 键 不能为null
* @return 时间(秒) 返回0代表为永久有效
*/
public long getExpire(String key) {
return redisTemplate.getExpire(key, TimeUnit.SECONDS);
}
/**
* 判断key是否存在
*
* @param key 键
* @return true 存在 false不存在
*/
public boolean hasKey(String key) {
try {
return redisTemplate.hasKey(key);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除缓存
*
* @param key 可以传一个值 或多个
*/
@SuppressWarnings("unchecked")
public void del(String... key) {
if (key != null && key.length > 0) {
if (key.length == 1) {
redisTemplate.delete(key[0]);
} else {
redisTemplate.delete(CollectionUtils.arrayToList(key));
}
}
}
//============================String=============================
/**
* 普通缓存获取
*
* @param key 键
* @return 值
*/
public Object get(String key) {
return key == null ? null : redisTemplate.opsForValue().get(key);
}
/**
* 普通缓存放入
*
* @param key 键
* @param value 值
* @return true成功 false失败
*/
public boolean set(String key, Object value) {
try {
redisTemplate.opsForValue().set(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 普通缓存放入并设置时间
*
* @param key 键
* @param value 值
* @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期
* @return true成功 false 失败
*/
public boolean set(String key, Object value, long time) {
try {
if (time > 0) {
redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
} else {
set(key, value);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 递增
*
* @param key 键
* @param delta 要增加几(大于0)
* @return
*/
public long incr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递增因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, delta);
}
/**
* 递减
*
* @param key 键
* @param delta 要减少几(小于0)
* @return
*/
public long decr(String key, long delta) {
if (delta < 0) {
throw new RuntimeException("递减因子必须大于0");
}
return redisTemplate.opsForValue().increment(key, -delta);
}
//================================Map=================================
/**
* HashGet
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return 值
*/
public Object hget(String key, String item) {
return redisTemplate.opsForHash().get(key, item);
}
/**
* 获取hashKey对应的所有键值
*
* @param key 键
* @return 对应的多个键值
*/
public Map<Object, Object> hmget(String key) {
return redisTemplate.opsForHash().entries(key);
}
/**
* HashSet
*
* @param key 键
* @param map 对应多个键值
* @return true 成功 false 失败
*/
public boolean hmset(String key, Map<String, Object> map) {
try {
redisTemplate.opsForHash().putAll(key, map);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* HashSet 并设置时间
*
* @param key 键
* @param map 对应多个键值
* @param time 时间(秒)
* @return true成功 false失败
*/
public boolean hmset(String key, Map<String, Object> map, long time) {
try {
redisTemplate.opsForHash().putAll(key, map);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value) {
try {
redisTemplate.opsForHash().put(key, item, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 向一张hash表中放入数据,如果不存在将创建
*
* @param key 键
* @param item 项
* @param value 值
* @param time 时间(秒) 注意:如果已存在的hash表有时间,这里将会替换原有的时间
* @return true 成功 false失败
*/
public boolean hset(String key, String item, Object value, long time) {
try {
redisTemplate.opsForHash().put(key, item, value);
if (time > 0) {
expire(key, time);
}
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 删除hash表中的值
*
* @param key 键 不能为null
* @param item 项 可以使多个 不能为null
*/
public void hdel(String key, Object... item) {
redisTemplate.opsForHash().delete(key, item);
}
/**
* 判断hash表中是否有该项的值
*
* @param key 键 不能为null
* @param item 项 不能为null
* @return true 存在 false不存在
*/
public boolean hHasKey(String key, String item) {
return redisTemplate.opsForHash().hasKey(key, item);
}
/**
* hash递增 如果不存在,就会创建一个 并把新增后的值返回
*
* @param key 键
* @param item 项
* @param by 要增加几(大于0)
* @return
*/
public double hincr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, by);
}
/**
* hash递减
*
* @param key 键
* @param item 项
* @param by 要减少记(小于0)
* @return
*/
public double hdecr(String key, String item, double by) {
return redisTemplate.opsForHash().increment(key, item, -by);
}
//============================set=============================
/**
* 根据key获取Set中的所有值
*
* @param key 键
* @return
*/
public Set<Object> sGet(String key) {
try {
return redisTemplate.opsForSet().members(key);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 根据value从一个set中查询,是否存在
*
* @param key 键
* @param value 值
* @return true 存在 false不存在
*/
public boolean sHasKey(String key, Object value) {
try {
return redisTemplate.opsForSet().isMember(key, value);
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将数据放入set缓存
*
* @param key 键
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSet(String key, Object... values) {
try {
return redisTemplate.opsForSet().add(key, values);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 将set数据放入缓存
*
* @param key 键
* @param time 时间(秒)
* @param values 值 可以是多个
* @return 成功个数
*/
public long sSetAndTime(String key, long time, Object... values) {
try {
Long count = redisTemplate.opsForSet().add(key, values);
if (time > 0) expire(key, time);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 获取set缓存的长度
*
* @param key 键
* @return
*/
public long sGetSetSize(String key) {
try {
return redisTemplate.opsForSet().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 移除值为value的
*
* @param key 键
* @param values 值 可以是多个
* @return 移除的个数
*/
public long setRemove(String key, Object... values) {
try {
Long count = redisTemplate.opsForSet().remove(key, values);
return count;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//===============================list=================================
/**
* 获取list缓存的内容
*
* @param key 键
* @param start 开始
* @param end 结束 0 到 -1代表所有值
* @return
*/
public List<Object> lGet(String key, long start, long end) {
try {
return redisTemplate.opsForList().range(key, start, end);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 获取list缓存的长度
*
* @param key 键
* @return
*/
public long lGetListSize(String key) {
try {
return redisTemplate.opsForList().size(key);
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
/**
* 通过索引 获取list中的值
*
* @param key 键
* @param index 索引 index>=0时, 0 表头,1 第二个元素,依次类推;index<0时,-1,表尾,-2倒数第二个元素,依次类推
* @return
*/
public Object lGetIndex(String key, long index) {
try {
return redisTemplate.opsForList().index(key, index);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, Object value) {
try {
redisTemplate.opsForList().rightPush(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, Object value, long time) {
try {
redisTemplate.opsForList().rightPush(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @return
*/
public boolean lSet(String key, List<Object> value) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 将list放入缓存
*
* @param key 键
* @param value 值
* @param time 时间(秒)
* @return
*/
public boolean lSet(String key, List<Object> value, long time) {
try {
redisTemplate.opsForList().rightPushAll(key, value);
if (time > 0) expire(key, time);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 根据索引修改list中的某条数据
*
* @param key 键
* @param index 索引
* @param value 值
* @return
*/
public boolean lUpdateIndex(String key, long index, Object value) {
try {
redisTemplate.opsForList().set(key, index, value);
return true;
} catch (Exception e) {
e.printStackTrace();
return false;
}
}
/**
* 移除N个值为value
*
* @param key 键
* @param count 移除多少个
* @param value 值
* @return 移除的个数
*/
public long lRemove(String key, long count, Object value) {
try {
Long remove = redisTemplate.opsForList().remove(key, count, value);
return remove;
} catch (Exception e) {
e.printStackTrace();
return 0;
}
}
//================有序集合 sort set===================
/**
* 有序set添加元素
*
* @param key
* @param value
* @param score
* @return
*/
public boolean zSet(String key, Object value, double score) {
return redisTemplate.opsForZSet().add(key, value, score);
}
public long batchZSet(String key, Set<ZSetOperations.TypedTuple> typles) {
return redisTemplate.opsForZSet().add(key, typles);
}
public void zIncrementScore(String key, Object value, long delta) {
redisTemplate.opsForZSet().incrementScore(key, value, delta);
}
public void zUnionAndStore(String key, Collection otherKeys, String destKey) {
redisTemplate.opsForZSet().unionAndStore(key, otherKeys, destKey);
}
/**
* 获取zset数量
* @param key
* @param value
* @return
*/
public long getZsetScore(String key, Object value) {
Double score = redisTemplate.opsForZSet().score(key, value);
if(score==null){
return 0;
}else{
return score.longValue();
}
}
/**
* 获取有序集 key 中成员 member 的排名 。
* 其中有序集成员按 score 值递减 (从大到小) 排序。
* @param key
* @param start
* @param end
* @return
*/
public Set<ZSetOperations.TypedTuple> getZSetRank(String key, long start, long end) {
return redisTemplate.opsForZSet().reverseRangeWithScores(key, start, end);
}
}
5.2、生成验证码codeConfig 首先我们先生成验证码,之前我们已经引用了google的验证码生成器,我们先来配置一下图片验证码的生成规则:
package com.stu.security.config;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.Properties;
/******************************
* 用途说明: 图片验证码生成规则
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
public class codeConfig {
@Bean
public DefaultKaptcha producer() {
Properties properties = new Properties();
properties.put("kaptcha.border", "no");
properties.put("kaptcha.textproducer.font.color", "black");
properties.put("kaptcha.textproducer.char.space", "4");
properties.put("kaptcha.image.height", "40");
properties.put("kaptcha.image.width", "120");
properties.put("kaptcha.textproducer.font.size", "30");
Config config = new Config(properties);
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
5.3、通过控制器提供生成验证码的方法captcha
package com.stu.service.controller;
import cn.hutool.core.lang.UUID;
import cn.hutool.core.map.MapUtil;
import com.alibaba.fastjson.JSONObject;
import com.google.code.kaptcha.Producer;
import com.stu.base.result.R;
import com.stu.security.utils.Const;
import com.stu.security.utils.RedisUtil;
import com.stu.service.service.IndexService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import sun.misc.BASE64Encoder;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/******************************
* 用途说明:
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@RestController
@CrossOrigin
public class IndexController {
@Autowired
private IndexService indexService;
@Autowired
Producer producer;
@Autowired
RedisUtil redisUtil;
@GetMapping("/captcha")
public R captcha() throws IOException {
String key = UUID.randomUUID().toString();
String code = producer.createText();
// 为了测试
// key = "aaaaa";
// code = "123456";
BufferedImage image = producer.createImage(code);
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
ImageIO.write(image, "jpg", outputStream);
BASE64Encoder encoder = new BASE64Encoder();
String str = "data:image/jpeg;base64,";
String base64Img = str + encoder.encode(outputStream.toByteArray());
redisUtil.hset(Const.CAPTCHA_KEY, key, code, 120);
return R.ok().data("data",MapUtil.builder()
.put("token", key)
.put("captchaImg", base64Img)
.build());
}
/***********************************
* 用途说明:退出
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@PostMapping("logout")
public R logout() {
return R.ok();
}
/***********************************
* 用途说明:
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("getUserInfo")
public R getUserInfo() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
Map<String, Object> userInfo = indexService.getUserInfo(userName);
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data(userInfo).data("permissionList", permissionList);
}
/***********************************
* 用途说明:根据用户明获取动态菜单
* 返回值说明:
* @return com.stu.service.base.result.R
***********************************/
@GetMapping("menu")
public R menu() {
String userName = SecurityContextHolder.getContext().getAuthentication().getName();
List<JSONObject> permissionList = indexService.getMenu(userName);
return R.ok().data("permissionList", permissionList);
}
}
因为前后端分离,我们禁用了session,所以我们把验证码放在了redis中,使用一个随机字符串作为key,并传送到前端,前端再把随机字符串和用户输入的验证码提交上来,这样我们就可以通过随机字符串获取到保存的验证码和用户的验证码进行比较了是否正确了。 然后因为图片验证码的方式,所以我们进行了encode,把图片进行了base64编码,这样前端就可以显示图片了。 5.4、验证码认证过滤器OncePerRequestFilter 图片验证码进行认证验证码是否正确。
6、CaptchaFilter
package com.stu.security.filter;
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.stu.security.utils.Const;
import com.stu.security.utils.RedisUtil;
import com.stu.security.Handler.LoginFailureHandler;
import com.stu.security.exception.CaptchaException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 验证码认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class CaptchaFilter extends OncePerRequestFilter {
@Autowired
RedisUtil redisUtil;
@Autowired
LoginFailureHandler loginFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
String url = httpServletRequest.getRequestURI();
if ("/login".equals(url) && httpServletRequest.getMethod().equals("POST")) {
try {
// 校验验证码
validate(httpServletRequest);
} catch (CaptchaException e) {
// 交给认证失败处理器
loginFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
// 校验验证码逻辑
private void validate(HttpServletRequest httpServletRequest) {
String code = httpServletRequest.getParameter("code");
String key = httpServletRequest.getParameter("token");
if (StringUtils.isBlank(code) || StringUtils.isBlank(key)) {
throw new CaptchaException("验证码错误");
}
if (!code.equals(redisUtil.hget(Const.CAPTCHA_KEY, key))) {
throw new CaptchaException("验证码错误");
}
// 一次性使用
redisUtil.hdel(Const.CAPTCHA_KEY, key);
}
}
7、SecurityConfig
package com.stu.security.config;
import com.stu.security.Handler.JwtAccessDeniedHandler;
import com.stu.security.Handler.JwtLogoutSuccessHandler;
import com.stu.security.Handler.LoginFailureHandler;
import com.stu.security.Handler.LoginSuccessHandler;
import com.stu.security.filter.CaptchaFilter;
import com.stu.security.filter.JwtAuthenticationFilter;
import com.stu.security.security.DefaultPasswordEncoder;
import com.stu.security.security.JwtAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// @Bean
// BCryptPasswordEncoder bCryptPasswordEncoder() {
// return new BCryptPasswordEncoder();
// }
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo",
"/v2/api-docs",//swagger api json
"/swagger-resources/configuration/ui",//用来获取支持的动作
"/swagger-resources",//用来获取api-docs的URI
"/swagger-resources/configuration/security",//安全选项
"/swagger-ui.html", "/doc.html"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
后端进行用户身份认证的时候,通过请求头中获取jwt,然后解析用户名,然后判断用户是否有权限等操作。 自定义一个JwtAuthenticationFilter过滤器用来进行识别jwt。
8、身份认证JwtAuthenticationFilter后端验证
登录成功之后前端获取到jwt的信息,前端jwt是保存在了store和cookie里,然后每次axios请求之前,我们都会添加上我们的请求头信息
if (store.getters.token) {
config.headers['Authorization'] = getToken() // 让每个请求携带自定义token 请根据实际情况自行修改
}
所以后端进行用户身份识别的时候,我们需要通过请求头中获取jwt,然后解析出我们的用户名,这样我们就可以知道是谁在访问我们的接口,然后判断用户是否有权限等操作。
那么我们自定义一个过滤器用来进行识别jwt。
package com.stu.security.filter;
import cn.hutool.core.util.StrUtil;
import com.stu.base.result.R;
import com.stu.security.utils.JwtUtils;
import com.stu.security.utils.ResponseUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.lang.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: Basic身份认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
JwtUtils tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
UserDetailsService userDetailService;
// @Autowired
// private IndexService indexService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
JwtUtils tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// if (request.getRequestURI().indexOf("/home") >= 0 ) {
// chain.doFilter(request, response);
// return;
// }
String jwt = request.getHeader(jwtUtils.getHeader());
System.out.println("jwt============================" + jwt);
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = null;
try {
// usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
usernamePasswordAuthenticationToken = getAuthentication(request);
} catch (Exception e) {
ResponseUtil.out(response, R.error());
}
if (usernamePasswordAuthenticationToken != null) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
ResponseUtil.out(response, R.error());
}
chain.doFilter(request, response);
}
/***********************************
* 用途说明:从request获取token,根据token获取权限列表
* 返回值说明:
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
***********************************/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(jwtUtils.getHeader());
if (token != null && !"".equals(token.trim())) {
Claims claims = jwtUtils.getClaimByToken(token);
String userName = claims.getSubject();
if (null == redisTemplate.opsForValue().get(userName)) {
return null;//indexService.getUserInfo(userName);
}
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (Collections.isEmpty(permissionValueList)) {
return null;
}
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permissionValue);
authorities.add(simpleGrantedAuthority);
}
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
}
上面的代码,获取到用户名之后封装UsernamePasswordAuthenticationToken,之后交给SecurityContextHolder参数传递authentication对象,这样后续security就能获取到当前登录的用户信息了,也就完成了用户认证。 然后配置到SecurityConfig里,这是完整的配置。
9、SecurityConfig
package com.stu.security.config;
import com.stu.security.Handler.JwtAccessDeniedHandler;
import com.stu.security.Handler.JwtLogoutSuccessHandler;
import com.stu.security.Handler.LoginFailureHandler;
import com.stu.security.Handler.LoginSuccessHandler;
import com.stu.security.filter.CaptchaFilter;
import com.stu.security.filter.JwtAuthenticationFilter;
import com.stu.security.security.DefaultPasswordEncoder;
import com.stu.security.security.JwtAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// @Bean
// BCryptPasswordEncoder bCryptPasswordEncoder() {
// return new BCryptPasswordEncoder();
// }
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo",
"/v2/api-docs",//swagger api json
"/swagger-resources/configuration/ui",//用来获取支持的动作
"/swagger-resources",//用来获取api-docs的URI
"/swagger-resources/configuration/security",//安全选项
"/swagger-ui.html", "/doc.html"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}
当认证失败的时候会进入AuthenticationEntryPoint,所以自定义认证失败返回的数据:
10、自定义认证失败返回的数据的JwtAuthenticationEntryPoint类
package com.stu.security.security;
import cn.hutool.json.JSONUtil;
import com.stu.base.result.R;
import com.stu.security.utils.ResponseUtil;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 认证失败入口,全局异常捕获前捕获异常
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
/* response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
ServletOutputStream outputStream = response.getOutputStream();
R result = R.error().message("请先登录");
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();*/
ResponseUtil.out(response,R.error().message("请先登录") );
}
}
如果认证失败,就提示【请先登录】。 然后我们把认证过滤器和认证失败入口配置到SecurityConfig中
11、自定义权限不足的JwtAccessDeniedHandler类 这个也需要配置到SecurityConfig中
package com.stu.security.Handler;
import cn.hutool.json.JSONUtil;
import com.stu.base.result.R;
import com.stu.security.utils.ResponseUtil;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 权限不足操作类,全局异常之后捕获
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
/* response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
ServletOutputStream outputStream = response.getOutputStream();
R result = R.error().message(accessDeniedException.getMessage());
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();*/
ResponseUtil.out(response, R.error().message(accessDeniedException.getMessage()));
}
}
12、Security用户密码身份认证DefaultPasswordEncoder
上边的用户名密码是写在在配置文件中的,而且密码用的是明文,这明显不符合我们的要求,我们的用户必须是存储在数据库中,密码也是得经过加密的。所以我们先来解决这个问题,然后再去弄授权。 自定义密码解析器PasswordEncoder,这里用到MD5对密码进行加密。
package com.stu.security.security;
import com.stu.base.utils.MD5;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/******************************
* 用途说明:
* 作者姓名: Administrator
* 创建时间: 2022-09-01 9:39
******************************/
@Component
public class DefaultPasswordEncoder implements PasswordEncoder {
/**
* 加密密码
*
* @param rawPassword
*/
@Override
public String encode(CharSequence rawPassword) {
return MD5.encrypt(rawPassword.toString());
}
/**
* 判断密码是否正确
*
* @param rawPassword the raw password to encode and match
* @param encodedPassword the encoded password from storage to compare with
* @return true if the raw password, after encoding, matches the encoded password from
* storage
*/
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
return encodedPassword.equals(MD5.encrypt(rawPassword.toString()));
}
}
13、权限授权
权限授权security的重要功能,当用户认证成功之后,我们就知道谁在访问系统接口,但是要知道用户有没有权限来访问我们这个接口,我们需要知道用户有哪些权限,哪些角色,这样security才能我们做权限判断。 权限授权需要定义五张表,分别是用户,角色,用户角色关联,角色权限关联,权限(菜单)这5个表。 1、左侧菜单的显示与否是根据用户查询菜单并封装返回给前端。通过IndexServiceImpl的getMenu方法实现。 2、一般当权限粒度比较细的时候,我们都通过判断用户有没有此菜单或操作的权限,而不是通过角色判断,而用户和菜单是不直接做关联的,是通过用户拥有哪些角色,然后角色拥有哪些菜单权限这样来获得的。这个通过UserDetailsServiceImpl里调用selectPermissionValueListByUserId方法实现,这里会用Redis存储权限。
14、sql查询权限 获取权限的sql,这里分两个sql,一个是超级管理员具有所有权限,另一个是根据用户的id跟角色和权限管理查询出该用户的所具有的权限。
<?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.stu.myserver.mapper.PermissionMapper">
<resultMap id="permissionMap" type="com.stu.myserver.entity.Permission">
<result property="id" column="id"/>
<result property="pid" column="pid"/>
<result property="name" column="name"/>
<result property="type" column="type"/>
<result property="permissionValue" column="permission_value"/>
<result property="path" column="path"/>
<result property="component" column="component"/>
<result property="icon" column="icon"/>
<result property="status" column="status"/>
<result property="isDeleted" column="is_deleted"/>
<result property="gmtCreate" column="gmt_create"/>
<result property="gmtModified" column="gmt_modified"/>
</resultMap>
<!-- 用于select查询公用抽取的列 -->
<sql id="columns">
p.id,p.pid,p.name,p.type,p.permission_value,path,p.component,p.icon,p.status,p.is_deleted,p.gmt_create,p.gmt_modified
</sql>
<select id="selectPermissionByUserId" resultMap="permissionMap">
select
<include refid="columns" />
from acl_user_role ur
inner join acl_role_permission rp on rp.role_id = ur.role_id
inner join acl_permission p on p.id = rp.permission_id
where ur.user_id = #{userId}
and ur.is_deleted = 0
and rp.is_deleted = 0
and p.is_deleted = 0
</select>
<select id="selectPermissionValueByUserId" resultType="String">
select
p.permission_value
from acl_user_role ur
inner join acl_role_permission rp on rp.role_id = ur.role_id
inner join acl_permission p on p.id = rp.permission_id
where ur.user_id = #{userId}
and ur.is_deleted = 0
and rp.is_deleted = 0
and p.is_deleted = 0
</select>
<select id="selectAllPermissionValue" resultType="String">
select
permission_value
from acl_permission
where is_deleted = 0
</select>
</mapper>
15、赋予用户权限的地方
15.1、用户登录,调用UserDetailsService.loadUserByUsername()方法时候可以返回用户的权限信息。 SecurityUser类需要实现UserDetails。(UserDetails是springsecurity自带的)
package com.stu.security.entity;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.StringUtils;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: SecurityUser
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Data
@Slf4j
public class SecurityUser implements UserDetails {
//当前登录用户
private transient User currentUserInfo;
//当前登录用户权限
private List<String> permissionValueList;
public SecurityUser() {
}
public SecurityUser(User user) {
if (user != null) {
this.currentUserInfo = user;
}
}
/***********************************
* 用途说明:获取当前用户的所有权限
* 返回值说明:
* @return java.util.Collection<? extends org.springframework.security.core.GrantedAuthority>
***********************************/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Collection<GrantedAuthority> authorities = new ArrayList<>();
for(String permissionValue:permissionValueList){
if(StringUtils.isEmpty(permissionValue)){
continue;
}
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(permissionValue);
authorities.add(authority);
}
return authorities;
}
@Override
public String getPassword() {
return currentUserInfo.getPassword();
}
@Override
public String getUsername() {
return currentUserInfo.getUsername();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
user
package com.stu.security.entity;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;
import java.io.Serializable;
/******************************
* 用途说明: user对象
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Data
@ApiModel(value = "User对象", description = "用户表")
public class User implements Serializable {
private static final long serialVersionUID = 1L;
@ApiModelProperty(value = "会员id")
private String id;
@ApiModelProperty(value = "用户名称")
private String username;
@ApiModelProperty(value = "密码")
private String password;
@ApiModelProperty(value = "昵称")
private String nickName;
@ApiModelProperty(value = "用户头像")
private String salt;
@ApiModelProperty(value = "用户签名")
private String token;
}
15.2、UserDetailsServiceImpl类需要实现UserDetailsService。(UserDetailsService是springsecurity自带的)
package com.stu.service.service.impl;
import com.stu.base.result.ResultCodeEnum;
import com.stu.security.entity.User;
import com.stu.service.service.IPermissionService;
import com.stu.service.service.IUserService;
import com.stu.security.entity.SecurityUser;
import com.stu.service.base.exception.CustomException;
import com.stu.service.service.IndexService;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
/******************************
* 用途说明: 自定义UserDetailsService实现类,认证用户详情
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Service("userDetailsService")
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private IUserService userService;
@Autowired
private IPermissionService permissionService;
@Autowired
private IndexService indexService;
/**
* 根据用户名查询用户信息
*
* @param username the username identifying the user whose data is required.
* @return a fully populated user record (never <code>null</code>)
* @throws UsernameNotFoundException if the user could not be found or the user has no
* GrantedAuthority
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
com.stu.service.entity.User user = userService.selectByUserName(username);
if (user == null) {
throw new CustomException(ResultCodeEnum.LOGIN_MOBILE_ERROR);
}
// 返回UserDetails实现类
com.stu.security.entity.User curUser = new User();
BeanUtils.copyProperties(user, curUser);
//根据用户id查询有权限的菜单
List<String> authorities = permissionService.selectPermissionValueListByUserId(user.getId());
SecurityUser securityUser = new SecurityUser(curUser);
securityUser.setPermissionValueList(authorities);
indexService.getUserInfo("admin");
return securityUser;
}
}
15.3、接口调用进行身份认证过滤器JWTAuthenticationFilter的时候,需要返回用户权限信息
package com.stu.security.filter;
import cn.hutool.core.util.StrUtil;
import com.stu.base.result.R;
import com.stu.security.utils.JwtUtils;
import com.stu.security.utils.ResponseUtil;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.lang.Collections;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.util.StringUtils;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
/******************************
* 用途说明: Basic身份认证过滤器
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
@Autowired
JwtUtils jwtUtils;
@Autowired
JwtUtils tokenManager;
@Autowired
private RedisTemplate redisTemplate;
@Autowired
UserDetailsService userDetailService;
// @Autowired
// private IndexService indexService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager,
JwtUtils tokenManager,
RedisTemplate redisTemplate) {
super(authenticationManager);
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
super(authenticationManager);
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
// if (request.getRequestURI().indexOf("/home") >= 0 ) {
// chain.doFilter(request, response);
// return;
// }
String jwt = request.getHeader(jwtUtils.getHeader());
System.out.println("jwt============================" + jwt);
if (StrUtil.isBlankOrUndefined(jwt)) {
chain.doFilter(request, response);
return;
}
Claims claim = jwtUtils.getClaimByToken(jwt);
if (claim == null) {
throw new JwtException("token 异常");
}
if (jwtUtils.isTokenExpired(claim)) {
throw new JwtException("token已过期");
}
String username = claim.getSubject();
// 获取用户的权限等信息
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = null;
try {
// usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(username, null, userDetailService.getUserAuthority(sysUser.getId()));
usernamePasswordAuthenticationToken = getAuthentication(request);
} catch (Exception e) {
ResponseUtil.out(response, R.error());
}
if (usernamePasswordAuthenticationToken != null) {
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
} else {
ResponseUtil.out(response, R.error());
}
chain.doFilter(request, response);
}
/***********************************
* 用途说明:从request获取token,根据token获取权限列表
* 返回值说明:
* @return org.springframework.security.authentication.UsernamePasswordAuthenticationToken
***********************************/
private UsernamePasswordAuthenticationToken getAuthentication(HttpServletRequest request) {
String token = request.getHeader(jwtUtils.getHeader());
if (token != null && !"".equals(token.trim())) {
Claims claims = jwtUtils.getClaimByToken(token);
String userName = claims.getSubject();
if (null == redisTemplate.opsForValue().get(userName)) {
return null;//indexService.getUserInfo(userName);
}
List<String> permissionValueList = (List<String>) redisTemplate.opsForValue().get(userName);
Collection<GrantedAuthority> authorities = new ArrayList<>();
if (Collections.isEmpty(permissionValueList)) {
return null;
}
for (String permissionValue : permissionValueList) {
if (StringUtils.isEmpty(permissionValue)) {
continue;
}
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permissionValue);
authorities.add(simpleGrantedAuthority);
}
return new UsernamePasswordAuthenticationToken(userName, token, authorities);
}
return null;
}
}
15.4、indexService的获取权限的接口
package com.stu.service.service.impl;
import com.alibaba.fastjson.JSONObject;
import com.stu.base.result.ResultCodeEnum;
import com.stu.service.base.exception.CustomException;
import com.stu.service.entity.Role;
import com.stu.service.entity.User;
import com.stu.service.service.IPermissionService;
import com.stu.service.service.IRoleService;
import com.stu.service.service.IUserService;
import com.stu.service.service.IndexService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/******************************
* 用途说明:
* 作者姓名: Administrator
* 创建时间: 2022-09-01 22:34
******************************/
@Service
public class IndexServiceImpl implements IndexService {
@Autowired
private IUserService userService;
@Autowired
private IRoleService roleService;
@Autowired
private IPermissionService permissionService;
@Autowired
private RedisTemplate redisTemplate;
/***********************************
* 用途说明:根据用户明获取用户登录信息
* @param userName
* 返回值说明:
* @return java.util.Map<java.lang.String, java.lang.Object>
***********************************/
@Override
public Map<String, Object> getUserInfo(String userName) {
Map<String, Object> result = new HashMap<>();
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户id获取角色
List<Role> roleList = roleService.selectRoleByUserId(user.getId());
//转换成角色名称列表
List<String> roleNameList = roleList.stream()
.map(item -> item.getRoleName()).collect(Collectors.toList());
//前端框架必须返回一个角色,否则报错,如果没有角色,返回一个空角色
if (roleNameList.size() == 0) {
roleNameList.add("");
}
List<String> permissionValueList = permissionService.selectPermissionValueListByUserId(user.getId());
redisTemplate.opsForValue().set(userName, permissionValueList);
List<String> permissionValueLisst = (List<String>) redisTemplate.opsForValue().get(userName);
result.put("name", user.getUsername());
result.put("roles", roleNameList);
result.put("permissionValueList", permissionValueList);
return result;
}
/***********************************
* 用途说明:根据用户动态获取菜单
* @param userName
* 返回值说明:
* @return java.util.List<org.json.JSONObject>
***********************************/
@Override
public List<JSONObject> getMenu(String userName) {
User user = userService.selectByUserName(userName);
if (user == null) {
throw new CustomException(ResultCodeEnum.FETCH_USERINFO_ERROR);
}
//根据用户动态获取菜单
return permissionService.selectPermissionByUserId(user.getId());
}
}
16、Security内置的权限注解: 授权、验证权限的流程:
- 用户登录或者调用接口时候识别到用户,并获取到用户的权限信息
- 注解标识Controller中的方法需要的权限或角色
- Security通过FilterSecurityInterceptor匹配URI和权限是否匹配
- 有权限则可以访问接口,当无权限的时候返回异常交给AccessDeniedHandler操作类处理
@PreAuthorize:方法执行前进行权限检查
@PostAuthorize:方法执行后进行权限检查
@Secured:类似于 @PreAuthorize
可以在Controller的方法前添加这些注解表示接口需要什么权限。 比如需要Admin角色权限:
@PreAuthorize("hasRole('admin')")
比如需要添加管理员的操作权限
@PreAuthorize("hasAuthority('user.add')")
ok,我们再来整体梳理一下授权、验证权限的流程:
- 用户登录或者调用接口时候识别到用户,并获取到用户的权限信息
- 注解标识Controller中的方法需要的权限或角色
- Security通过FilterSecurityInterceptor匹配URI和权限是否匹配
- 有权限则可以访问接口,当无权限的时候返回异常交给AccessDeniedHandler操作类处理
17、退出LogoutSuccessHandler
1、调用退出的JwtLogoutSuccessHandler
2、清空Redis缓存的用户权限信息
3、清空token
package com.stu.security.Handler;
import cn.hutool.json.JSONUtil;
import com.stu.security.utils.JwtUtils;
import com.stu.base.result.R;
import com.stu.security.utils.ResponseUtil;
import io.jsonwebtoken.Claims;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
/******************************
* 用途说明: 登出成功处理
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Component
public class JwtLogoutSuccessHandler implements LogoutSuccessHandler {
@Autowired
JwtUtils jwtUtils;
private JwtUtils tokenManager;
private RedisTemplate redisTemplate;
public JwtLogoutSuccessHandler(JwtUtils tokenManager, RedisTemplate redisTemplate) {
this.tokenManager = tokenManager;
this.redisTemplate = redisTemplate;
}
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
if (authentication != null) {
new SecurityContextLogoutHandler().logout(request, response, authentication);
}
String token = request.getHeader(jwtUtils.getHeader());
Claims claim = jwtUtils.getClaimByToken(token);
if (claim != null && !jwtUtils.isTokenExpired(claim)) {
String userName = claim.getSubject();
redisTemplate.delete(userName);
}
response.setHeader(jwtUtils.getHeader(), "");
/*response.setContentType("application/json;charset=UTF-8");
ServletOutputStream outputStream = response.getOutputStream();
R result = R.ok();
outputStream.write(JSONUtil.toJsonStr(result).getBytes("UTF-8"));
outputStream.flush();
outputStream.close();*/
ResponseUtil.out(response,R.ok() );
}
}
18、上边的组件需要配置到SecurityConfig
package com.stu.security.config;
import com.stu.security.Handler.JwtAccessDeniedHandler;
import com.stu.security.Handler.JwtLogoutSuccessHandler;
import com.stu.security.Handler.LoginFailureHandler;
import com.stu.security.Handler.LoginSuccessHandler;
import com.stu.security.filter.CaptchaFilter;
import com.stu.security.filter.JwtAuthenticationFilter;
import com.stu.security.security.DefaultPasswordEncoder;
import com.stu.security.security.JwtAuthenticationEntryPoint;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
/******************************
* 用途说明: Security配置
* 作者姓名: 公众号:小明的学习圈子 https://www.stucoding.com/
* 创建时间: 2022-07-27 23:16
******************************/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
LoginFailureHandler loginFailureHandler;
@Autowired
LoginSuccessHandler loginSuccessHandler;
@Autowired
CaptchaFilter captchaFilter;
@Autowired
JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
@Autowired
JwtAccessDeniedHandler jwtAccessDeniedHandler;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
JwtLogoutSuccessHandler jwtLogoutSuccessHandler;
@Bean
JwtAuthenticationFilter jwtAuthenticationFilter() throws Exception {
JwtAuthenticationFilter jwtAuthenticationFilter = new JwtAuthenticationFilter(authenticationManager());
return jwtAuthenticationFilter;
}
// @Bean
// BCryptPasswordEncoder bCryptPasswordEncoder() {
// return new BCryptPasswordEncoder();
// }
private static final String[] URL_WHITELIST = {
"/user/register",
"/js/**",
"/login",
"/logout",
"/captcha",
"/favicon.ico",
"/login**",
"/login#/login",
"/home/getData",
"captcha/getUserInfo",
"/v2/api-docs",//swagger api json
"/swagger-resources/configuration/ui",//用来获取支持的动作
"/swagger-resources",//用来获取api-docs的URI
"/swagger-resources/configuration/security",//安全选项
"/swagger-ui.html", "/doc.html"
};
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable() // 登录配置
.formLogin()
.successHandler(loginSuccessHandler)
.failureHandler(loginFailureHandler)
.and()
.logout()
.logoutSuccessHandler(jwtLogoutSuccessHandler)
// 禁用session
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// 配置拦截规则
.and()
.authorizeRequests()
.antMatchers(URL_WHITELIST).permitAll()
.anyRequest().authenticated()
// 异常处理器
.and()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
// 配置自定义的过滤器
.and()
.addFilter(jwtAuthenticationFilter())
.addFilterBefore(captchaFilter, UsernamePasswordAuthenticationFilter.class);
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService);
}
}