一.搭建数据库(运行sql脚本文件)
二.搭建前端项目环境
- 前提:已经安装好node.js环境
1. 安装yarn报管理器
2. 给yarn包管理器配置镜像
3.根据前端项目的package.json配置文件启动项目
4. 查看.env配置文件
.env配置文件
# 以下环境变量均为自定义
# 版本号
VITE_VERSION=v1.1.0
# 打包输出目录
VITE_OUTPUT_DIR=dist
# 项目访问基础路径
VITE_BASE_URL=/
# 服务端路径前缀
VITE_API_BASE_URL=/api
# 本地token存储名称
VITE_LOCAL_TOKEN=Sure-Token
# 向服务端发送token信息的名称
VITE_HEADER_TOKEN=Token
# 系统登录路由名称
VITE_LOGIN_ROUTE_NAME=Login
# 后台地址的上下文路径
VITE_WAREHOUSE_CONTEXT_PATH=http://localhost:9999/warehouse
#VITE_WAREHOUSE_CONTEXT_PATH=http://8.141.144.116:9999/warehouse
三.搭建后端环境
1.创建一个空的SpringBoot的项目,然后在慢慢引入依赖
2.导入依赖
控制SpringBoot的版本: 方式一: 在创建boot工程的pom文件中,使用parent标签继承SpringBoot的父工程。 同时指定版本 == 背后其实也是在指定了SpringBoot的父工程继承SpringBoot版本仲裁中心工程的版本, 依次来控制整个SpringBoot所有相关依赖的版本。 方式二: 在依赖版本控制标签<dependencyManagement>标签中引入SpringBoot版本仲裁中心工程并指定版本 == 也就控制了整个SpringBoot所有相关依赖的版本。
<?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>
<!--
控制SpringBoot的版本:
方式一:
在创建boot工程的pom文件中,使用parent标签继承SpringBoot的父工程。
同时指定版本 == 背后其实也是在指定了SpringBoot的父工程继承SpringBoot版本仲裁中心工程的版本,
依次来控制整个SpringBoot所有相关依赖的版本。
方式二:
在依赖版本控制标签<dependencyManagement>标签中引入SpringBoot版本仲裁中心工程并指定版本 ==
也就控制了整个SpringBoot所有相关依赖的版本。
-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.pn</groupId>
<artifactId>warehouse_manager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>warehouse_manager</name>
<description>warehouse_manager</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!--web依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!--mybatis的依赖-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
<!--mysql驱动-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<!--redis的依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
<!--lombok的依赖-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--fastjson的依赖-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.79</version>
</dependency>
<!--JSR-303的依赖(数据校验)-->
<dependency>
<groupId>javax.validation</groupId>
<artifactId>validation-api</artifactId>
<version>2.0.1.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
<version>6.2.0.Final</version>
</dependency>
<!--验证码工具kaptcha的依赖-->
<dependency>
<groupId>com.github.penggle</groupId>
<artifactId>kaptcha</artifactId>
<version>2.3.2</version>
</dependency>
<!--pinyin4j的依赖(将汉字转成拼音的工具)-->
<dependency>
<groupId>com.belerweb</groupId>
<artifactId>pinyin4j</artifactId>
<version>2.5.1</version>
</dependency>
<!--commons-lang工具包(提供了很多工具类)-->
<dependency>
<groupId>commons-lang</groupId>
<artifactId>commons-lang</artifactId>
<version>2.6</version>
</dependency>
<!--jwt的依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.18.3</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<!-- <dependencyManagement>-->
<!-- <dependencies>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-dependencies</artifactId>-->
<!-- <version>${spring-boot.version}</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
<!-- </dependencies>-->
<!-- </dependencyManagement>-->
<!-- <build>-->
<!-- <plugins>-->
<!-- <plugin>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
<!-- </plugin>-->
<!-- </plugins>-->
<!-- </build>-->
<!-- <build>-->
<!-- <plugins>-->
<!-- <plugin>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-maven-plugin</artifactId>-->
<!-- <version>${spring-boot.version}</version>-->
<!-- <executions>-->
<!-- <execution>-->
<!-- <id>repackage</id>-->
<!-- <goals>-->
<!-- <goal>repackage</goal>-->
<!-- </goals>-->
<!-- </execution>-->
<!-- </executions>-->
<!-- </plugin>-->
<!-- </plugins>-->
<!-- </build>-->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
3. 配置mapper接口扫描器
- mapper接口扫描器,指明mapper接口所在的包,然后就会自动为mapper接口创建代理对象并加入到 IOC 容器
@MapperScan(basePackages = "com.pn.mapper")
@SpringBootApplication
public class WarehouseManagerApplication {
public static void main(String[] args) {
SpringApplication.run(WarehouseManagerApplication.class, args);
}
}
4.配置文件的配置
- 目的访问路径
- 项目的访问端口
- mybatis的配置
- redis的配置
- 自定义属性的配置
- token的过期时间
- 图片的上传位置
- 上传的图片保存数据库的访问路径的目录路径
# 服务器配置
# 应用程序上下文路径
server.servlet.context-path=/warehouse
# 服务器端口
server.port=9999
# 数据源配置
# 数据库驱动类名
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
# 数据库连接URL
spring.datasource.url=jdbc:mysql://localhost:3306/db_warehouse?serverTimezone=UTC
# 数据库用户名
spring.datasource.username=root
# 数据库密码
spring.datasource.password=1234
# MyBatis配置
# Mapper的XML文件位置
mybatis.mapper-locations=classpath:mapper/**/*.xml
# 自动将下划线命名转换为驼峰命名
mybatis.configuration.map-underscore-to-camel-case=true
# 配置日志实现
mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl
# 实体类包路径
mybatis.type-aliases-package=com.pn.entity
# Redis配置
# Redis服务器主机地址
spring.redis.host=127.0.0.1
# Redis服务器端口
spring.redis.port=6379
# Redis数据库索引
spring.redis.database=0
# 仓库配置
# 仓库数据过期时间(秒)
warehouse.expire-time=7200
# 文件上传配置
# 文件上传路径
file.upload-path=classpath:static/img/upload
# 文件访问路径
file.access-path=/img/upload/
四.技术补充:JWT - - Token
- token -- 令牌 -- 会话技术 -- 登录成功之后,在一段时间之内不需要重复登录便可以直接访问系统资源;
常见的会话技术
- session: 弊端是只适合单体应用,不适用于分布式微服务集群的项目;
- token -- 令牌 -- 一段字符串: 是适用于分布式微服务集群的项目的会话技术;
- jwt token:
jwt token 组成:
头部(header): { "alg": "HS256", "typ": "JWT" }
载体 -- 存放有用户信息: { "sub": "1234567890", "name": "John Doe", "admin": true }
签名: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret )
加密规则:
base64UrlEncode(header) .base64UrlEncode(payload)
.HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret))
session技术
基于session的认证方式
它的交互流程是,用户认证成功后,在服务端将用户信息保存在session(当前会话)中,发给客户端的sesssion_id存放到cookie中,这样用户客户端请求时带上session_id就可以验证服务器端是否存在 session数据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了
基于token的认证方式
它的交互流程是,用户认证成功后,服务端生成一个token(令牌,一个字符串标识)发给客户端,客户端可以放到cookie或 localStorage等存储中,每次请求时带上token,服务端收到token通过验证后即可确认用户身份。这种方式主要用于分布式系统中,将token和用户信息存储在Redis中,实现会话共享。
基于JWT--Token的认证方式
1.JWT--Token简介
Json Web Token(JWT),是一种⽤于通信双⽅之间传递信息的简洁的、安全的声明规范;作为⼀个开放的标准(RFC 7519),它定义了一种简洁的、自包含的协议格式,用于在通信双方之间以Json对象的形式安全的传递信息。
JWT一般被用来在身份提供者和服务提供者之间传递被认证的用户身份信息,即传递Token,以便于从资源服务器获取资源;特别适用于分布式站点的单点登录(SSO)场景。
2.跨域认证
例如,A网站和B网站是同一家公司的关联服务,现在要求,用户只要在其中一个网站登录,再访问另一个网站就无需登录。如何实现?
(1)方案一
将Token持久化,保存到持久层;所有服务收到请求后,都从持久层获取Token进行校验。这种方案的优点是架构清晰;缺点是工程量比较大,另外,持久层万一挂了,就会单点失败。
(2)方案二
数据库不再保存 Token了,所有Token都保存在客户端,每次请求都将Token发回服务器,服务器解析校验就行了。JWT就是这种方案的一个代表。
3.总结
JWT是一种用于传递Token的解决方案,而且可以无需持久化Token实现跨域认证。
通俗来讲,JWT是一个含签名并携带用户相关信息的加密串,客户端请求服务端时,请求中携带JWT串到服务端,服务端通过签名加密串匹配校验,保证信息未被篡改,校验通过则认为是可靠的请求,将正常返回数据。
4.JWT的原理
JWT的原理是,服务端认证通过以后,会生成一个JSON对象,发回给客户端,就像下面这样:
{
"姓名": "张三",
"角色": "管理员",
"到期时间": "2018年7月1日0点0分"
}
之后,客户端与服务端通信的时候,都要发回这个JSON对象给服务端,服务端完全只靠这个JSON对象认定用户身份。为了防止用户篡改数据,服务端在生成这个JSON对象的时候,还会加上签名。服务端就不保存任何数据了,即服务端变成无状态了,从而比较容易实现扩展。
5.JWT的数据结构
JWT--Token长相
它是一个很长的字符串,中间用点(.)分隔成三个部分。
1.Header(头部)
2.Payload(载体)
3.Signature(签名)
1.Header(头部)
Header部分是一个JSON对象,描述了JWT的元数据,通常是下面的样子:
{
"alg": "HS256",
"typ": "JWT"
}
alg属性表示签名用的算法(algorithm),默认是HMAC SHA256(写成HS256);
typ属性表示这个令牌(token)的类型(type),JWT统一写为JWT;
最后,将上面的JSON对象使用 Base64URL编码转成字符串。
2.Payload(载体)
Payload部分也是一个JSON对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段供选用:
sub(subject):主题
iat(issuedAt):签发时间
exp(expiresAt):过期时间
iss(issuer):签发人
aud(audience):受众
nbf(notBefore):生效时间
jti(jwtId):编号
除了官方字段,还可以在这个部分定义私有字段,例如:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
JWT默认是不加密的,任何人都可以读到,所以不要把秘密信息(密码,手机号等)放在这个部分;但也是可以加密的,生成原始Token以后,可以用密钥再加密一次。
这个JSON对象也要使用Base64URL编码转成字符串。
3.Signature(签名)
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。
然后,使用Header里面指定的算法(HMAC SHA256),按照下面的公式产生签名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
最后,算出签名以后,把Header、Payload、Signature三个部分拼成一个字符串,每个部分之间用点(.)分隔,就可以返回给用户了。
6.JWT的传递方式
客户端收到服务端返回的JWT串后,可以储存在Cookie里面,也可以储存在localStorage。
此后,客户端每次请求服务端,请求中都要带上这个JWT串。可以把它放在Cookie里面自动发送,但是这样不能跨域,所以更好的做法是放在请求头Authorization里面,或放在POST请求的数据体里面。
7.JWT的API
创建一个maven工程:
添加jwt的依赖:
pom.xml:
<?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>
<groupId>com.mmy</groupId>
<artifactId>jwt-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<!--jwt依赖-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
</dependencies>
</project>
编码操作:
src/main/java/com.mmy.jwt.Demo.java:
package com.mmy.jwt;
public class Demo {
public static void main(String[] args) {
/*
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.
eyJzdWIiOiJ1c2VyLWluZm8iLCJuYW1lIjoibGlzaSIsImlkIjoxMDEsImV4cCI6MTY1NjgyMzU1
MywiaWF0IjoxNjU2ODIzNDMzfQ.
IghmOPUVvQCGAtXUZaA4udE-4A7h_RCr3mDkID6qlm0
*/
System.out.println(createToken());
parseToken(createToken());//101 lisi
}
//生成jwt加密串 --- token
public static String createToken(){
//创建Map<String,Object>封装Header信息
Map<String,Object> header = new HashMap<>();
header.put("alg", "HS256");//签名用的算法
header.put("typ", "JWT");//token类型,统一为JWT
//当前时间 -- 签发时间
Date createTime = new Date();
//两小时后的时间 -- 过期时间
Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, 2);
Date expireTime = calendar.getTime();
String token = JWT.create()
//设置Header
.withHeader(header)
//设置Payload(载体)
.withSubject("user-info")//设置主题
.withIssuedAt(createTime)//设置签发时间
.withExpiresAt(expireTime)//设置过期时间
.withClaim("id", 101)//设置私有字段id
.withClaim("name", "lisi")//设置私有字段name
//设置Signature(签名)
.sign(Algorithm.HMAC256("mmy-123"));//指定秘钥为mmy-123
return token;
}
//解析jwt加密串
public static void parseToken(String token){
//指定秘钥拿到JWT解析器
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("mmy-123")).build();
//传递jwt加密串(token),拿到解析后的jwt串
DecodedJWT decodedJWT = jwtVerifier.verify(token);
//从解析后的jwt串中获取相关信息
Claim idClaim = decodedJWT.getClaim("id");//用户id--私有字段id
System.out.println(idClaim.asInt());
Claim nameClaim = decodedJWT.getClaim("name");//用户名--私有字段name
System.out.println(nameClaim.asString());
}
}
五.定义各种配置类、实体类、异常类、工具类
1.配置类
1.生成图片验证码 CaptchaConfig
package com.pn.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;
/**
* 验证码工具kaptcha的配置类
*/
/**
* 配置验证码的类,用于设置验证码的各种属性。
*/
@Configuration
public class CaptchaConfig {
/**
* 创建并配置验证码生成器的bean。
*
* @return 配置好的验证码生成器。
*/
/**
* 配置Producer接口的实现类DefaultKaptcha的bean对象,该对象用于生成验证码图片;
* 并给其指定生成的验证码图片的设置项;bean对象的id引用名为captchaProducer;
*/
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
// 创建DefaultKaptcha实例
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
// 配置验证码的属性
Properties properties = new Properties();
// 设置是否有边框
//是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty("kaptcha.border", "yes");
// 设置边框颜色
//边框颜色 默认为Color.BLACK
properties.setProperty("kaptcha.border.color", "105,179,90");
// 设置文本颜色
//验证码文本字符颜色 默认为Color.BLACK
properties.setProperty("kaptcha.textproducer.font.color", "blue");
// 设置图片宽度
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "120");
// 设置图片高度
//验证码图片高度 默认为50
properties.setProperty("kaptcha.image.height", "40");
// 设置字体大小
//验证码文本字符大小 默认为40
properties.setProperty("kaptcha.textproducer.font.size", "32");
// 设置会话键名
//KAPTCHA_SESSION_KEY
properties.setProperty("kaptcha.session.key", "kaptchaCode");
// 设置字符间距
//验证码文本字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
// 设置字符长度
//验证码文本字符长度 默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
// 设置字体样式
//验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier");
// 设置噪点颜色
//验证码噪点颜色 默认为Color.BLACK
properties.setProperty("kaptcha.noise.color", "gray");
// 创建Config实例并设置属性
Config config = new Config(properties);
// 设置验证码生成器的配置
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
2.异常类
1.用户操作不当导致的异常 BusinessException
package com.pn.exception;
/**
* 用户操作不当导致的异常
*/
/**
* 业务异常类,用于表示在业务逻辑执行过程中发生的异常情况。
* 继承自RuntimeException,表示这是一种非检查(Unchecked)异常。
* 通过定义这个类,可以更细致地管理业务逻辑中可能出现的异常,提供更丰富的异常信息。
*/
public class BusinessException extends RuntimeException{
/**
* 构造函数,用于创建一个无消息的业务异常实例。
*/
public BusinessException() {
}
/**
* 构造函数,用于创建一个带有消息的业务异常实例。
* @param message 异常的消息,用于描述异常的具体情况。
*/
public BusinessException(String message) {
super(message);
}
/**
* 构造函数,用于创建一个带有消息和原因的业务异常实例。
* @param message 异常的消息,用于描述异常的具体情况。
* @param cause 异常的原因,即导致这个异常发生的根本原因。
*/
public BusinessException(String message, Throwable cause) {
super(message, cause);
}
/**
* 构造函数,用于创建一个带有原因的业务异常实例。
* @param cause 异常的原因,即导致这个异常发生的根本原因。
*/
public BusinessException(Throwable cause) {
super(cause);
}
/**
* 构造函数,用于创建一个带有消息、原因、是否允许抑制和是否允许修改堆栈跟踪的业务异常实例。
* @param message 异常的消息,用于描述异常的具体情况。
* @param cause 异常的原因,即导致这个异常发生的根本原因。
* @param enableSuppression 是否允许抑制异常,即是否可以不抛出这个异常。
* @param writableStackTrace 是否允许修改堆栈跟踪,即是否可以改变异常的堆栈跟踪信息。
*/
public BusinessException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
2. 系统异常 SysException
package com.pn.exception;
/**
* 系统异常
*/
/**
* 自定义系统异常类,继承自RuntimeException。
* 该类用于表示系统运行时发生的异常情况,可以根据需要捕获并处理这些异常。
*/
public class SysException extends RuntimeException{
/**
* 构造函数,无参。
* 用于创建一个不带消息和原因的系统异常实例。
*/
public SysException() {
}
/**
* 构造函数,带异常消息。
* @param message 异常的消息,用于描述异常的具体情况。
*/
public SysException(String message) {
super(message);
}
/**
* 构造函数,带异常消息和原因。
* @param message 异常的消息。
* @param cause 异常的原因,即导致当前异常的另一个异常。
*/
public SysException(String message, Throwable cause) {
super(message, cause);
}
/**
* 构造函数,带原因。
* @param cause 异常的原因。
*/
public SysException(Throwable cause) {
super(cause);
}
/**
* 构造函数,带异常消息、原因、是否允许抑制和是否允许写入堆栈跟踪。
* @param message 异常的消息。
* @param cause 异常的原因。
* @param enableSuppression 是否允许抑制异常,即是否允许在特定情况下不抛出异常。
* @param writableStackTrace 是否允许写入堆栈跟踪,即是否在序列化时保留堆栈跟踪。
*/
public SysException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(message, cause, enableSuppression, writableStackTrace);
}
}
3. 工具类
1. CurrentUser
package com.pn.utils;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.ToString;
/**
* 此User类只封装了用户的用户id、用户名和真实姓名
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString
public class CurrentUser {
private int userId;//用户id
private String userCode;//用户名
private String userName;//真实姓名
}
2. 加密工具类 DigestUtil
package com.pn.utils;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
/**
* 加密工具类,提供MD5加密和HMAC签名功能。
*/
/**
* 加密工具类 -- 提供了MD5加密算法
*/
public class DigestUtil {
// 编码字符集
private static String encodingCharset = "UTF-8";
/**
* 使用HMAC-SHA1算法对给定的值进行签名。
*
* @param aValue 待签名的值
* @return 签名后的字符串
*/
//对参数数据进行MD5加密的算法
public static String hmacSign(String aValue) {
return hmacSign(aValue, "warehouse");
}
/**
* 使用HMAC-SHA1算法和指定的密钥对给定的值进行签名。
*
* @param aValue 待签名的值
* @param aKey 签名所用的密钥
* @return 签名后的字符串
*/
public static String hmacSign(String aValue, String aKey) {
byte k_ipad[] = new byte[64];
byte k_opad[] = new byte[64];
byte keyb[];
byte value[];
try {
keyb = aKey.getBytes(encodingCharset);
value = aValue.getBytes(encodingCharset);
} catch (UnsupportedEncodingException e) {
keyb = aKey.getBytes();
value = aValue.getBytes();
}
Arrays.fill(k_ipad, keyb.length, 64, (byte) 54);
Arrays.fill(k_opad, keyb.length, 64, (byte) 92);
for (int i = 0; i < keyb.length; i++) {
k_ipad[i] = (byte) (keyb[i] ^ 0x36);
k_opad[i] = (byte) (keyb[i] ^ 0x5c);
}
MessageDigest md = null;
try {
md = MessageDigest.getInstance("MD5");
} catch (NoSuchAlgorithmException e) {
return null;
}
md.update(k_ipad);
md.update(value);
byte dg[] = md.digest();
md.reset();
md.update(k_opad);
md.update(dg, 0, 16);
dg = md.digest();
return toHex(dg);
}
/**
* 将字节数组转换为十六进制字符串。
*
* @param input 待转换的字节数组
* @return 转换后的十六进制字符串
*/
public static String toHex(byte input[]) {
if (input == null)
return null;
StringBuffer output = new StringBuffer(input.length * 2);
for (int i = 0; i < input.length; i++) {
int current = input[i] & 0xff;
if (current < 16)
output.append("0");
output.append(Integer.toString(current, 16));
}
return output.toString();
}
}
3. token工具类 TokenUtils
package com.pn.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.pn.exception.BusinessException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.Date;
import java.util.concurrent.TimeUnit;
/**
* token工具类
*/
/**
* Token工具类,用于生成和解析JWT Token。
*/
@Component
public class TokenUtils {
/**
* Redis模板,用于存储和检索Token。
*/
//注入redis模板
@Autowired
private StringRedisTemplate stringRedisTemplate;
/**
* Token的过期时间,单位为秒。
*/
//注入配置文件中的warehouse.expire-time属性 -- token的过期时间
@Value("${warehouse.expire-time}")//7200
private int expireTime;
/**
* JWT中的claim名称,用于存放用户ID。
*/
//token中存放用户id对应的名字
private static final String CLAIM_NAME_USERID = "CLAIM_NAME_USERID";
/**
* JWT中的claim名称,用于存放用户代码。
*/
//token中存放用户名对应的名字
private static final String CLAIM_NAME_USERCODE = "CLAIM_NAME_USERCODE";
/**
* JWT中的claim名称,用于存放用户姓名。
*/
//token中存放用户真实姓名对应的名字
private static final String CLAIM_NAME_USERNAME = "CLAIM_NAME_USERNAME";
/**
* 根据当前用户和安全密钥生成Token。
*
* @param currentUser 当前用户信息。
* @param securityKey 安全密钥。
* @return 生成的Token。
*/
private String sign(CurrentUser currentUser,String securityKey){
String token = JWT.create()
//jwt token的载体
//给jwt token中存放用户信息
.withClaim(CLAIM_NAME_USERID, currentUser.getUserId())
.withClaim(CLAIM_NAME_USERCODE, currentUser.getUserCode())
.withClaim(CLAIM_NAME_USERNAME, currentUser.getUserName())
.withIssuedAt(new Date())//jwt token颁发时间
.withExpiresAt(new Date(System.currentTimeMillis() + expireTime *1000))//过期时间
// 加密算法,签名就是用户的密码
.sign(Algorithm.HMAC256(securityKey));
return token;
}
/**
* 用户登录时生成Token并保存到Redis。
*
* @param currentUser 当前用户信息。
* @param password 用户密码,作为Token的签发密钥。
* @return 生成的Token。
*/
/**
* 将当前用户信息以用户密码为密钥生成token的方法
*/
public String loginSign(CurrentUser currentUser, String password){
//生成token
String token = sign(currentUser, password);
//将token保存到redis中,并设置token在redis中的过期时间
stringRedisTemplate.opsForValue().set(token, token, expireTime, TimeUnit.SECONDS);
return token;
}
/**
* 根据Token获取当前用户信息。
*
* @param token 用户的Token。
* @return 当前用户信息。
* @throws BusinessException 如果Token无效或解析失败。
*/
/**
* 从客户端归还的token中获取用户信息的方法
*/
public CurrentUser getCurrentUser(String token) {
if(StringUtils.isEmpty(token)){
throw new BusinessException("令牌为空,请登录!");
}
//对token进行解码,获取解码后的token
DecodedJWT decodedJWT = null;
try {
decodedJWT = JWT.decode(token);
} catch (JWTDecodeException e) {
throw new BusinessException("令牌格式错误,请登录!");
}
//从解码后的token中获取用户信息并封装到CurrentUser对象中返回
int userId = decodedJWT.getClaim(CLAIM_NAME_USERID).asInt();//用户账号id
String userCode = decodedJWT.getClaim(CLAIM_NAME_USERCODE).asString();//用户账号
String userName = decodedJWT.getClaim(CLAIM_NAME_USERNAME).asString();//用户姓名
if(StringUtils.isEmpty(userCode) || StringUtils.isEmpty(userName)){
throw new BusinessException("令牌缺失用户信息,请登录!");
}
return new CurrentUser(userId, userCode, userName);
}
}
4. 常量类 interface WarehouseConstants
package com.pn.utils;
/**
* 常量类:
*/
public interface WarehouseConstants {
//用户未审核
public String USER_STATE_NOT_PASS = "0";
//用户已审核
public String USER_STATE_PASS = "1";
//传递token的请求头名称
public String HEADER_TOKEN_NAME = "Token";
}
4. 实体类
1. Auth
package com.pn.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.util.Date;
import java.util.List;
/**
* auth_info表的实体类:
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Auth {
private int authId;//权限(菜单)id
private int parentId;//父权限(菜单)id
private String authName;//权限(菜单)名称
private String authDesc;//权限(菜单)描述
private int authGrade;//权限(菜单)层级
private String authType;//权限(菜单)类型
private String authUrl;//权限(菜单)访问的url接口
private String authCode;//权限(菜单)标识
private int authOrder;//权限(菜单)的优先级
private String authState;//权限(菜单)状态(1.启用,0.禁用)
private int createBy;//创建权限(菜单)的用户id
private Date createTime;//权限(菜单)的创建时间
private int updateBy;//修改权限(菜单)的用户id
private Date updateTime;//权限(菜单)的修改时间
//追加的List<Auth>集合属性 -- 用于存储当前权限(菜单)的子级权限(菜单)
private List<Auth> childAuth;
}
2.
六.登录业务
1.生成验证码图片
(1)创建生成图片验证码的配置类:CaptchaConfig
config/CaptchaConfig
package com.pn.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;
/**
* 验证码工具kaptcha的配置类
*/
/**
* 配置验证码的类,用于设置验证码的各种属性。
*/
@Configuration
public class CaptchaConfig {
/**
* 创建并配置验证码生成器的bean。
*
* @return 配置好的验证码生成器。
*/
/**
* 配置Producer接口的实现类DefaultKaptcha的bean对象,该对象用于生成验证码图片;
* 并给其指定生成的验证码图片的设置项;bean对象的id引用名为captchaProducer;
*/
@Bean(name = "captchaProducer")
public DefaultKaptcha getKaptchaBean() {
// 创建DefaultKaptcha实例
DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
// 配置验证码的属性
Properties properties = new Properties();
// 设置是否有边框
//是否有边框 默认为true 我们可以自己设置yes,no
properties.setProperty("kaptcha.border", "yes");
// 设置边框颜色
//边框颜色 默认为Color.BLACK
properties.setProperty("kaptcha.border.color", "105,179,90");
// 设置文本颜色
//验证码文本字符颜色 默认为Color.BLACK
properties.setProperty("kaptcha.textproducer.font.color", "blue");
// 设置图片宽度
//验证码图片宽度 默认为200
properties.setProperty("kaptcha.image.width", "120");
// 设置图片高度
//验证码图片高度 默认为50
properties.setProperty("kaptcha.image.height", "40");
// 设置字体大小
//验证码文本字符大小 默认为40
properties.setProperty("kaptcha.textproducer.font.size", "32");
// 设置会话键名
//KAPTCHA_SESSION_KEY
properties.setProperty("kaptcha.session.key", "kaptchaCode");
// 设置字符间距
//验证码文本字符间距 默认为2
properties.setProperty("kaptcha.textproducer.char.space", "4");
// 设置字符长度
//验证码文本字符长度 默认为5
properties.setProperty("kaptcha.textproducer.char.length", "4");
// 设置字体样式
//验证码文本字体样式 默认为new Font("Arial", 1, fontSize), new Font("Courier", 1, fontSize)
properties.setProperty("kaptcha.textproducer.font.names", "Arial,Courier");
// 设置噪点颜色
//验证码噪点颜色 默认为Color.BLACK
properties.setProperty("kaptcha.noise.color", "gray");
// 创建Config实例并设置属性
Config config = new Config(properties);
// 设置验证码生成器的配置
defaultKaptcha.setConfig(config);
return defaultKaptcha;
}
}
接口:
(1).interface:表示对外暴露的规则,里面定义的全是抽象方法和全局常量。
(2).api接口 url接口:请求的url地址。(3).生成验证码图片
http://localhost:9999/warehouse/captcha/captchaImage -- 服务器后台生成一张验证码图片,然后响应给前端,前端以img标签展示。(4).登录
http://localhost:3000/ -- 是前端vue项目的访问地址
前端项目目录下的vite.config.js文件:
//服务端代理设置
proxy: {
//如果访问地址以"/api"开头,则自动代理到变量VITE_WAREHOUSE_CONTEXT_PATH所表示的
//服务端地址http://localhost:9999/warehouse
'/api': {
target: env.VITE_WAREHOUSE_CONTEXT_PATH,
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}url接口/api是前端vue项目的接口,是个代理接口,代理的是后台项目的访问路径;
所以只要是前端vue项目发出的请求的地址是http://localhost:3000/api/xxx都是发给后台项目的,访问的后台向的具体的url接口;
http://localhost:3000/api/login -- 访问的是后台项目的/login接口;
{
userCode:"admin"
userPwd:"123456"
verificationCode:"xax4"
}
2.登录
- 先校验验证码是否正确 -- 根据 stringRedisTemplate.hasKey(键)判断(验证码就是键)-- true 表示验证码输入正确
- 对输入的账户和密码进行非空判断
- 根据账号查询用户 -- 查不到表示账号不存在 反之,表示用户存在
- 查询用户状态 -- 是否审核通过
- 对输入的密码进行MD5加密后,与数据库查询到的密码进行比较,不相等表示密码错误
- 生成token令牌并放回前端
3.登录限制
- 使用过滤器拦截非法请求 注意:SecurityFilter类中 redisTemplate不能使用自动注入
必须登录成功之后才能访问系统资源。
使用过滤器实现:
在后台项目中配置一个过滤器,会拦截前端发出的所有请求,拦截之后判断用户是否已经登录
来决定是否允许访问系统资源(url接口);
回顾SpringBoot中配置原生Servlet中的过滤器:
(1)自定义过滤器 -- 定义一个Filter接口的实现类并重写doFilter()方法,doFilter()方法中
就是过滤器拦截到请求执行的内容;
(2)向IOC容器中配置FilterRegistrationBean的bean对象,再将我们自定义的Servlet的过滤器
注册给FilterRegistrationBean的bean对象 -- 注册过滤器
com.pn.filter.SecurityFilter类
package com.pn.filter;
import com.alibaba.fastjson.JSON;
import com.pn.entity.Result;
import com.pn.utils.WarehouseConstants;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;
/**
* 登录限制过滤器:
*/
public class SecurityFilter implements Filter {
//将redis模板定义为其成员变量,这里不能注入ioc容器
private StringRedisTemplate redisTemplate;
//成员变量redis模板的set方法
public void setRedisTemplate(StringRedisTemplate redisTemplate) {
this.redisTemplate = redisTemplate;
}
/**
* 过滤器拦截到请求执行的方法:
*/
@Override
public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)resp;
//获取请求url接口
String path = request.getServletPath();
/*
白名单请求都直接放行:
*/
List<String> urlList = new ArrayList<>();
urlList.add("/captcha/captchaImage");
urlList.add("/login");
urlList.add("/logout");
//对上传图片的url接口/product/img-upload的请求直接放行
urlList.add("/product/img-upload");
//对static下的/img/upload中的静态资源图片的访问直接放行
if(urlList.contains(path)||path.contains("/img/upload")){
chain.doFilter(request, response);
return;
}
/*
其它请求都校验token:
*/
//拿到前端归还的token
// public String HEADER_TOKEN_NAME = "Token"; 前端浏览器请求头就交Token
String clientToken = request.getHeader(WarehouseConstants.HEADER_TOKEN_NAME);
//校验token,校验通过请求放行,
//StringUtils.hasText() 用于检查字符串是否具有文本内容。这里的“文本内容”指的是除了空白字符(如空格、制表符等)之外的任何字符。
if(StringUtils.hasText(clientToken)&&redisTemplate.hasKey(clientToken)){
chain.doFilter(request, response);
return;
}
//校验失败,向前端响应失败的Result对象转成的json串
// 创建一个表示错误结果的对象,错误代码为未登录,错误信息为"请登录!"
Result result = Result.err(Result.CODE_ERR_UNLOGINED, "请登录!");
// 将错误结果对象转换为JSON字符串
String jsonStr = JSON.toJSONString(result);
// 设置响应类型为JSON,指定字符集为UTF-8
response.setContentType("application/json;charset=UTF-8");
// 获取响应的打印writer,用于向客户端发送响应
PrintWriter out = response.getWriter();
// 将JSON字符串写入响应中
out.print(jsonStr);
// 刷新输出流,确保所有数据都被写入
out.flush();
// 关闭输出流
out.close();
}
}
com.pn.config.ServletConfig类
package com.pn.config;
import com.pn.filter.SecurityFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.StringRedisTemplate;
/**
* 原生Servlet的配置类:
*/
/**
* ServletConfig配置类
* 用于配置应用程序的特定于Servlet的设置,以及注册自定义过滤器。
*/
@Configuration
public class ServletConfig {
// 使用@Autowired注解将StringRedisTemplate注入到类中,以便在SecurityFilter中使用
// 注入redis模板
@Autowired
private StringRedisTemplate redisTemplate;
/**
* 注册安全过滤器
*
* @return FilterRegistrationBean 对象,用于注册和配置SecurityFilter
*
* 此方法创建并配置了一个过滤器注册对象,用于拦截应用程序的所有请求。
* 它通过设置过滤器对象和指定拦截的URL模式来实现。
*/
/**
* 注册原生Servlet的Filter
*/
@Bean
public FilterRegistrationBean securityFilter(){
// 创建FilterRegistrationBean实例,用于注册过滤器
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
// 创建SecurityFilter实例,用于实现具体的过滤逻辑
// 创建SecurityFilter对象
SecurityFilter securityFilter = new SecurityFilter();
// 将redisTemplate注入到SecurityFilter中,以便在过滤过程中使用
// 这一行代码的作用是将 StringRedisTemplate 实例注入到 SecurityFilter 对象中。
// 这是为了让 SecurityFilter 能够访问和使用 Redis 缓存。
//securityFilters是一个自定义的类,里面有一个setRedisTemplate方法,用于设置redisTemplate。
//实现了注入redisTemplate的功能
securityFilter.setRedisTemplate(redisTemplate);
// 将创建的SecurityFilter实例设置到FilterRegistrationBean中
// 注册SecurityFilter
filterRegistrationBean.setFilter(securityFilter);
// 配置过滤器拦截所有请求
// 配置SecurityFilter拦截所有请求
filterRegistrationBean.addUrlPatterns("/*");
return filterRegistrationBean;
}
}
4.获取登录用户信息
- /curr-user -- 拿到前端归还的token,使用token工具类解析token的封装方法拿到从token中
解析出的用户信息并封装到的CurrentUser对象;
com.pn.LoginController类的 currUser方法
/**
* 获取当前登录用户信息的url接口/curr-user
*
* @RequestHeader(WarehouseConstants.HEADER_TOKEN_NAME) String clientToken
* 将请求头Token的值即前端归还的token,赋值给请求处理方法的参数String clientToken
*/
@GetMapping("/curr-user")
public Result currUser(@RequestHeader(WarehouseConstants.HEADER_TOKEN_NAME) String clientToken) {
//从前端归还的token中解析出当前登录用户的信息
// 调用TokenUtils的getCurrentUser方法解析Token中的值
CurrentUser currentUser = tokenUtils.getCurrentUser(clientToken);
return Result.ok(currentUser);
}
5.加载权限菜单树
/user/auth-list
加载用户权限菜单树的方式:
(1)后台系统中是查询出用户权限下的所有菜单List<Auth>, 然后将用户所有菜单的List<Auth>响应给前端,前端框架就使用菜单树组件通过用户所有菜单的List<Auth>中每个菜单的id和pid的关系
生成菜单树; -- 菜单树是前端生成的;
(2)后台系统中是查询出用户权限下的所有菜单List<Auth>,然后将用户所有菜单的List<Auth>转成
菜单树List<Auth>,最后将菜单树List<Auth>响应给前端,前端只需要做个循环迭代展示菜单树;
--- 菜单树是在后端生成的; √
RBAC:用户角色权限控制
通过给用户分配不同的角色,再给角色分配不同的菜单权限,进而实现给用户分配不同的菜单权限;
实现RBAC至少设计到5张表:
1.用户表user_info:存放用户信息 userId userName...
2.角色表role:存放角色信息 roleId roleName
3.用户角色中间表user_role:存放用户角色关系的,体现了给用户分配的角色,而且用户和角色是多对多关系 userId roleId
4.菜单权限表auth_info:存放菜单信息 authId pid authName
5.角色菜单权限中间表role_auth:存放的是角色菜单关系,体现了给角色分配的菜单权限,而且角色和菜单是多对多关系 roleId menuId