后端基础框架

This commit is contained in:
2026-02-25 09:56:35 +08:00
commit b964b54fc6
146 changed files with 6919 additions and 0 deletions
+105
View File
@@ -0,0 +1,105 @@
sys:
# Your App secret key will be used for securely signing the session cookie
# Make sure you are changing this key for your deployment with a strong key.
# You can generate a strong key using `openssl rand -base64 42`.
# Alternatively you can set it with `SECRET_KEY` environment variable.
secret-key: WrLAa6u2LxhO+rQYQM7id01/FJPZ2GxH3pY/A2IFO6mOA0uaKzUIYWOC
# Console API base URL
console-api-url: http://localhost:8181
console-web-url: http://localhost:5173
# Model Configuration
multimodal-send-image-format: base64
# Files URL
files-url: http://127.0.0.1:5123
# Storage configuration
# use for store upload files, private keys...
# storage type: local, aliyun-oss, tencent-cos
storage-type: local
storage-local-path: D:/dev/ai-writing/storage/
# Aliyun oss Storage configuration
aliyun-oss-bucket-name: your-bucket-name
aliyun-oss-access-key: your-access-key
aliyun-oss-secret-key: your-secret-key
aliyun-oss-endpoint: your-endpoint
aliyun-oss-auth-version: v1
aliyun-oss-region: your-region
# Invite Configuration
invite-expiry-hours: 24
server:
# 应用服务 WEB 访问端口
port: 8282
spring:
### 处理连接池冲突 #####
main:
allow-bean-definition-overriding: true
# 数据源
datasource:
# url: jdbc:kingbase8://172.26.138.234:54321/ai-writing?currentSchema=public
# username: system
# password: 123456
# driver-class-name: com.kingbase8.Driver
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://223.83.129.121:33060/oai-writting?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: oai-write
password: rpriBbbPRzBNFDYS
# url: jdbc:mysql://127.0.0.1:3306/ai-writting?rewriteBatchedStatements=true&useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
# username: root
# password: zhangjie
type: com.zaxxer.hikari.HikariDataSource
# 数据库 连接池
hikari:
auto-commit: true
connection-test-query: SELECT 1
validation-timeout: 1000
connection-timeout: 60000
idle-timeout: 600000
max-lifetime: 1200000
maximum-pool-size: 15
minimum-idle: 10
pool-name: HikariCP
data:
redis:
port: 6379
host: localhost
password:
database: 0
lettuce:
pool:
# 连接池最大连接数(使用负值表示没有限制) 默认 8
max-active: 8
# 连接池中的最大空闲连接 默认 8
max-idle: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制) 默认 -1
max-wait: -1
# 连接池中的最小空闲连接 默认 0
min-idle: 0
mail:
# default(默认smtp邮箱服务) , aliyun-mail-push (阿里云邮件推送)
type: aliyun-mail-push
ak: LTAI5t9xS8GFTSxwWJmi8BMk
sk: ntjZii2NFteyh4JshILyDf9xZwc79L
host: smtpdm.aliyun.com
port: 465
username: robot@mail.oailab.com
password: OaiMail13579
protocol: smtp
properties:
mail:
smtp:
auth: true
ssl:
enable: true
# Login
jwt:
# Token 有效天数
token-validity-days: 15
# Upload configuration
upload:
upload-file-size-limit: 15
upload-file-batch-limit: 5
upload-image-file-size-limit: 10
+38
View File
@@ -0,0 +1,38 @@
spring:
application:
name: ai-base
profiles:
active: local
servlet:
multipart:
enabled: true
max-file-size: 100MB
max-request-size: 100MB
server:
tomcat:
connection-timeout: 300000
servlet:
session:
timeout: 300000
# mybatis-plus 配置
mybatis-plus:
global-config:
db-config:
# 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-field: deleted
# 逻辑已删除值(默认为 1)
logic-delete-value: 1
# 逻辑未删除值(默认为 0)
logic-not-delete-value: 0
configuration:
map-underscore-to-camel-case: true
auto-mapping-behavior: full
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
use-generated-keys: true
mapper-locations: classpath*:mapper/**/*Mapper.xml
type-aliases-package: com.jxyunge.oai.mybatis.entity
+106
View File
@@ -0,0 +1,106 @@
<?xml version='1.0' encoding='UTF-8'?>
<!--日志配置-->
<configuration>
<property name="log.base" value="logs/"/>
<!-- 控制台输出 -->
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
</appender>
<!-- 按照每天生成错误日志文件 -->
<appender name="ERRORFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${log.base}LOG_ERROR-%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>5</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印错误日志 -->
<level>ERROR</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 按照每天生成警告日志文件 -->
<appender name="WARNFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${log.base}LOG_WARN-%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>5</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印警告日志 -->
<level>WARN</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 按照每天生成INFO日志文件 -->
<appender name="INFOFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${log.base}LOG_INFO-%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>5</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印INFO日志 -->
<level>INFO</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 按照每天生成DEBUG日志文件 -->
<appender name="DEBUGFILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
<Prudent>true</Prudent>
<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
<!--日志文件输出的文件名 -->
<FileNamePattern>${log.base}LOG_DEBUG-%d{yyyy-MM-dd}.log</FileNamePattern>
<!--日志文件保留天数 -->
<MaxHistory>5</MaxHistory>
</rollingPolicy>
<encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
<!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符 -->
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
</pattern>
</encoder>
<filter class="ch.qos.logback.classic.filter.LevelFilter"><!-- 只打印DEBUG日志 -->
<level>DEBUG</level>
<onMatch>ACCEPT</onMatch>
<onMismatch>DENY</onMismatch>
</filter>
</appender>
<!-- 日志输出级别 -->
<root level="info">
<appender-ref ref="STDOUT"/>
<appender-ref ref="ERRORFILE"/>
<appender-ref ref="WARNFILE"/>
<appender-ref ref="INFOFILE"/>
<appender-ref ref="DEBUGFILE"/>
</root>
</configuration>
+72
View File
@@ -0,0 +1,72 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>oailab.com</groupId>
<artifactId>ai-base</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<artifactId>api-start</artifactId>
<version>${revision}</version>
<name>api-start</name>
<description>api-start</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>21</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>oailab.com</groupId>
<artifactId>api-service</artifactId>
<version>${revision}</version>
<scope>compile</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>21</source>
<target>21</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
@@ -0,0 +1,26 @@
package oailab.com.apistart;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.PropertySource;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(scanBasePackages = "oailab.com", exclude = {SecurityAutoConfiguration.class})
@EnableScheduling
@EnableCaching
@EnableAsync
@EnableTransactionManagement
@Slf4j
@PropertySource(value = {"file:${user.dir}/config/application.yml"})
public class ApiStartApplication {
public static void main(String[] args) {
SpringApplication.run(ApiStartApplication.class, args);
}
}
@@ -0,0 +1,27 @@
package oailab.com.apistart.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Cross-origin configuration.
*
* @author Benny Deng
* @see <a href="http://www.bianmaren.com">Benny Deng</a>
* @since 6/17/24 PM11:19
* @version 1.0
**/
@Configuration
public class CorsConfig implements WebMvcConfigurer{
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowCredentials(true)
.allowedOriginPatterns("*")
.allowedHeaders("*")
.allowedMethods("*");
}
}
@@ -0,0 +1,63 @@
package oailab.com.apistart.config;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
import oailab.com.apistart.interceptor.SysTokenInterceptor;
import oailab.com.apistart.interceptor.WebTokenInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* Mvc configuration.
*
* @author Benny Deng
* @version 1.0
* @see <a href="http://www.bianmaren.com">Benny Deng</a>
* @since 6/17/24 PM11:19
**/
@Slf4j
@Configuration
public class WebMvcConf implements WebMvcConfigurer {
@Resource
private SysTokenInterceptor sysTokenInterceptor;
@Resource
private WebTokenInterceptor webTokenInterceptor;
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 将非 /api/** 的请求转发到 index.html
registry.addViewController("/{path:[^\\.]*}")
.setViewName("forward:/index.html");
registry.addViewController("/{path1:[^\\.]*}/{path2:[^\\.]*}")
.setViewName("forward:/index.html");
registry.addViewController("/{path1:[^\\.]*}/{path2:[^\\.]*}/{path3:[^\\.]*}")
.setViewName("forward:/index.html");
registry.addViewController("/{path1:[^\\.]*}/{path2:[^\\.]*}/{path3:[^\\.]*}/{path4:[^\\.]*}")
.setViewName("forward:/index.html");
registry.addViewController("/{path1:[^\\.]*}/{path2:[^\\.]*}/{path3:[^\\.]*}/{path4:[^\\.]*}/{path5:[^\\.]*}")
.setViewName("forward:/index.html");
// 根据需要支持更深的路径
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 确保静态资源可以正常访问
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/");
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(sysTokenInterceptor).addPathPatterns("/api/sys/**");
registry.addInterceptor(webTokenInterceptor).addPathPatterns("/api/web/**");
registry.addInterceptor(sysTokenInterceptor).addPathPatterns("/api/payment/**");
}
}
@@ -0,0 +1,127 @@
package oailab.com.apistart.controller;
import cn.hutool.core.util.StrUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import oailab.com.apiservice.constants.LoginConstants;
import oailab.com.apiservice.enums.sys.ErrorCodeEnum;
import oailab.com.apiservice.exception.BizException;
import oailab.com.apiservice.mybatis.entity.sys.TenantAccount;
import oailab.com.apiservice.mybatis.entity.sys.TenantWorkspace;
import oailab.com.apiservice.mybatis.entity.workspace.EndUsers;
import oailab.com.apiservice.mybatis.service.sys.TenantAccountService;
import oailab.com.apiservice.mybatis.service.sys.TenantWorkspaceService;
import oailab.com.apiservice.mybatis.service.workspace.EndUsersService;
/**
* Base controller..
*
* @author Benny Deng
* @version 1.0
* @see <a href="http://www.bianmaren.com">Benny Deng</a>
* @since 6/17/24 PM11:19
**/
public class BaseController {
@Resource
private TenantAccountService tenantAccountService;
@Resource
private TenantWorkspaceService tenantWorkspaceService;
@Resource
private EndUsersService endUsersService;
/**
* 获取当前登录的账号
*
* @param request 请求
* @return 租户账号
*/
protected String getCurrentTenantAccountFromReq(HttpServletRequest request) {
return getAttrFromReq(request, LoginConstants.SYS_LOGIN_USER_ACCOUNT);
}
/**
* 获取当前工作间ID
*
* @param request 请求
*/
protected String getCurrentWorkspaceIdFromReq(HttpServletRequest request) {
return getAttrFromReqHeaders(request, LoginConstants.HEADER_WORKSPACE_ID);
}
/**
* 获取当前工作间
*
* @param request 请求
*/
protected TenantWorkspace getCurrentWorkspaceFromReq(HttpServletRequest request) {
TenantWorkspace workspace = tenantWorkspaceService.getById(getCurrentWorkspaceIdFromReq(request));
if (null == workspace) {
throw new BizException(ErrorCodeEnum.WorkspaceNotFound);
}
return workspace;
}
/**
* 获取当前登录的租户
*
* @param request 请求
* @return 租户
*/
protected TenantAccount getCurrentTenantFromReq(HttpServletRequest request) {
return tenantAccountService.getByAccount(getCurrentTenantAccountFromReq(request));
}
protected String getEndUserIdFromFromReq(HttpServletRequest request) {
return getAttrFromReq(request, LoginConstants.WEB_LOGIN_USER_ACCOUNT);
}
protected EndUsers getEndUserFromFromReq(HttpServletRequest request) {
return endUsersService.getById(getEndUserIdFromFromReq(request));
}
protected String getAppIdFromReq(HttpServletRequest request) {
return getAttrFromReq(request, LoginConstants.API_APP_ID);
}
/**
* 从请求头中获取属性数据
*
* @param request 请求
* @param key 属性key
* @return 属性值
*/
protected String getAttrFromReq(HttpServletRequest request, String key) {
Object userId = request.getAttribute(key);
if (null != userId) {
return userId.toString();
}
return null;
}
/**
* 从请求头中获取信息
*
* @param request 请求
* @param key 属性key
*/
protected String getAttrFromReqHeaders(HttpServletRequest request, String key) {
Object userId = request.getHeader(key);
if (null != userId) {
return userId.toString();
}
return null;
}
protected String getApiTokenFromReq(HttpServletRequest request) {
// 通常从 header 中读取:Authorization: Bearer token
String authHeader = request.getHeader("Authorization");
if (StrUtil.isNotBlank(authHeader) && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
}
@@ -0,0 +1,110 @@
package oailab.com.apistart.interceptor;
import cn.hutool.jwt.JWT;
import cn.hutool.jwt.JWTUtil;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import oailab.com.apiservice.annotation.PassToken;
import oailab.com.apiservice.constants.LoginConstants;
import oailab.com.apiservice.enums.sys.ErrorCodeEnum;
import oailab.com.apiservice.exception.BizException;
import oailab.com.apiservice.mybatis.entity.sys.TenantAccount;
import oailab.com.apiservice.mybatis.service.sys.TenantAccountService;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.lang.reflect.Method;
/**
* Token validation interceptor.
*
* @author Benny Deng
* @version 1.0
* @see <a href="http://www.bianmaren.com">Benny Deng</a>
* @since 6/17/24 PM11:19
**/
@Slf4j
@Component("sysTokenInterceptor")
public class SysTokenInterceptor implements HandlerInterceptor {
@Resource
private TenantAccountService tenantAccountService;
@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
// 从 http 请求头中取出 token
String token = request.getHeader(LoginConstants.LOGIN_AUTHORIZATION);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
Method method = handlerMethod.getMethod();
// 检查是否有 @PassToken 注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// 无token
if (token == null) {
throw new BizException(ErrorCodeEnum.TokenIsNull);
}
token = extractBearerToken(token);
// 获取 token 中的 user account
String account;
JWT jwt = JWTUtil.parseToken(token);
account = jwt.getPayload("account").toString();
TenantAccount tenantAccount = tenantAccountService.getByAccount(account);
if (null == tenantAccount) {
throw new BizException(ErrorCodeEnum.AccountDoesNotExist);
}
// 验证 token
if (!JWTUtil.verify(token, tenantAccount.getPassword().getBytes())) {
throw new BizException(ErrorCodeEnum.AuthTokenInvalid);
}
// 将提取的account,放入请求
request.setAttribute(LoginConstants.SYS_LOGIN_USER_ACCOUNT, account);
return true;
}
/**
* Remove "Bearer " prefix
*
* @param authorizationHeader 授权码
*/
private static String extractBearerToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// Remove "Bearer " prefix
return authorizationHeader.substring(7);
}
return null;
}
@Override
public void postHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) {
}
}
@@ -0,0 +1,111 @@
package oailab.com.apistart.interceptor;
import jakarta.annotation.Resource;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotNull;
import lombok.extern.slf4j.Slf4j;
import oailab.com.apiservice.annotation.PassToken;
import oailab.com.apiservice.constants.LoginConstants;
import oailab.com.apiservice.enums.sys.ErrorCodeEnum;
import oailab.com.apiservice.exception.BizException;
import oailab.com.apiservice.mybatis.entity.workspace.EndUsers;
import oailab.com.apiservice.mybatis.service.workspace.EndUsersService;
import oailab.com.apiservice.service.PassportBusinessService;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import java.lang.reflect.Method;
import java.util.Map;
/**
* Token validation interceptor.
*
* @author Benny Deng
* @version 1.0
* @see <a href="http://www.bianmaren.com">Benny Deng</a>
* @since 6/17/24 PM11:19
**/
@Slf4j
@Component("webTokenInterceptor")
public class WebTokenInterceptor implements HandlerInterceptor {
@Resource
private PassportBusinessService passportBusinessService;
@Resource
private EndUsersService endUsersService;
@Override
public boolean preHandle(HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler) {
// 从 http 请求头中取出 token
String token = request.getHeader(LoginConstants.LOGIN_AUTHORIZATION);
// 如果不是映射到方法直接通过
if (!(handler instanceof HandlerMethod handlerMethod)) {
return true;
}
Method method = handlerMethod.getMethod();
// 检查是否有 @PassToken 注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// 无token
if (token == null) {
throw new BizException(ErrorCodeEnum.TokenIsNull);
}
token = extractBearerToken(token);
Map<String, Object> payload = passportBusinessService.parse(token);
String endUserId = (String) payload.get("endUserId");
EndUsers endUser = endUsersService.getById(endUserId);
if (null == endUser) {
throw new BizException(ErrorCodeEnum.AccountDoesNotExist);
}
// 验证 token
if (!passportBusinessService.verify(token)) {
throw new BizException(ErrorCodeEnum.AuthTokenInvalid);
}
// 将提取的account,放入请求
request.setAttribute(LoginConstants.WEB_LOGIN_USER_ACCOUNT, endUserId);
return true;
}
/**
* Remove "Bearer " prefix
*
* @param authorizationHeader 授权码
*/
private static String extractBearerToken(String authorizationHeader) {
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
// Remove "Bearer " prefix
return authorizationHeader.substring(7);
}
return null;
}
@Override
public void postHandle(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, ModelAndView modelAndView) {
}
@Override
public void afterCompletion(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull Object handler, Exception ex) {
}
}