Initial commit
This commit is contained in:
@@ -0,0 +1,2 @@
|
|||||||
|
/mvnw text eol=lf
|
||||||
|
*.cmd text eol=crlf
|
||||||
+36
@@ -0,0 +1,36 @@
|
|||||||
|
HELP.md
|
||||||
|
target/
|
||||||
|
.mvn/wrapper/maven-wrapper.jar
|
||||||
|
!**/src/main/**/target/
|
||||||
|
!**/src/test/**/target/
|
||||||
|
|
||||||
|
### STS ###
|
||||||
|
.apt_generated
|
||||||
|
.classpath
|
||||||
|
.factorypath
|
||||||
|
.project
|
||||||
|
.settings
|
||||||
|
.springBeans
|
||||||
|
.sts4-cache
|
||||||
|
|
||||||
|
### IntelliJ IDEA ###
|
||||||
|
.idea
|
||||||
|
*.iws
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
|
||||||
|
### NetBeans ###
|
||||||
|
/nbproject/private/
|
||||||
|
/nbbuild/
|
||||||
|
/dist/
|
||||||
|
/nbdist/
|
||||||
|
/.nb-gradle/
|
||||||
|
build/
|
||||||
|
!**/src/main/**/build/
|
||||||
|
!**/src/test/**/build/
|
||||||
|
|
||||||
|
### VS Code ###
|
||||||
|
.vscode/
|
||||||
|
/.mvn/
|
||||||
|
/logs/
|
||||||
|
/src/test/
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2026 wzy-warehouse
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
# basic_template_not_login_back
|
||||||
|
开发基本模版——后端
|
||||||
|
|
||||||
|
# basic_template_not_login_back
|
||||||
|
|
||||||
|
## 项目介绍
|
||||||
|
`basic_template_not_login_back` 是一个包含后端的基础开发模板,旨在为快速搭建Web应用提供完整的技术栈支持。项目后端基于Spring Boot 3 + MyBatis + PostgreSQL构建,集成了国密算法(SM2/SM3/SM4)加解密功能、动态数据源、Redis缓存等核心功能,可直接作为中小型Web项目的开发起点。
|
||||||
|
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
```
|
||||||
|
basic_template_not_login_back
|
||||||
|
├── bin # 批处理脚本
|
||||||
|
│ └── package.bat # 打包脚本
|
||||||
|
├── src
|
||||||
|
│ ├── main
|
||||||
|
│ │ ├── java/com/gis/basic_template_not_login_back
|
||||||
|
│ │ │ ├── config # 配置类
|
||||||
|
│ │ │ │ ├── CryptoProperties.java # 加解密配置属性
|
||||||
|
│ │ │ │ ├── DataSource.java # 数据源注解
|
||||||
|
│ │ │ │ ├── DataSourceAspect.java # 数据源切面
|
||||||
|
│ │ │ │ ├── DataSourceConfig.java # 数据源配置
|
||||||
|
│ │ │ │ ├── DataSourceContextHolder.java # 数据源上下文
|
||||||
|
│ │ │ │ ├── DynamicDataSource.java # 动态数据源
|
||||||
|
│ │ │ │ └── RedisConfig.java # Redis配置
|
||||||
|
│ │ │ ├── controller # 控制器层
|
||||||
|
│ │ │ │ ├── BaseController.java # 基础控制器
|
||||||
|
│ │ │ │ └── CryptoController.java # 加解密控制器
|
||||||
|
│ │ │ ├── domain # 领域对象
|
||||||
|
│ │ │ │ └── ApiResponse.java # 统一响应类
|
||||||
|
│ │ │ ├── filter # 过滤器
|
||||||
|
│ │ │ │ └── DecryptFilter.java # 解密过滤器
|
||||||
|
│ │ │ ├── mapper # MyBatis Mapper接口
|
||||||
|
│ │ │ ├── service/ex # 服务层异常
|
||||||
|
│ │ │ │ └── ServiceException.java # 业务异常类
|
||||||
|
│ │ │ ├── utils/safety # 安全工具类
|
||||||
|
│ │ │ │ ├── SM2Utils.java # SM2非对称加密工具
|
||||||
|
│ │ │ │ ├── SM3Utils.java # SM3摘要算法工具
|
||||||
|
│ │ │ │ └── SM4Utils.java # SM4对称加密工具
|
||||||
|
│ │ │ ├── vo # 视图对象
|
||||||
|
│ │ │ ├── wrapper # 请求响应包装
|
||||||
|
│ │ │ │ ├── DecryptRequestWrapper.java # 解密请求包装器
|
||||||
|
│ │ │ │ ├── EncryptResponseAdvice.java # 加密响应通知
|
||||||
|
│ │ │ │ └── Sm4KeyHolder.java # SM4密钥持有者
|
||||||
|
│ │ │ └── BasicTemplateNotLoginBackApplication.java # 启动类
|
||||||
|
│ │ └── resources # 资源文件
|
||||||
|
│ │ ├── application-database-dev.yml # 开发环境数据库配置
|
||||||
|
│ │ ├── application-database-prod.yml # 生产环境数据库配置
|
||||||
|
│ │ ├── application-dev.yml # 开发环境配置
|
||||||
|
│ │ ├── application-prod.yml # 生产环境配置
|
||||||
|
│ │ └── application.yml # 主配置文件
|
||||||
|
├── LICENSE # 许可证
|
||||||
|
└── README.md # 介绍文件
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境要求
|
||||||
|
- JDK 17+
|
||||||
|
- Maven 3.6+
|
||||||
|
- PostgreSQL 数据库
|
||||||
|
- Redis 服务器
|
||||||
|
|
||||||
|
### 项目克隆
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/wzy-warehouse/basic_template_not_login_back.git
|
||||||
|
cd basic_template_not_login_back
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
1. **数据库配置**: 修改 `src/main/resources/application-database-dev.yml` 中的数据库连接信息
|
||||||
|
2. **Redis配置**: 修改 `src/main/resources/application.yml` 中的Redis连接信息
|
||||||
|
3. **端口配置**: 在 `application.yml` 中修改 `server.port`(默认8080)
|
||||||
|
|
||||||
|
### 启动方式
|
||||||
|
|
||||||
|
#### 方式一:Maven命令启动
|
||||||
|
```bash
|
||||||
|
# 开发环境(默认)
|
||||||
|
mvn spring-boot:run
|
||||||
|
|
||||||
|
# 生产环境
|
||||||
|
mvn spring-boot:run -Pprod
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式二:打包后运行
|
||||||
|
```bash
|
||||||
|
# 开发环境打包
|
||||||
|
mvn clean package
|
||||||
|
|
||||||
|
# 生产环境打包
|
||||||
|
mvn clean package -Pprod
|
||||||
|
|
||||||
|
# 运行jar包
|
||||||
|
java -jar target/basic_template_not_login_back.jar
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 方式三:使用批处理脚本
|
||||||
|
```bash
|
||||||
|
# Windows系统
|
||||||
|
bin\package.bat
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置文件说明
|
||||||
|
- `application.yml`: 主配置文件,公共配置
|
||||||
|
- `application-dev.yml`: 开发环境特定配置
|
||||||
|
- `application-prod.yml`: 生产环境特定配置
|
||||||
|
- `application-database-dev.yml`: 开发环境数据库配置
|
||||||
|
- `application-database-prod.yml`: 生产环境数据库配置
|
||||||
|
|
||||||
|
## 许可证
|
||||||
|
本项目基于 [MIT License](LICENSE) 开源,详情请查看LICENSE文件。
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
@echo off
|
||||||
|
chcp 65001 >nul
|
||||||
|
setlocal enabledelayedexpansion
|
||||||
|
|
||||||
|
echo ========================================
|
||||||
|
echo Spring Boot Project Package Script
|
||||||
|
echo ========================================
|
||||||
|
echo.
|
||||||
|
|
||||||
|
REM Set project root directory
|
||||||
|
set PROJECT_ROOT=%~dp0..
|
||||||
|
|
||||||
|
echo [1/4] Cleaning previous build...
|
||||||
|
cd /d %PROJECT_ROOT%
|
||||||
|
call mvn clean
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Clean failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Clean completed!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [2/4] Downloading dependencies...
|
||||||
|
call mvn dependency:resolve
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Dependency download failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Dependencies downloaded!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [3/4] Packaging with prod profile...
|
||||||
|
call mvn package -DskipTests -Pprod
|
||||||
|
if errorlevel 1 (
|
||||||
|
echo Package failed!
|
||||||
|
pause
|
||||||
|
exit /b 1
|
||||||
|
)
|
||||||
|
echo Package completed!
|
||||||
|
echo.
|
||||||
|
|
||||||
|
echo [4/4] Searching for generated jar file...
|
||||||
|
for /f "delims=" %%i in ('dir /b /od "%PROJECT_ROOT%\target\*.jar"') do (
|
||||||
|
set JAR_FILE=%%i
|
||||||
|
)
|
||||||
|
|
||||||
|
if defined JAR_FILE (
|
||||||
|
echo ========================================
|
||||||
|
echo Package Success!
|
||||||
|
echo Jar file location: %PROJECT_ROOT%\target\%JAR_FILE%
|
||||||
|
echo ========================================
|
||||||
|
) else (
|
||||||
|
echo Jar file not found!
|
||||||
|
)
|
||||||
|
|
||||||
|
echo.
|
||||||
|
pause
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
# or more contributor license agreements. See the NOTICE file
|
||||||
|
# distributed with this work for additional information
|
||||||
|
# regarding copyright ownership. The ASF licenses this file
|
||||||
|
# to you under the Apache License, Version 2.0 (the
|
||||||
|
# "License"); you may not use this file except in compliance
|
||||||
|
# with the License. You may obtain a copy of the License at
|
||||||
|
#
|
||||||
|
# http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
#
|
||||||
|
# Unless required by applicable law or agreed to in writing,
|
||||||
|
# software distributed under the License is distributed on an
|
||||||
|
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
# KIND, either express or implied. See the License for the
|
||||||
|
# specific language governing permissions and limitations
|
||||||
|
# under the License.
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
#
|
||||||
|
# Optional ENV vars
|
||||||
|
# -----------------
|
||||||
|
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||||
|
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||||
|
# ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
set -euf
|
||||||
|
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||||
|
|
||||||
|
# OS specific support.
|
||||||
|
native_path() { printf %s\\n "$1"; }
|
||||||
|
case "$(uname)" in
|
||||||
|
CYGWIN* | MINGW*)
|
||||||
|
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||||
|
native_path() { cygpath --path --windows "$1"; }
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# set JAVACMD and JAVACCMD
|
||||||
|
set_java_home() {
|
||||||
|
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||||
|
if [ -n "${JAVA_HOME-}" ]; then
|
||||||
|
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||||
|
# IBM's JDK on AIX uses strange locations for the executables
|
||||||
|
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||||
|
else
|
||||||
|
JAVACMD="$JAVA_HOME/bin/java"
|
||||||
|
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||||
|
|
||||||
|
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||||
|
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||||
|
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
JAVACMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v java
|
||||||
|
)" || :
|
||||||
|
JAVACCMD="$(
|
||||||
|
'set' +e
|
||||||
|
'unset' -f command 2>/dev/null
|
||||||
|
'command' -v javac
|
||||||
|
)" || :
|
||||||
|
|
||||||
|
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||||
|
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# hash string like Java String::hashCode
|
||||||
|
hash_string() {
|
||||||
|
str="${1:-}" h=0
|
||||||
|
while [ -n "$str" ]; do
|
||||||
|
char="${str%"${str#?}"}"
|
||||||
|
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||||
|
str="${str#?}"
|
||||||
|
done
|
||||||
|
printf %x\\n $h
|
||||||
|
}
|
||||||
|
|
||||||
|
verbose() { :; }
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||||
|
|
||||||
|
die() {
|
||||||
|
printf %s\\n "$1" >&2
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
trim() {
|
||||||
|
# MWRAPPER-139:
|
||||||
|
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||||
|
# Needed for removing poorly interpreted newline sequences when running in more
|
||||||
|
# exotic environments such as mingw bash on Windows.
|
||||||
|
printf "%s" "${1}" | tr -d '[:space:]'
|
||||||
|
}
|
||||||
|
|
||||||
|
scriptDir="$(dirname "$0")"
|
||||||
|
scriptName="$(basename "$0")"
|
||||||
|
|
||||||
|
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
while IFS="=" read -r key value; do
|
||||||
|
case "${key-}" in
|
||||||
|
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||||
|
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||||
|
esac
|
||||||
|
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
|
||||||
|
case "${distributionUrl##*/}" in
|
||||||
|
maven-mvnd-*bin.*)
|
||||||
|
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||||
|
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||||
|
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||||
|
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||||
|
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||||
|
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||||
|
*)
|
||||||
|
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||||
|
distributionPlatform=linux-amd64
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||||
|
;;
|
||||||
|
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||||
|
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||||
|
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||||
|
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||||
|
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||||
|
|
||||||
|
exec_maven() {
|
||||||
|
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||||
|
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ -d "$MAVEN_HOME" ]; then
|
||||||
|
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
exec_maven "$@"
|
||||||
|
fi
|
||||||
|
|
||||||
|
case "${distributionUrl-}" in
|
||||||
|
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||||
|
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||||
|
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||||
|
trap clean HUP INT TERM EXIT
|
||||||
|
else
|
||||||
|
die "cannot create temp dir"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
verbose "Downloading from: $distributionUrl"
|
||||||
|
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
# select .zip or .tar.gz
|
||||||
|
if ! command -v unzip >/dev/null; then
|
||||||
|
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||||
|
distributionUrlName="${distributionUrl##*/}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# verbose opt
|
||||||
|
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||||
|
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||||
|
|
||||||
|
# normalize http auth
|
||||||
|
case "${MVNW_PASSWORD:+has-password}" in
|
||||||
|
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||||
|
verbose "Found wget ... using wget"
|
||||||
|
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||||
|
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||||
|
verbose "Found curl ... using curl"
|
||||||
|
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||||
|
elif set_java_home; then
|
||||||
|
verbose "Falling back to use Java to download"
|
||||||
|
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||||
|
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
cat >"$javaSource" <<-END
|
||||||
|
public class Downloader extends java.net.Authenticator
|
||||||
|
{
|
||||||
|
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||||
|
{
|
||||||
|
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||||
|
}
|
||||||
|
public static void main( String[] args ) throws Exception
|
||||||
|
{
|
||||||
|
setDefault( new Downloader() );
|
||||||
|
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
END
|
||||||
|
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||||
|
verbose " - Compiling Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||||
|
verbose " - Running Downloader.java ..."
|
||||||
|
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
if [ -n "${distributionSha256Sum-}" ]; then
|
||||||
|
distributionSha256Result=false
|
||||||
|
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||||
|
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||||
|
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
elif command -v sha256sum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
elif command -v shasum >/dev/null; then
|
||||||
|
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||||
|
distributionSha256Result=true
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||||
|
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ $distributionSha256Result = false ]; then
|
||||||
|
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||||
|
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
if command -v unzip >/dev/null; then
|
||||||
|
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||||
|
else
|
||||||
|
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
actualDistributionDir=""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||||
|
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$distributionUrlNameMain"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
# enable globbing to iterate over items
|
||||||
|
set +f
|
||||||
|
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||||
|
if [ -d "$dir" ]; then
|
||||||
|
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||||
|
actualDistributionDir="$(basename "$dir")"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
set -f
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$actualDistributionDir" ]; then
|
||||||
|
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||||
|
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||||
|
die "Could not find Maven distribution directory in extracted archive"
|
||||||
|
fi
|
||||||
|
|
||||||
|
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||||
|
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||||
|
|
||||||
|
clean || :
|
||||||
|
exec_maven "$@"
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
<# : batch portion
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||||
|
@REM or more contributor license agreements. See the NOTICE file
|
||||||
|
@REM distributed with this work for additional information
|
||||||
|
@REM regarding copyright ownership. The ASF licenses this file
|
||||||
|
@REM to you under the Apache License, Version 2.0 (the
|
||||||
|
@REM "License"); you may not use this file except in compliance
|
||||||
|
@REM with the License. You may obtain a copy of the License at
|
||||||
|
@REM
|
||||||
|
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
@REM
|
||||||
|
@REM Unless required by applicable law or agreed to in writing,
|
||||||
|
@REM software distributed under the License is distributed on an
|
||||||
|
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
@REM KIND, either express or implied. See the License for the
|
||||||
|
@REM specific language governing permissions and limitations
|
||||||
|
@REM under the License.
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||||
|
@REM
|
||||||
|
@REM Optional ENV vars
|
||||||
|
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||||
|
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||||
|
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||||
|
@REM ----------------------------------------------------------------------------
|
||||||
|
|
||||||
|
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||||
|
@SET __MVNW_CMD__=
|
||||||
|
@SET __MVNW_ERROR__=
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||||
|
@SET PSModulePath=
|
||||||
|
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||||
|
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||||
|
)
|
||||||
|
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||||
|
@SET __MVNW_PSMODULEP_SAVE=
|
||||||
|
@SET __MVNW_ARG0_NAME__=
|
||||||
|
@SET MVNW_USERNAME=
|
||||||
|
@SET MVNW_PASSWORD=
|
||||||
|
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||||
|
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||||
|
@GOTO :EOF
|
||||||
|
: end batch / begin powershell #>
|
||||||
|
|
||||||
|
$ErrorActionPreference = "Stop"
|
||||||
|
if ($env:MVNW_VERBOSE -eq "true") {
|
||||||
|
$VerbosePreference = "Continue"
|
||||||
|
}
|
||||||
|
|
||||||
|
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||||
|
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||||
|
if (!$distributionUrl) {
|
||||||
|
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||||
|
}
|
||||||
|
|
||||||
|
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||||
|
"maven-mvnd-*" {
|
||||||
|
$USE_MVND = $true
|
||||||
|
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||||
|
$MVN_CMD = "mvnd.cmd"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
$USE_MVND = $false
|
||||||
|
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||||
|
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||||
|
if ($env:MVNW_REPOURL) {
|
||||||
|
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||||
|
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||||
|
}
|
||||||
|
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||||
|
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||||
|
|
||||||
|
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||||
|
if ($env:MAVEN_USER_HOME) {
|
||||||
|
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||||
|
}
|
||||||
|
|
||||||
|
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||||
|
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_WRAPPER_DISTS = $null
|
||||||
|
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||||
|
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||||
|
} else {
|
||||||
|
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||||
|
}
|
||||||
|
|
||||||
|
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||||
|
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||||
|
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||||
|
|
||||||
|
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||||
|
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
|
exit $?
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||||
|
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||||
|
}
|
||||||
|
|
||||||
|
# prepare tmp dir
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||||
|
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||||
|
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||||
|
trap {
|
||||||
|
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||||
|
|
||||||
|
# Download and Install Apache Maven
|
||||||
|
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||||
|
Write-Verbose "Downloading from: $distributionUrl"
|
||||||
|
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||||
|
|
||||||
|
$webclient = New-Object System.Net.WebClient
|
||||||
|
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||||
|
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||||
|
}
|
||||||
|
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||||
|
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||||
|
|
||||||
|
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||||
|
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||||
|
if ($distributionSha256Sum) {
|
||||||
|
if ($USE_MVND) {
|
||||||
|
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||||
|
}
|
||||||
|
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||||
|
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||||
|
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# unzip and move
|
||||||
|
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||||
|
|
||||||
|
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||||
|
$actualDistributionDir = ""
|
||||||
|
|
||||||
|
# First try the expected directory name (for regular distributions)
|
||||||
|
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||||
|
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||||
|
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||||
|
$actualDistributionDir = $distributionUrlNameMain
|
||||||
|
}
|
||||||
|
|
||||||
|
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||||
|
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||||
|
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||||
|
$actualDistributionDir = $_.Name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$actualDistributionDir) {
|
||||||
|
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||||
|
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||||
|
try {
|
||||||
|
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||||
|
} catch {
|
||||||
|
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||||
|
Write-Error "fail to move MAVEN_HOME"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||||
|
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
<?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>
|
||||||
|
|
||||||
|
<properties>
|
||||||
|
<maven.compiler.source>17</maven.compiler.source>
|
||||||
|
<maven.compiler.target>17</maven.compiler.target>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<mybatis.spring.version>3.0.5</mybatis.spring.version>
|
||||||
|
<druid.version>1.2.27</druid.version>
|
||||||
|
<postgresql.version>42.7.8</postgresql.version>
|
||||||
|
<lombok.version>1.18.42</lombok.version>
|
||||||
|
<bcprov-jdk15to18.version>1.82</bcprov-jdk15to18.version>
|
||||||
|
<fastjson.version>2.0.60</fastjson.version>
|
||||||
|
</properties>
|
||||||
|
|
||||||
|
<parent>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-parent</artifactId>
|
||||||
|
<version>3.5.13</version>
|
||||||
|
<relativePath/>
|
||||||
|
</parent>
|
||||||
|
|
||||||
|
<groupId>com.gis</groupId>
|
||||||
|
<artifactId>basic_template_not_login_back</artifactId>
|
||||||
|
<version>0.0.1-SNAPSHOT</version>
|
||||||
|
<name>basic_template_not_login_back</name>
|
||||||
|
<description>basic_template_not_login_back</description>
|
||||||
|
|
||||||
|
<dependencies>
|
||||||
|
<!-- Spring Boot Web 核心依赖 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot 配置处理器 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot验证 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-validation</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- MyBatis 整合 Spring Boot -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.mybatis.spring.boot</groupId>
|
||||||
|
<artifactId>mybatis-spring-boot-starter</artifactId>
|
||||||
|
<version>${mybatis.spring.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 数据库连接池 Druid -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>druid-spring-boot-3-starter</artifactId>
|
||||||
|
<version>${druid.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- postgresql驱动 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<version>${postgresql.version}</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- WebSocket 支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- lombok -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.projectlombok</groupId>
|
||||||
|
<artifactId>lombok</artifactId>
|
||||||
|
<version>${lombok.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 提供 Redis 连接池 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.commons</groupId>
|
||||||
|
<artifactId>commons-pool2</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 国密算法 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bouncycastle</groupId>
|
||||||
|
<artifactId>bcprov-jdk15to18</artifactId>
|
||||||
|
<version>${bcprov-jdk15to18.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot Redis Starter -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- fastjson -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.alibaba</groupId>
|
||||||
|
<artifactId>fastjson</artifactId>
|
||||||
|
<version>${fastjson.version}</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- Spring Boot AOP 支持 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-aop</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
|
<!-- 测试框架 -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
|
||||||
|
<profiles>
|
||||||
|
<!-- 开发环境(默认) -->
|
||||||
|
<profile>
|
||||||
|
<id>dev</id>
|
||||||
|
<properties>
|
||||||
|
<spring.profiles.active>dev</spring.profiles.active>
|
||||||
|
</properties>
|
||||||
|
<activation>
|
||||||
|
<activeByDefault>true</activeByDefault>
|
||||||
|
</activation>
|
||||||
|
</profile>
|
||||||
|
|
||||||
|
<!-- 生产环境 -->
|
||||||
|
<profile>
|
||||||
|
<id>prod</id>
|
||||||
|
<properties>
|
||||||
|
<spring.profiles.active>prod</spring.profiles.active>
|
||||||
|
</properties>
|
||||||
|
</profile>
|
||||||
|
</profiles>
|
||||||
|
|
||||||
|
<build>
|
||||||
|
<!-- 配置资源文件过滤,支持Maven profile变量替换 -->
|
||||||
|
<resources>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>true</filtering>
|
||||||
|
<includes>
|
||||||
|
<include>**/*.yml</include>
|
||||||
|
<include>**/*.yaml</include>
|
||||||
|
<include>**/*.properties</include>
|
||||||
|
<include>**/*.xml</include>
|
||||||
|
</includes>
|
||||||
|
</resource>
|
||||||
|
<resource>
|
||||||
|
<directory>src/main/resources</directory>
|
||||||
|
<filtering>false</filtering>
|
||||||
|
<excludes>
|
||||||
|
<exclude>**/*.yml</exclude>
|
||||||
|
<exclude>**/*.yaml</exclude>
|
||||||
|
<exclude>**/*.properties</exclude>
|
||||||
|
<exclude>**/*.xml</exclude>
|
||||||
|
</excludes>
|
||||||
|
</resource>
|
||||||
|
</resources>
|
||||||
|
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.14.0</version>
|
||||||
|
<configuration>
|
||||||
|
<source>17</source>
|
||||||
|
<target>17</target>
|
||||||
|
<release>17</release>
|
||||||
|
<encoding>UTF-8</encoding>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
<finalName>basic_template_not_login_back</finalName>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
</project>
|
||||||
+29
@@ -0,0 +1,29 @@
|
|||||||
|
package com.gis.basic_template_not_login_back;
|
||||||
|
|
||||||
|
import org.mybatis.spring.annotation.MapperScan;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.SpringApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 后台启动入口
|
||||||
|
* 启动时过滤DataSourceAutoConfiguration,避免数据源自动配置
|
||||||
|
*/
|
||||||
|
@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)
|
||||||
|
@MapperScan("com.gis.basic_template_not_login_back.mapper") // 扫描MyBatis的Mapper接口
|
||||||
|
public class BasicTemplateNotLoginBackApplication {
|
||||||
|
|
||||||
|
// 使用非静态变量
|
||||||
|
@Value("${server.port}")
|
||||||
|
private Integer port;
|
||||||
|
|
||||||
|
public static void main(String[] args) {
|
||||||
|
// 启动Spring Boot应用并获取应用上下文
|
||||||
|
var context = SpringApplication.run(BasicTemplateNotLoginBackApplication.class, args);
|
||||||
|
|
||||||
|
// 从上下文中获取当前实例
|
||||||
|
BasicTemplateNotLoginBackApplication app = context.getBean(BasicTemplateNotLoginBackApplication.class);
|
||||||
|
System.out.println("后端服务启动成功!访问地址: http://localhost:" + app.port);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加解密配置属性类
|
||||||
|
* 从application.yml中读取safety.crypto配置
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
@Component
|
||||||
|
@ConfigurationProperties(prefix = "safety.crypto")
|
||||||
|
public class CryptoProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应无需加密的路径列表
|
||||||
|
*/
|
||||||
|
private List<String> noEncryptPaths = Collections.emptyList();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求无需解密的路径列表
|
||||||
|
*/
|
||||||
|
private List<String> noDecryptPaths = Collections.emptyList();
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import java.lang.annotation.*;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源切换注解
|
||||||
|
* 可用于类或方法上,指定使用的数据源
|
||||||
|
*/
|
||||||
|
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||||
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
|
@Documented
|
||||||
|
public @interface DataSource {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源名称
|
||||||
|
* 可选值:master(主库)、slave等
|
||||||
|
* @return 数据源名称
|
||||||
|
*/
|
||||||
|
String value() default "master";
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.aspectj.lang.ProceedingJoinPoint;
|
||||||
|
import org.aspectj.lang.annotation.Around;
|
||||||
|
import org.aspectj.lang.annotation.Aspect;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
@Aspect
|
||||||
|
@Component
|
||||||
|
@Order(1)
|
||||||
|
@Slf4j
|
||||||
|
public class DataSourceAspect {
|
||||||
|
|
||||||
|
@Around("@annotation(dataSource) || @within(dataSource)")
|
||||||
|
public Object around(ProceedingJoinPoint point, DataSource dataSource) throws Throwable {
|
||||||
|
try {
|
||||||
|
String dsName = dataSource.value();
|
||||||
|
log.debug("切换数据源: {}", dsName);
|
||||||
|
DataSourceContextHolder.setDataSource(dsName);
|
||||||
|
return point.proceed();
|
||||||
|
} finally {
|
||||||
|
DataSourceContextHolder.clearDataSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceBuilder;
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Primary;
|
||||||
|
|
||||||
|
import javax.sql.DataSource;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class DataSourceConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties("spring.datasource.master")
|
||||||
|
public DataSource master() {
|
||||||
|
return DruidDataSourceBuilder.create().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConfigurationProperties("spring.datasource.slave")
|
||||||
|
public DataSource slave() {
|
||||||
|
return DruidDataSourceBuilder.create().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@Primary
|
||||||
|
public DataSource dataSource() {
|
||||||
|
DynamicDataSource ds = new DynamicDataSource();
|
||||||
|
Map<Object, Object> map = new HashMap<>();
|
||||||
|
map.put("master", master());
|
||||||
|
map.put("slave", slave());
|
||||||
|
ds.setTargetDataSources(map);
|
||||||
|
ds.setDefaultTargetDataSource(master());
|
||||||
|
return ds;
|
||||||
|
}
|
||||||
|
}
|
||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 数据源上下文持有者
|
||||||
|
* 使用ThreadLocal存储当前线程使用的数据源名称
|
||||||
|
*/
|
||||||
|
public class DataSourceContextHolder {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 默认数据源名称
|
||||||
|
*/
|
||||||
|
private static final String DEFAULT_DATASOURCE = "master";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ThreadLocal存储数据源名称
|
||||||
|
*/
|
||||||
|
private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置数据源名称
|
||||||
|
* @param dataSourceName 数据源名称
|
||||||
|
*/
|
||||||
|
public static void setDataSource(String dataSourceName) {
|
||||||
|
CONTEXT_HOLDER.set(dataSourceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取数据源名称
|
||||||
|
* @return 数据源名称
|
||||||
|
*/
|
||||||
|
public static String getDataSource() {
|
||||||
|
String dataSource = CONTEXT_HOLDER.get();
|
||||||
|
return dataSource == null ? DEFAULT_DATASOURCE : dataSource;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除数据源名称
|
||||||
|
*/
|
||||||
|
public static void clearDataSource() {
|
||||||
|
CONTEXT_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 动态数据源路由
|
||||||
|
* 根据DataSourceContextHolder中设置的数据源名称,动态选择数据源
|
||||||
|
*/
|
||||||
|
public class DynamicDataSource extends AbstractRoutingDataSource {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Object determineCurrentLookupKey() {
|
||||||
|
return DataSourceContextHolder.getDataSource();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
|
||||||
|
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class RedisConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
|
||||||
|
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||||
|
template.setConnectionFactory(factory);
|
||||||
|
|
||||||
|
// 字符串序列化器(key 用字符串存储)
|
||||||
|
StringRedisSerializer stringSerializer = new StringRedisSerializer();
|
||||||
|
template.setKeySerializer(stringSerializer);
|
||||||
|
template.setHashKeySerializer(stringSerializer);
|
||||||
|
|
||||||
|
// JSON 序列化器(value 用 JSON 存储,支持对象)
|
||||||
|
Jackson2JsonRedisSerializer<Object> jsonSerializer = new Jackson2JsonRedisSerializer<>(new ObjectMapper(), Object.class);
|
||||||
|
template.setValueSerializer(jsonSerializer);
|
||||||
|
template.setHashValueSerializer(jsonSerializer);
|
||||||
|
|
||||||
|
template.afterPropertiesSet();
|
||||||
|
return template;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.controller;
|
||||||
|
|
||||||
|
import com.gis.basic_template_not_login_back.domain.ApiResponse;
|
||||||
|
import com.gis.basic_template_not_login_back.service.ex.ServiceException;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基础controller类,所有controller层对象必须继承这个类
|
||||||
|
*/
|
||||||
|
public class BaseController{
|
||||||
|
|
||||||
|
// 用于统一处理应用层抛出的异常
|
||||||
|
@ExceptionHandler(ServiceException.class)
|
||||||
|
public ApiResponse<Void> handleServiceException(Throwable e) {
|
||||||
|
return ApiResponse.error(e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于统一处理其余抛出的异常
|
||||||
|
@ExceptionHandler(RuntimeException.class)
|
||||||
|
public ApiResponse<Void> handleException(Throwable e) {
|
||||||
|
return ApiResponse.error(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.controller;
|
||||||
|
|
||||||
|
import com.gis.basic_template_not_login_back.domain.ApiResponse;
|
||||||
|
import com.gis.basic_template_not_login_back.utils.safety.SM2Utils;
|
||||||
|
import jakarta.annotation.PostConstruct;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/crypto")
|
||||||
|
public class CryptoController extends BaseController {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Value("${safety.sm2.global}")
|
||||||
|
private String sm2KeyPairRedisKey;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 初始化SM2密钥对
|
||||||
|
*/
|
||||||
|
@PostConstruct
|
||||||
|
public void initSm2KeyPair() {
|
||||||
|
Object sm2KeyPairObj = redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
|
||||||
|
if (sm2KeyPairObj == null) {
|
||||||
|
Map<String, String> sm2KeyPair = SM2Utils.generateKeyPair();
|
||||||
|
redisTemplate.opsForValue().set(sm2KeyPairRedisKey, sm2KeyPair);
|
||||||
|
System.out.println("SM2密钥对已生成并存储到Redis");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取SM2公钥
|
||||||
|
*/
|
||||||
|
@GetMapping("/sm2/public-key")
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
public ApiResponse<Map<String, String>> getSm2PublicKey() {
|
||||||
|
Map<String, String> sm2KeyPair = (Map<String, String>) redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
|
||||||
|
if (sm2KeyPair == null) {
|
||||||
|
throw new RuntimeException("SM2密钥对未初始化");
|
||||||
|
}
|
||||||
|
|
||||||
|
Map<String, String> result = new HashMap<>();
|
||||||
|
result.put("publicKey", sm2KeyPair.get("publicKey"));
|
||||||
|
return ApiResponse.ok(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.domain;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.io.Serial;
|
||||||
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public class ApiResponse<T> implements Serializable {
|
||||||
|
@Serial
|
||||||
|
private static final long serialVersionUID = -7318963194081396360L;
|
||||||
|
|
||||||
|
private int code;
|
||||||
|
private String message;
|
||||||
|
private T data;
|
||||||
|
|
||||||
|
public ApiResponse() {}
|
||||||
|
|
||||||
|
public ApiResponse(int code, String message, T data) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
this.data = data;
|
||||||
|
}
|
||||||
|
|
||||||
|
public ApiResponse(int code, String message) {
|
||||||
|
this.code = code;
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应 - 无数据
|
||||||
|
public static ApiResponse<Void> ok() {
|
||||||
|
return new ApiResponse<>(200, "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应 - 带数据
|
||||||
|
public static <T> ApiResponse<T> ok(T data) {
|
||||||
|
return new ApiResponse<>(200, "success", data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 成功响应 - 自定义消息和数据
|
||||||
|
public static <T> ApiResponse<T> ok(String message, T data) {
|
||||||
|
return new ApiResponse<>(200, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应 - 默认错误
|
||||||
|
public static ApiResponse<Void> error() {
|
||||||
|
return new ApiResponse<>(500, "error");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应 - 自定义错误消息
|
||||||
|
public static ApiResponse<Void> error(String message) {
|
||||||
|
return new ApiResponse<>(500, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误响应 - 自定义状态码和错误消息
|
||||||
|
public static ApiResponse<Void> error(Integer code, String message) {
|
||||||
|
return new ApiResponse<>(code, message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误相应,自定义所有信息
|
||||||
|
public static <T> ApiResponse<T> error(Integer code, String message, T data) {
|
||||||
|
return new ApiResponse<>(code, message, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.filter;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.TypeReference;
|
||||||
|
import com.gis.basic_template_not_login_back.config.CryptoProperties;
|
||||||
|
import com.gis.basic_template_not_login_back.utils.safety.SM2Utils;
|
||||||
|
import com.gis.basic_template_not_login_back.utils.safety.SM4Utils;
|
||||||
|
import com.gis.basic_template_not_login_back.wrapper.Sm4KeyHolder;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import jakarta.servlet.*;
|
||||||
|
import jakarta.servlet.annotation.WebFilter;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import lombok.Setter;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.core.annotation.Order;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
|
import java.io.*;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 请求解密过滤器
|
||||||
|
*/
|
||||||
|
@WebFilter(urlPatterns = "/*")
|
||||||
|
@Order(1)
|
||||||
|
@Component
|
||||||
|
public class DecryptFilter implements Filter {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Value("${safety.sm2.global}")
|
||||||
|
private String sm2KeyPairRedisKey;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CryptoProperties cryptoProperties;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
|
||||||
|
throws IOException, ServletException {
|
||||||
|
HttpServletRequest request = (HttpServletRequest) servletRequest;
|
||||||
|
String requestUri = request.getRequestURI();
|
||||||
|
|
||||||
|
// 检查是否为无需解密的接口
|
||||||
|
if (isNoDecryptPath(requestUri)) {
|
||||||
|
chain.doFilter(request, servletResponse);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 提取加密参数
|
||||||
|
String[] encryptedParams = extractEncryptedParams(request);
|
||||||
|
String encryptedData = encryptedParams[0];
|
||||||
|
String sm4KeyEncrypted = encryptedParams[1];
|
||||||
|
|
||||||
|
// 校验加密参数(仅校验sm4KeyEncrypted,确保必传)
|
||||||
|
if (!StringUtils.hasText(sm4KeyEncrypted)) {
|
||||||
|
throw new IllegalArgumentException("加密参数缺失(sm4KeyEncrypted)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解密SM4密钥
|
||||||
|
String sm2PrivateKey = getSm2PrivateKey();
|
||||||
|
String sm4Key = SM2Utils.decrypt(sm2PrivateKey, sm4KeyEncrypted);
|
||||||
|
|
||||||
|
// 存储SM4密钥
|
||||||
|
Sm4KeyHolder.setSm4Key(sm4Key);
|
||||||
|
|
||||||
|
// 仅当encryptedData有实际内容时才解密(排除null和空字符串)
|
||||||
|
String decryptedData = null;
|
||||||
|
if (StringUtils.hasText(encryptedData)) {
|
||||||
|
decryptedData = SM4Utils.decrypt(sm4Key, encryptedData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 包装解密后的请求(兼容空数据场景)
|
||||||
|
DecryptRequestWrapper wrappedRequest = wrapDecryptedRequest(request, decryptedData);
|
||||||
|
chain.doFilter(wrappedRequest, servletResponse);
|
||||||
|
|
||||||
|
} catch (Exception e) {
|
||||||
|
handleDecryptError(servletResponse, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查是否为无需解密的路径
|
||||||
|
*/
|
||||||
|
private boolean isNoDecryptPath(String requestUri) {
|
||||||
|
for (String path : cryptoProperties.getNoDecryptPaths()) {
|
||||||
|
if (requestUri.contains(path)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 提取加密参数
|
||||||
|
*/
|
||||||
|
private String[] extractEncryptedParams(HttpServletRequest request) throws IOException {
|
||||||
|
String encryptedData = null;
|
||||||
|
String sm4KeyEncrypted = null;
|
||||||
|
String method = request.getMethod();
|
||||||
|
String contentType = request.getContentType();
|
||||||
|
|
||||||
|
if ("GET".equalsIgnoreCase(method)) {
|
||||||
|
// GET请求从Query参数提取(允许参数为空字符串)
|
||||||
|
encryptedData = request.getParameter("encryptedData");
|
||||||
|
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
|
||||||
|
} else {
|
||||||
|
// POST/PUT请求从Body提取
|
||||||
|
if (contentType == null) {
|
||||||
|
throw new IllegalArgumentException("请求Content-Type不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentType.contains("application/json")) {
|
||||||
|
String jsonBody = readRequestBody(request);
|
||||||
|
// 空JSON体处理(避免解析空字符串报错)
|
||||||
|
if (StringUtils.hasText(jsonBody)) {
|
||||||
|
Map<String, Object> bodyMap = JSON.parseObject(jsonBody, new TypeReference<Map<String, Object>>() {});
|
||||||
|
encryptedData = (String) bodyMap.get("encryptedData");
|
||||||
|
sm4KeyEncrypted = (String) bodyMap.get("sm4KeyEncrypted");
|
||||||
|
}
|
||||||
|
} else if (contentType.contains("multipart/form-data")) {
|
||||||
|
// FormData从表单参数提取
|
||||||
|
encryptedData = request.getParameter("encryptedData");
|
||||||
|
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
|
||||||
|
} else if (contentType.contains("application/x-www-form-urlencoded")) {
|
||||||
|
encryptedData = request.getParameter("encryptedData");
|
||||||
|
sm4KeyEncrypted = request.getParameter("sm4KeyEncrypted");
|
||||||
|
} else {
|
||||||
|
throw new IllegalArgumentException("不支持的Content-Type: " + contentType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new String[]{encryptedData, sm4KeyEncrypted};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取SM2私钥
|
||||||
|
*/
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String getSm2PrivateKey() {
|
||||||
|
Map<String, String> sm2KeyPair = (Map<String, String>) redisTemplate.opsForValue().get(sm2KeyPairRedisKey);
|
||||||
|
if (sm2KeyPair == null || !sm2KeyPair.containsKey("privateKey")) {
|
||||||
|
throw new RuntimeException("Redis中未找到SM2私钥,请先初始化密钥对");
|
||||||
|
}
|
||||||
|
return sm2KeyPair.get("privateKey");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 包装解密后的请求(兼容空数据)
|
||||||
|
*/
|
||||||
|
private DecryptRequestWrapper wrapDecryptedRequest(HttpServletRequest request, String decryptedData) {
|
||||||
|
if ("GET".equalsIgnoreCase(request.getMethod())) {
|
||||||
|
// GET请求:解析为参数Map(空数据返回空Map)
|
||||||
|
Map<String, String[]> originalParams = parseGetParams(decryptedData);
|
||||||
|
DecryptRequestWrapper wrapper = new DecryptRequestWrapper(request, new byte[0]);
|
||||||
|
wrapper.setDecryptedParams(originalParams);
|
||||||
|
return wrapper;
|
||||||
|
} else {
|
||||||
|
// POST请求:空数据转换为空字节数组(避免null)
|
||||||
|
byte[] decryptedBody = StringUtils.hasText(decryptedData)
|
||||||
|
? decryptedData.getBytes(StandardCharsets.UTF_8)
|
||||||
|
: new byte[0];
|
||||||
|
return new DecryptRequestWrapper(request, decryptedBody);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析GET参数(兼容空数据)
|
||||||
|
*/
|
||||||
|
private Map<String, String[]> parseGetParams(String decryptedData) {
|
||||||
|
// 空数据直接返回空Map,避免JSON解析报错
|
||||||
|
if (!StringUtils.hasText(decryptedData)) {
|
||||||
|
return new HashMap<>();
|
||||||
|
}
|
||||||
|
Map<String, Object> paramMap = JSON.parseObject(decryptedData);
|
||||||
|
Map<String, String[]> result = new HashMap<>(paramMap.size());
|
||||||
|
|
||||||
|
for (Map.Entry<String, Object> entry : paramMap.entrySet()) {
|
||||||
|
String key = entry.getKey();
|
||||||
|
Object value = entry.getValue();
|
||||||
|
|
||||||
|
if (value == null) {
|
||||||
|
result.put(key, new String[0]);
|
||||||
|
} else if (value instanceof String) {
|
||||||
|
result.put(key, new String[]{(String) value});
|
||||||
|
} else {
|
||||||
|
result.put(key, new String[]{value.toString()});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 读取请求体
|
||||||
|
*/
|
||||||
|
private String readRequestBody(HttpServletRequest request) throws IOException {
|
||||||
|
try (ServletInputStream is = request.getInputStream();
|
||||||
|
BufferedReader reader = new BufferedReader(new InputStreamReader(is, StandardCharsets.UTF_8))) {
|
||||||
|
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
String line;
|
||||||
|
while ((line = reader.readLine()) != null) {
|
||||||
|
sb.append(line);
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理解密错误
|
||||||
|
*/
|
||||||
|
private void handleDecryptError(ServletResponse servletResponse, Exception e) throws IOException {
|
||||||
|
servletResponse.setContentType("application/json;charset=UTF-8");
|
||||||
|
servletResponse.setCharacterEncoding("UTF-8");
|
||||||
|
|
||||||
|
Map<String, Object> errorResult = new HashMap<>();
|
||||||
|
errorResult.put("code", 500);
|
||||||
|
errorResult.put("msg", "请求解密失败: " + e.getMessage());
|
||||||
|
errorResult.put("success", false);
|
||||||
|
|
||||||
|
PrintWriter writer = servletResponse.getWriter();
|
||||||
|
writer.write(JSON.toJSONString(errorResult));
|
||||||
|
writer.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义请求包装类
|
||||||
|
*/
|
||||||
|
public static class DecryptRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {
|
||||||
|
private final byte[] decryptedBody;
|
||||||
|
@Setter
|
||||||
|
private Map<String, String[]> decryptedParams;
|
||||||
|
|
||||||
|
public DecryptRequestWrapper(HttpServletRequest request, byte[] decryptedBody) {
|
||||||
|
super(request);
|
||||||
|
this.decryptedBody = decryptedBody;
|
||||||
|
this.decryptedParams = new HashMap<>(request.getParameterMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public ServletInputStream getInputStream() {
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(decryptedBody);
|
||||||
|
return new ServletInputStream() {
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return bis.available() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(ReadListener listener) {
|
||||||
|
// 无需实现
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() {
|
||||||
|
return bis.read();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public BufferedReader getReader() {
|
||||||
|
return new BufferedReader(new InputStreamReader(getInputStream(), StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String getParameter(String name) {
|
||||||
|
String[] values = decryptedParams.get(name);
|
||||||
|
return values != null && values.length > 0 ? values[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String[]> getParameterMap() {
|
||||||
|
return decryptedParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String[] getParameterValues(String name) {
|
||||||
|
return decryptedParams.get(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.service.ex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 业务层所有自定义异常父类
|
||||||
|
*/
|
||||||
|
public class ServiceException extends RuntimeException {
|
||||||
|
public ServiceException() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceException(String message) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceException(String message, Throwable cause) {
|
||||||
|
super(message, cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceException(Throwable cause) {
|
||||||
|
super(cause);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ServiceException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
|
||||||
|
super(message, cause, enableSuppression, writableStackTrace);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.utils.safety;
|
||||||
|
|
||||||
|
import org.bouncycastle.asn1.gm.GMNamedCurves;
|
||||||
|
import org.bouncycastle.asn1.x9.X9ECParameters;
|
||||||
|
import org.bouncycastle.crypto.AsymmetricCipherKeyPair;
|
||||||
|
import org.bouncycastle.crypto.generators.ECKeyPairGenerator;
|
||||||
|
import org.bouncycastle.crypto.params.*;
|
||||||
|
import org.bouncycastle.jce.provider.BouncyCastleProvider;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
|
||||||
|
import java.math.BigInteger;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.Security;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM2工具类(与前端gm-crypto兼容,加解密使用Hex编码)
|
||||||
|
*/
|
||||||
|
public class SM2Utils {
|
||||||
|
|
||||||
|
static {
|
||||||
|
if (Security.getProvider(BouncyCastleProvider.PROVIDER_NAME) == null) {
|
||||||
|
Security.addProvider(new BouncyCastleProvider());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static final X9ECParameters x9ECParameters = GMNamedCurves.getByName("sm2p256v1");
|
||||||
|
private static final ECDomainParameters ecDomainParameters = new ECDomainParameters(x9ECParameters.getCurve(), x9ECParameters.getG(), x9ECParameters.getN());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成SM2密钥对
|
||||||
|
*/
|
||||||
|
public static Map<String, String> generateKeyPair() {
|
||||||
|
try {
|
||||||
|
ECKeyPairGenerator generator = new ECKeyPairGenerator();
|
||||||
|
ECKeyGenerationParameters genParams = new ECKeyGenerationParameters(ecDomainParameters, new SecureRandom());
|
||||||
|
generator.init(genParams);
|
||||||
|
AsymmetricCipherKeyPair keyPair = generator.generateKeyPair();
|
||||||
|
|
||||||
|
// 公钥:非压缩格式(64字节,包含0x04前缀共65字节)
|
||||||
|
ECPublicKeyParameters publicKey = (ECPublicKeyParameters) keyPair.getPublic();
|
||||||
|
String publicKeyHex = Hex.toHexString(publicKey.getQ().getEncoded(false));
|
||||||
|
|
||||||
|
// 私钥:32字节Hex字符串
|
||||||
|
ECPrivateKeyParameters privateKey = (ECPrivateKeyParameters) keyPair.getPrivate();
|
||||||
|
String privateKeyHex = privateKey.getD().toString(16);
|
||||||
|
// 补零到64位
|
||||||
|
privateKeyHex = String.format("%64s", privateKeyHex).replace(' ', '0');
|
||||||
|
|
||||||
|
Map<String, String> keyMap = new HashMap<>(2);
|
||||||
|
keyMap.put("publicKey", publicKeyHex);
|
||||||
|
keyMap.put("privateKey", privateKeyHex);
|
||||||
|
return keyMap;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("生成SM2密钥对失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM2公钥加密(返回Hex字符串)
|
||||||
|
*/
|
||||||
|
public static String encrypt(String publicKeyHex, String plaintext) {
|
||||||
|
try {
|
||||||
|
// 解析带0x04前缀的公钥(65字节)
|
||||||
|
byte[] publicKeyBytes = Hex.decode(publicKeyHex);
|
||||||
|
|
||||||
|
ECPublicKeyParameters publicKey = new ECPublicKeyParameters(x9ECParameters.getCurve().decodePoint(publicKeyBytes), ecDomainParameters);
|
||||||
|
|
||||||
|
// 初始化加密器(C1C3C2模式)
|
||||||
|
org.bouncycastle.crypto.engines.SM2Engine engine = new org.bouncycastle.crypto.engines.SM2Engine((org.bouncycastle.crypto.engines.SM2Engine.Mode.C1C3C2));
|
||||||
|
engine.init(true, new ParametersWithRandom(publicKey, new SecureRandom()));
|
||||||
|
|
||||||
|
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] encryptedBytes = engine.processBlock(plaintextBytes, 0, plaintextBytes.length);
|
||||||
|
// 加密结果转为Hex字符串
|
||||||
|
return Hex.toHexString(encryptedBytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("SM2加密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM2私钥解密(接收Hex字符串密文)
|
||||||
|
*/
|
||||||
|
public static String decrypt(String privateKeyHex, String ciphertextHex) {
|
||||||
|
try {
|
||||||
|
// 解析私钥
|
||||||
|
byte[] privateKeyBytes = Hex.decode(privateKeyHex);
|
||||||
|
ECPrivateKeyParameters privateKey = new ECPrivateKeyParameters(new BigInteger(1, privateKeyBytes), ecDomainParameters);
|
||||||
|
|
||||||
|
// 解码Hex格式密文
|
||||||
|
byte[] ciphertextBytes = Hex.decode(ciphertextHex);
|
||||||
|
|
||||||
|
// 初始化解密器
|
||||||
|
org.bouncycastle.crypto.engines.SM2Engine engine = new org.bouncycastle.crypto.engines.SM2Engine(org.bouncycastle.crypto.engines.SM2Engine.Mode.C1C3C2);
|
||||||
|
engine.init(false, privateKey);
|
||||||
|
|
||||||
|
byte[] decryptedBytes = engine.processBlock(ciphertextBytes, 0, ciphertextBytes.length);
|
||||||
|
return new String(decryptedBytes, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("SM2解密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.utils.safety;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.digests.SM3Digest;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM3算法工具类(纯算法实现,不依赖业务逻辑)
|
||||||
|
* 仅负责:计算字节数组的SM3哈希值、字节数组与十六进制转换
|
||||||
|
*/
|
||||||
|
public class SM3Utils {
|
||||||
|
/**
|
||||||
|
* 计算字节数组的SM3哈希值
|
||||||
|
* @param input 输入字节数组(可任意数据,如盐值+密码的组合)
|
||||||
|
* @return SM3哈希值(32字节)
|
||||||
|
*/
|
||||||
|
public static byte[] hash(byte[] input) {
|
||||||
|
if (input == null || input.length == 0) {
|
||||||
|
throw new IllegalArgumentException("输入字节数组不能为空");
|
||||||
|
}
|
||||||
|
SM3Digest digest = new SM3Digest();
|
||||||
|
digest.update(input, 0, input.length);
|
||||||
|
byte[] hashBytes = new byte[digest.getDigestSize()];
|
||||||
|
digest.doFinal(hashBytes, 0);
|
||||||
|
return hashBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 字节数组转16进制字符串(用于哈希值可视化)
|
||||||
|
*/
|
||||||
|
public static String toHex(byte[] bytes) {
|
||||||
|
if (bytes == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Hex.toHexString(bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 16进制字符串转字节数组(反向操作)
|
||||||
|
*/
|
||||||
|
public static byte[] fromHex(String hexStr) {
|
||||||
|
if (hexStr == null || hexStr.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Hex.decode(hexStr);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.utils.safety;
|
||||||
|
|
||||||
|
import org.bouncycastle.crypto.CipherParameters;
|
||||||
|
import org.bouncycastle.crypto.engines.SM4Engine;
|
||||||
|
import org.bouncycastle.crypto.paddings.PKCS7Padding;
|
||||||
|
import org.bouncycastle.crypto.paddings.PaddedBufferedBlockCipher;
|
||||||
|
import org.bouncycastle.crypto.params.KeyParameter;
|
||||||
|
import org.bouncycastle.util.encoders.Hex;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM4工具类(ECB模式,与前端兼容,加解密使用Hex编码)
|
||||||
|
*/
|
||||||
|
public class SM4Utils {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM4加密(ECB模式,返回Hex字符串)
|
||||||
|
*/
|
||||||
|
public static String encrypt(String keyHex, String plaintext) {
|
||||||
|
try {
|
||||||
|
// 解析密钥(16字节=32位Hex字符串)
|
||||||
|
byte[] key = Hex.decode(keyHex);
|
||||||
|
if (key.length != 16) {
|
||||||
|
throw new IllegalArgumentException("SM4密钥必须是16字节(32位Hex字符串)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 初始化加密器(ECB模式 + PKCS7填充)
|
||||||
|
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
|
||||||
|
new SM4Engine(),
|
||||||
|
new PKCS7Padding()
|
||||||
|
);
|
||||||
|
CipherParameters params = new KeyParameter(key);
|
||||||
|
cipher.init(true, params);
|
||||||
|
|
||||||
|
// 处理明文
|
||||||
|
byte[] plaintextBytes = plaintext.getBytes(StandardCharsets.UTF_8);
|
||||||
|
byte[] output = new byte[cipher.getOutputSize(plaintextBytes.length)];
|
||||||
|
int length = cipher.processBytes(plaintextBytes, 0, plaintextBytes.length, output, 0);
|
||||||
|
length += cipher.doFinal(output, length);
|
||||||
|
|
||||||
|
// 加密结果转为Hex字符串
|
||||||
|
return Hex.toHexString(output, 0, length);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("SM4加密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM4解密(ECB模式,接收Hex字符串密文)
|
||||||
|
*/
|
||||||
|
public static String decrypt(String keyHex, String ciphertextHex) {
|
||||||
|
try {
|
||||||
|
// 解析密钥(16字节=32位Hex字符串)
|
||||||
|
byte[] key = Hex.decode(keyHex);
|
||||||
|
if (key.length != 16) {
|
||||||
|
throw new IllegalArgumentException("SM4密钥必须是16字节(32位Hex字符串)");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 解码Hex格式密文
|
||||||
|
byte[] ciphertextBytes = Hex.decode(ciphertextHex);
|
||||||
|
|
||||||
|
// 初始化解密器
|
||||||
|
PaddedBufferedBlockCipher cipher = new PaddedBufferedBlockCipher(
|
||||||
|
new SM4Engine(),
|
||||||
|
new PKCS7Padding()
|
||||||
|
);
|
||||||
|
CipherParameters params = new KeyParameter(key);
|
||||||
|
cipher.init(false, params);
|
||||||
|
|
||||||
|
// 处理密文
|
||||||
|
byte[] output = new byte[cipher.getOutputSize(ciphertextBytes.length)];
|
||||||
|
int length = cipher.processBytes(ciphertextBytes, 0, ciphertextBytes.length, output, 0);
|
||||||
|
length += cipher.doFinal(output, length);
|
||||||
|
|
||||||
|
// 解密结果转为字符串
|
||||||
|
return new String(output, 0, length, StandardCharsets.UTF_8);
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("SM4解密失败: " + e.getMessage(), e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+68
@@ -0,0 +1,68 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.wrapper;
|
||||||
|
|
||||||
|
import jakarta.servlet.ReadListener;
|
||||||
|
import jakarta.servlet.ServletInputStream;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解密拦截器
|
||||||
|
*/
|
||||||
|
public class DecryptRequestWrapper extends HttpServletRequestWrapper {
|
||||||
|
// 缓存解密后的请求体(用于POST等带Body的请求)
|
||||||
|
private final byte[] decryptedBody;
|
||||||
|
// 用于更新解密后的参数(GET场景)
|
||||||
|
// 缓存解密后的参数(用于GET等带Query参数的请求)
|
||||||
|
@Setter
|
||||||
|
private Map<String, String[]> decryptedParams;
|
||||||
|
|
||||||
|
public DecryptRequestWrapper(HttpServletRequest request, byte[] decryptedBody) {
|
||||||
|
super(request);
|
||||||
|
this.decryptedBody = decryptedBody;
|
||||||
|
this.decryptedParams = new HashMap<>(request.getParameterMap());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重写输入流,返回解密后的Body
|
||||||
|
@Override
|
||||||
|
public ServletInputStream getInputStream() throws IOException {
|
||||||
|
ByteArrayInputStream bis = new ByteArrayInputStream(decryptedBody);
|
||||||
|
return new ServletInputStream() {
|
||||||
|
@Override
|
||||||
|
public boolean isFinished() {
|
||||||
|
return bis.available() == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isReady() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setReadListener(ReadListener listener) {}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int read() throws IOException {
|
||||||
|
return bis.read();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 重写参数获取方法,返回解密后的参数(用于GET)
|
||||||
|
@Override
|
||||||
|
public String getParameter(String name) {
|
||||||
|
String[] values = decryptedParams.get(name);
|
||||||
|
return values != null && values.length > 0 ? values[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, String[]> getParameterMap() {
|
||||||
|
return decryptedParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+77
@@ -0,0 +1,77 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.wrapper;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.gis.basic_template_not_login_back.config.CryptoProperties;
|
||||||
|
import com.gis.basic_template_not_login_back.utils.safety.SM4Utils;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import org.springframework.core.MethodParameter;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.converter.HttpMessageConverter;
|
||||||
|
import org.springframework.http.server.ServerHttpRequest;
|
||||||
|
import org.springframework.http.server.ServerHttpResponse;
|
||||||
|
import org.springframework.web.bind.annotation.ControllerAdvice;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||||
|
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||||
|
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应数据加密拦截器
|
||||||
|
*/
|
||||||
|
@ControllerAdvice
|
||||||
|
public class EncryptResponseAdvice implements ResponseBodyAdvice<Object> {
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private ObjectMapper objectMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private CryptoProperties cryptoProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断是否需要加密:排除特定路径,其余全部加密
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
|
||||||
|
// 获取当前请求的URI
|
||||||
|
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||||
|
if (attributes == null) {
|
||||||
|
return false; // 非Web请求场景,不加密
|
||||||
|
}
|
||||||
|
HttpServletRequest request = attributes.getRequest();
|
||||||
|
String requestUri = request.getRequestURI();
|
||||||
|
|
||||||
|
// 检查是否为无需加密的路径
|
||||||
|
for (String path : cryptoProperties.getNoEncryptPaths()) {
|
||||||
|
if (requestUri.contains(path)) {
|
||||||
|
return false; // 排除路径,不加密
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 其余路径均需要加密
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 响应体加密逻辑(保持不变)
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
|
||||||
|
Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||||
|
ServerHttpRequest request, ServerHttpResponse response) {
|
||||||
|
try {
|
||||||
|
String sm4Key = Sm4KeyHolder.getSm4Key();
|
||||||
|
if (sm4Key == null || sm4Key.length() != 32) {
|
||||||
|
throw new RuntimeException("SM4密钥不存在或格式错误,无法加密响应");
|
||||||
|
}
|
||||||
|
|
||||||
|
String plaintext = objectMapper.writeValueAsString(body);
|
||||||
|
String encryptedText = SM4Utils.encrypt(sm4Key, plaintext);
|
||||||
|
return encryptedText;
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("响应数据加密失败: " + e.getMessage(), e);
|
||||||
|
} finally {
|
||||||
|
Sm4KeyHolder.clear(); // 清除线程本地存储,避免内存泄漏
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package com.gis.basic_template_not_login_back.wrapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SM4密钥存储
|
||||||
|
*/
|
||||||
|
public class Sm4KeyHolder {
|
||||||
|
// 线程本地存储SM4密钥(Hex格式,32位字符串)
|
||||||
|
private static final ThreadLocal<String> SM4_KEY_HOLDER = new ThreadLocal<>();
|
||||||
|
|
||||||
|
// 设置当前线程的SM4密钥
|
||||||
|
public static void setSm4Key(String sm4Key) {
|
||||||
|
SM4_KEY_HOLDER.set(sm4Key);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取当前线程的SM4密钥
|
||||||
|
public static String getSm4Key() {
|
||||||
|
return SM4_KEY_HOLDER.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除线程本地存储(避免内存泄漏)
|
||||||
|
public static void clear() {
|
||||||
|
SM4_KEY_HOLDER.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
# Druid 公共配置
|
||||||
|
druid:
|
||||||
|
initial-size: 5
|
||||||
|
min-idle: 5
|
||||||
|
max-active: 20
|
||||||
|
max-wait: 60000
|
||||||
|
time-between-eviction-runs-millis: 60000
|
||||||
|
min-evictable-idle-time-millis: 300000
|
||||||
|
validation-query: SELECT 1
|
||||||
|
test-while-idle: true
|
||||||
|
test-on-borrow: false
|
||||||
|
test-on-return: false
|
||||||
|
pool-prepared-statements: true
|
||||||
|
max-pool-prepared-statement-per-connection-size: 20
|
||||||
|
filters: stat,wall
|
||||||
|
web-stat-filter:
|
||||||
|
enabled: true
|
||||||
|
stat-view-servlet:
|
||||||
|
enabled: true
|
||||||
|
url-pattern: /druid/*
|
||||||
|
login-username: admin
|
||||||
|
login-password: admin
|
||||||
|
|
||||||
|
# 主库
|
||||||
|
master:
|
||||||
|
url: jdbc:postgresql://47.92.216.173:7654/xian?characterEncoding=utf8&TimeZone=Asia/Shanghai
|
||||||
|
username: postgres
|
||||||
|
password: zhangsan
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
# 从库
|
||||||
|
slave:
|
||||||
|
url: jdbc:postgresql://47.92.216.173:7654/assess_disaster?characterEncoding=utf8&TimeZone=Asia/Shanghai
|
||||||
|
username: postgres
|
||||||
|
password: zhangsan
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
spring:
|
||||||
|
datasource:
|
||||||
|
# Druid 公共配置
|
||||||
|
druid:
|
||||||
|
initial-size: 10
|
||||||
|
min-idle: 10
|
||||||
|
max-active: 50
|
||||||
|
max-wait: 60000
|
||||||
|
time-between-eviction-runs-millis: 60000
|
||||||
|
min-evictable-idle-time-millis: 300000
|
||||||
|
validation-query: SELECT 1
|
||||||
|
test-while-idle: true
|
||||||
|
test-on-borrow: false
|
||||||
|
test-on-return: false
|
||||||
|
pool-prepared-statements: true
|
||||||
|
max-pool-prepared-statement-per-connection-size: 20
|
||||||
|
filters: stat,wall
|
||||||
|
web-stat-filter:
|
||||||
|
enabled: true
|
||||||
|
stat-view-servlet:
|
||||||
|
enabled: false
|
||||||
|
|
||||||
|
# 主库
|
||||||
|
master:
|
||||||
|
url: jdbc:postgresql://10.22.245.138:54321/xianDC?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
|
||||||
|
username: zaihailian
|
||||||
|
password: XAYJ@gis2603
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
|
|
||||||
|
# 从库
|
||||||
|
slave:
|
||||||
|
url: jdbc:postgresql://10.22.245.138:54321/xianDCAccess?useUnicode=true&characterEncoding=UTF-8&allowMultiQueries=true&serverTimezone=Asia/Shanghai
|
||||||
|
username: zaihailian
|
||||||
|
password: XAYJ@gis2603
|
||||||
|
driver-class-name: org.postgresql.Driver
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
# 开发环境配置
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
import: classpath:application-database-dev.yml
|
||||||
|
# redis
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 47.92.216.173
|
||||||
|
port: 7655
|
||||||
|
password: zhangsan
|
||||||
|
database: 0
|
||||||
|
connect-timeout: 3000ms
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: INFO
|
||||||
|
pattern:
|
||||||
|
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
|
||||||
|
file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
|
||||||
|
file:
|
||||||
|
name: ./logs/springboot-app.log
|
||||||
|
logback:
|
||||||
|
rollingpolicy:
|
||||||
|
file-name-pattern: ./logs/springboot-app-%d{yyyy-MM-dd}.%i.log
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-history: 7
|
||||||
|
total-size-cap: 1GB
|
||||||
|
clean-history-on-start: true
|
||||||
|
|
||||||
|
# 安全配置
|
||||||
|
safety:
|
||||||
|
# 加解密过滤路径配置
|
||||||
|
crypto:
|
||||||
|
# 响应无需加密的路径
|
||||||
|
no-encrypt-paths:
|
||||||
|
- /crypto/sm2/public-key
|
||||||
|
- /druid
|
||||||
|
# 请求无需解密的路径
|
||||||
|
no-decrypt-paths:
|
||||||
|
- /crypto/sm2/public-key
|
||||||
|
- /druid
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
# 生产环境配置
|
||||||
|
spring:
|
||||||
|
config:
|
||||||
|
import: classpath:application-database-prod.yml
|
||||||
|
# redis
|
||||||
|
data:
|
||||||
|
redis:
|
||||||
|
host: 10.22.245.246
|
||||||
|
port: 6379
|
||||||
|
password: XAYJ@gis2603
|
||||||
|
database: 0
|
||||||
|
connect-timeout: 3000ms
|
||||||
|
|
||||||
|
# 日志配置
|
||||||
|
logging:
|
||||||
|
level:
|
||||||
|
root: WARN
|
||||||
|
pattern:
|
||||||
|
console: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
|
||||||
|
file: '%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{50} - %msg%n'
|
||||||
|
file:
|
||||||
|
name: ./logs/springboot-app.log
|
||||||
|
logback:
|
||||||
|
rollingpolicy:
|
||||||
|
file-name-pattern: ./logs/springboot-app-%d{yyyy-MM-dd}.%i.log
|
||||||
|
max-file-size: 100MB
|
||||||
|
max-history: 7
|
||||||
|
total-size-cap: 1GB
|
||||||
|
clean-history-on-start: true
|
||||||
|
|
||||||
|
|
||||||
|
# 安全配置
|
||||||
|
safety:
|
||||||
|
# 加解密过滤路径配置
|
||||||
|
crypto:
|
||||||
|
# 响应无需加密的路径
|
||||||
|
no-encrypt-paths:
|
||||||
|
- /crypto/sm2/public-key
|
||||||
|
# 请求无需解密的路径
|
||||||
|
no-decrypt-paths:
|
||||||
|
- /crypto/sm2/public-key
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
# 端口
|
||||||
|
server:
|
||||||
|
port: 8080
|
||||||
|
|
||||||
|
# 激活的配置文件(默认dev,打包时会被Maven profile替换)
|
||||||
|
spring:
|
||||||
|
profiles:
|
||||||
|
active: @spring.profiles.active@
|
||||||
|
|
||||||
|
# MyBatis 配置
|
||||||
|
mybatis:
|
||||||
|
mapper-locations: classpath:mapper/*.xml
|
||||||
|
type-aliases-package: com.gis.basic_template_not_login_back.entity
|
||||||
|
configuration:
|
||||||
|
map-underscore-to-camel-case: true
|
||||||
|
|
||||||
|
# 安全配置
|
||||||
|
safety:
|
||||||
|
sm2:
|
||||||
|
# sm2公钥在redis存储名
|
||||||
|
global: 'sm2:keypair:global'
|
||||||
Reference in New Issue
Block a user