Compare commits

..

1 Commits

Author SHA1 Message Date
b687dfd22e fastapi鉴权 2024-12-16 14:12:07 +08:00
26 changed files with 280 additions and 998 deletions

38
.gitignore vendored
View File

@ -1,38 +0,0 @@
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### IntelliJ IDEA ###
.idea/modules.xml
.idea/jarRepositories.xml
.idea/compiler.xml
.idea/libraries/
*.iws
*.iml
*.ipr
### Eclipse ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/
### Mac OS ###
.DS_Store

24
linxyun/config.py Normal file
View File

@ -0,0 +1,24 @@
from dataclasses import dataclass, field
from typing import List, Dict
@dataclass
class Upload:
img_quality: float = 1.0 # 默认值为 1.0f
@dataclass
class Config:
url: str = "http://www.linxyun.com" # 默认值
entCode: str = field(default=None, metadata={"required": True, "message": "The 'entCode' property is mandatory"})
project: str = field(default=None, metadata={"required": True, "message": "The 'project' property is mandatory"})
role: Dict[str, List[str]] = field(default_factory=dict)
white_list: List[str] = field(default_factory=list)
upload: Upload = field(default_factory=Upload)
def __post_init__(self):
# 进行字段校验,例如检查必填字段
if self.entCode is None:
raise ValueError("The 'entCode' property is mandatory")
if self.project is None:
raise ValueError("The 'project' property is mandatory")

66
linxyun/core.py Normal file
View File

@ -0,0 +1,66 @@
import requests
from fastapi import FastAPI
from linxyun.utils.result import Result, SysCodes
from linxyun.utils.logger import get_logger
from linxyun.config import Config
from linxyun.sso.middleware.security import SecurityMiddleware
from expiringdict import ExpiringDict
import json
logger = get_logger(__name__)
class Linxyun:
def __init__(self, config: Config):
logger.info(f"Linxyun init{config}")
self.user_auth_dict = ExpiringDict(max_len=10000, max_age_seconds=60*60*24) # 24小时
self.config = config
def add_security_middleware(self, app: FastAPI):
app.add_middleware(SecurityMiddleware, linxyun=self)
def get_api_url(self, path: str) -> str:
# 基础 URL 和项目名称
base_url = f"{self.config.url}/{self.config.project}"
# 如果 path 为空,返回 base_url
if not path:
return base_url
# 如果 path 不以 / 开头,添加 /
if not path.startswith("/"):
path = "/" + path
# 如果 path 不以 .action 结尾,添加 .action
if not path.endswith(".action"):
path += ".action"
# 返回完整的 URL
return base_url + path
def user_login_auth(self, token: str) -> Result:
if not token:
return Result.error(SysCodes.USER_NOT_LOGIN)
url = self.get_api_url("userLoginAuth")
resp = requests.post(url, json={"LoginID": token})
if resp.status_code != 200:
return Result.error(SysCodes.REQUEST_FAILED)
json_resp = resp.json()
if json_resp.get("success") is False:
return Result.error(SysCodes.LOGIN_FAIL)
data = json_resp.get("data")
if not data:
return Result.error(SysCodes.USER_NOT_LOGIN)
data = json.loads(data)
logger.info(f"登录成功:{data}")
self.user_auth_dict[token] = data
return Result.ok(data)
def get_user_auth(self, token: str) -> Result:
if token in self.user_auth_dict:
return Result.ok(self.user_auth_dict[token])
return self.user_login_auth(token)

0
linxyun/sso/__init__.py Normal file
View File

View File

View File

@ -0,0 +1,77 @@
from starlette.middleware.base import BaseHTTPMiddleware
import fnmatch
from starlette.responses import JSONResponse
from linxyun.utils.logger import get_logger
from linxyun.utils.result import Result, SysCodes
import re
logger = get_logger(__name__)
# 鉴权中间件
class SecurityMiddleware(BaseHTTPMiddleware):
def __init__(self, app, linxyun):
logger.info(f"添加鉴权中间件 SecurityMiddleware")
super().__init__(app)
self.linxyun = linxyun
self.pattern = re.compile("LoginID_\\d{14}_\\d{6}")
async def dispatch(self, request, call_next):
# 获取请求路径
path = request.url.path
method = request.method
logger.info(f"鉴权拦截: {method} {path}")
# 获取请求头 Token
token = request.headers.get("token")
if not token:
return JSONResponse(content=Result.error(SysCodes.USER_NOT_LOGIN).to_dict())
if token.startswith("Session"):
search_resp = self.pattern.search(token)
if search_resp:
token = search_resp.group()
user_auth_result:dict = self.linxyun.get_user_auth(token).to_dict()
if user_auth_result.get("success") is False:
logger.info(f"用户登录信息失效: {user_auth_result}")
return JSONResponse(content=user_auth_result)
user_auth = user_auth_result.get("data")
user_role = user_auth.get("UserRoles")
if user_role is None:
logger.info(f"用户角色为空: {user_auth}")
return JSONResponse(content=Result.error(SysCodes.USER_NO_AUTHORITY).to_dict())
if self.linxyun.config.entCode != user_auth.get("EntCode"):
logger.info(f"用户企业编码错误: {user_auth.get('EntCode')}")
return JSONResponse(content=Result.error(SysCodes.LOGIN_ERROR).to_dict())
role_map = self.linxyun.config.role
if user_role not in role_map:
logger.info(f"用户权限未在系统权限中: {user_role}")
return JSONResponse(content=Result.error(SysCodes.LOGIN_ERROR).to_dict())
# 是否有权限访问该路径
path_list = role_map.get(user_role)
if self.is_uri_authorized(path, path_list) is False:
logger.info(f"用户没有权限访问该路径: {path}")
return JSONResponse(content=Result.error(SysCodes.USER_NO_AUTHORITY).to_dict())
logger.info(f"鉴权通过: {path} {token}")
return await call_next(request)
@staticmethod
def is_uri_authorized(path: str, path_list: list[str]) -> bool:
# 遍历所有授权路径
for authorized_path in path_list:
# 如果授权路径包含 **,使用 fnmatch 模块进行通配符匹配
if authorized_path.endswith("/**"):
# 将 ** 转换为通配符 *,然后进行匹配
pattern = authorized_path.replace("/**", "/*")
if fnmatch.fnmatch(path, pattern):
return True
elif path == authorized_path:
# 完全匹配授权路径
return True
return False

View File

30
linxyun/utils/logger.py Normal file
View File

@ -0,0 +1,30 @@
import logging
import os
from logging.handlers import RotatingFileHandler
# 创建日志目录(如果不存在)
LOG_DIR = "logs"
os.makedirs(LOG_DIR, exist_ok=True)
# 日志文件路径
LOG_FILE = os.path.join(LOG_DIR, "app.log")
# 日志格式
LOG_FORMAT = "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
# 配置日志
logging.basicConfig(
level=logging.INFO, # 设置日志级别
format=LOG_FORMAT,
handlers=[
# 控制台输出
logging.StreamHandler(),
# 文件输出,支持日志文件轮转
RotatingFileHandler(LOG_FILE, maxBytes=5 * 1024 * 1024, backupCount=3, encoding="utf-8"),
],
)
# 获取日志实例
def get_logger(name: str):
"""获取带有模块名称的日志记录器"""
return logging.getLogger(name)

83
linxyun/utils/result.py Normal file
View File

@ -0,0 +1,83 @@
from enum import Enum
class SysCodes(Enum):
SUCCESS = ("0000", "操作成功")
PARAM_ERROR = ("1006", "参数错误")
URI_NOT_FOUND = ("240", "URI不存在")
URI_EXISTS = ("241", "URI已经存在")
SERVICE_ERROR = ("320", "服务的路由异常,请联系管理员")
TIMEOUT = ("998", "处理超时,服务配置出错,请联系管理员")
LOGIN_TIMEOUT = ("9998", "登录超时,请重新登录")
LOGIN_FAIL = ("401", "登录超时,请重新登录")
OPERATE_FAIL = ("1005", "操作失败")
USER_EXISTS = ("1007", "用户已经存在")
USER_ID_ERROR = ("1009", "用户ID参数错误")
USER_NOT_EXISTS = ("1010", "用户不存在或密码不匹配")
USER_EMAIL_ERROR = ("1011", "用户邮箱地址错误")
USER_EMAIL_EXISTS = ("1012", "邮箱地址已经存在")
USER_PHONE_ERROR = ("1013", "手机号码格式不正确")
USER_PHONE_EXISTS = ("1014", "手机号码已经存在")
LOGIN_ERROR = ("1020", "登录验证出错")
USER_NOT_LOGIN = ("1021", "用户没有登录")
USER_STATUS_ERROR = ("1022", "用户状态不正常")
SEND_CODE_FAIL = ("1023", "发送验证码失败")
CHECK_CODE_FAIL = ("1024", "验证码验证失败")
USER_NO_AUTHORITY = ("1025", "用户没有权限")
USER_GROUP_EXISTS = ("1030", "用户组已经存在")
USER_GROUP_NOT_EXISTS = ("1031", "用户组不经存在")
OPERATE_ERROR = ("1999", "操作出错")
FRONT_ERROR = ("886000", "前端处理验证数据出错")
DELETE_CONTENT = ("2012", "请先删除内容")
FILE_NOT_FOUND = ("3000", "文件不存在")
FILE_SIZE_EXCEEDED = ("3001", "文件大小超出限制")
FILE_UPLOAD_FAILED = ("3002", "文件上传失败")
REQUEST_FAILED = ("3003", "请求失败")
NONE = ("9999", "异常错误")
def __init__(self, code, msg):
self._value_ = code
self._msg = msg
@property
def code(self):
return self._value_
@property
def msg(self):
return self._msg
class Result:
code:str = ""
msg:str = ""
data:dict = None
success:bool = False
def __init__(self, code:str, msg:str, data:object, success:bool, sys_codes: SysCodes = None) -> None:
if sys_codes:
self.code = sys_codes.code
self.msg = sys_codes.msg
else:
self.code = code
self.msg = msg
self.data = data
self.success = success
def to_dict(self):
return {
"code": self.code,
"msg": self.msg,
"data": self.data,
"success": self.success
}
@staticmethod
def ok(data):
return Result("0000", "操作成功", data, True)
@staticmethod
def error(sys_codes: SysCodes = None):
if sys_codes:
return Result(sys_codes.code, sys_codes.msg, None, False)
return Result.error(SysCodes.NONE)

82
pom.xml
View File

@ -1,82 +0,0 @@
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.linxyun</groupId>
<artifactId>spring-boot-starter-linxyun</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starters</artifactId>
<version>2.1.0.RELEASE</version>
</parent>
<name>Spring Boot Linxyun Starter</name>
<description>Starter for using Linxyun</description>
<developers>
<developer>
<name>Wen Xin</name>
<email>1731551615@qq.com</email>
</developer>
</developers>
<dependencies>
<!--Spring Validation &ndash;&gt;-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
<scope>provided</scope>
</dependency>
<!-- Spring Web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<scope>provided</scope>
</dependency>
<!-- Lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
<version>1.18.34</version>
</dependency>
<!-- Thumbnailator -->
<dependency>
<groupId>net.coobird</groupId>
<artifactId>thumbnailator</artifactId>
<version>0.4.20</version>
</dependency>
<!--OkHttp-->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-http</artifactId>
<version>5.8.34</version>
</dependency>
<!-- FastJSON2 -->
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.53</version>
</dependency>
<!-- ExpiringMap -->
<dependency>
<groupId>net.jodah</groupId>
<artifactId>expiringmap</artifactId>
<version>0.5.11</version>
</dependency>
<!-- Spring Configuration Processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</project>

View File

@ -1,57 +0,0 @@
package com.linxyun.core.common.entity;
import com.linxyun.core.common.enums.ErrorCode;
import lombok.Data;
import lombok.ToString;
@Data
@ToString
public class Result<E> {
public E data;
public String code;
public String msg;
public boolean success = true;
public Result(E data, String code, String msg, boolean success) {
this.data = data;
this.code = code;
this.msg = msg;
this.success = success;
}
public static <T> Result<T> error(ErrorCode codeEnum) {
return new Result<>(null, codeEnum.getCode(), codeEnum.getMessage(), false);
}
public static <T> Result<T> error(ErrorCode codeEnum, String message) {
return new Result<>(null, codeEnum.getCode(), message, false);
}
public static <T> Result<T> error(String code, String msg) {
return new Result<>(null, code, msg, false);
}
public static <T> Result<T> ok() {
return new Result<>(null, "0000", "操作成功", true);
}
public static <T> Result<T> ok(String msg) {
return new Result<>(null, "0000", msg, true);
}
public static <T> Result<T> ok(T data) {
return new Result<>(data, "0000", "操作成功", true);
}
public static <T> Result<T> ok(T data, String msg) {
return new Result<>(data, "0000", msg, true);
}
public Result() {
this.success = false;
this.code = "9999";
this.msg = "操作失败";
this.data = null;
}
}

View File

@ -1,16 +0,0 @@
package com.linxyun.core.common.entity;
import lombok.Data;
@Data
public class UserAuth {
private String ContainerID;
private String EntCode;
private String LoginEntCode;
private String LoginIP;
private String LoginNodeID;
private String LoginTime;
private String UserID;
private String UserName;
private String UserRoles;
}

View File

@ -1,126 +0,0 @@
package com.linxyun.core.common.enums;
import lombok.Getter;
@Getter
public enum ErrorCode {
// 用户不存在或密码不匹配
USER_NOT_FOUND_OR_PASSWORD_MISMATCH("1006", "用户不存在或密码不匹配"),
// 参数错误
PARAMETER_ERROR("204", "参数错误"),
// URI 不存在
URI_NOT_FOUND("240", "URI不存在"),
// URI 已经存在
URI_ALREADY_EXISTS("241", "URI已经存在"),
// 服务的路由异常请联系管理员
ROUTING_EXCEPTION("320", "服务的路由异常,请联系管理员"),
// 处理超时服务配置出错请联系管理员
TIMEOUT_ERROR("998", "处理超时,服务配置出错,请联系管理员"),
// 登录超时请重新登录
LOGIN_TIMEOUT("9998", "登录超时,请重新登录"),
// 登录超时请重新登录
LOGIN_TIMEOUT_2("401", "登录超时,请重新登录"),
// 操作失败
OPERATION_FAIL("1005", "操作失败"),
// 用户已经存在
USER_ALREADY_EXISTS("1007", "用户已经存在"),
// 操作ID配置错误请联系管理员
OPERATION_ID_ERROR("1008", "操作ID配置错误请联系管理员"),
// 用户ID参数错误
USER_ID_PARAMETER_ERROR("1009", "用户ID参数错误"),
// 用户邮箱地址错误
USER_EMAIL_ERROR("1011", "用户邮箱地址错误"),
// 邮箱地址已经存在
EMAIL_ALREADY_EXISTS("1012", "邮箱地址已经存在"),
// 手机号码格式不正确
PHONE_NUMBER_FORMAT_ERROR("1013", "手机号码格式不正确"),
// 手机号码已经存在
PHONE_NUMBER_ALREADY_EXISTS("1014", "手机号码已经存在"),
// 登录验证出错
LOGIN_VALIDATION_ERROR("1020", "登录验证出错"),
// 用户没有登录
USER_NOT_LOGGED_IN("1021", "用户没有登录"),
// 用户状态不正常
USER_STATUS_ERROR("1022", "用户状态不正常"),
// 发送验证码失败
SEND_VERIFICATION_CODE_FAIL("1023", "发送验证码失败"),
// 验证码验证失败
VERIFICATION_CODE_FAIL("1024", "验证码验证失败"),
USER_NO_AUTHORITY("1025", "用户没有权限"),
// 用户组已经存在
USER_GROUP_ALREADY_EXISTS("1030", "用户组已经存在"),
// 用户组不存在
USER_GROUP_NOT_EXISTS("1031", "用户组不经存在"),
// 操作出错
OPERATION_ERROR("1999", "操作出错"),
// 前端处理验证数据出错
FRONTEND_VALIDATION_ERROR("886000", "前端处理验证数据出错"),
// 请先删除内容
DELETE_CONTENT_FIRST("2012", "请先删除内容"),
// 文件不存在
FILE_NOT_FOUND("3000", "文件不存在"),
// 文件大小超出限制
FILE_SIZE_EXCEEDED("3001", "文件大小超出限制"),
// 文件上传失败
FILE_UPLOAD_FAILED("3002", "文件上传失败"),
// 请求失败
REQUEST_FAILED("3003", "请求失败"),
NONE("9999", "异常错误");
private final String code;
private final String message;
ErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
// 根据错误代码获取对应的错误信息
public static String getMessageByCode(String code) {
for (ErrorCode errorCode : values()) {
if (errorCode.getCode().equals(code)) {
return errorCode.getMessage();
}
}
return "未知错误";
}
public static ErrorCode getErrorCodeByCode(String code) {
for (ErrorCode errorCode : values()) {
if (errorCode.getCode().equals(code)) {
return errorCode;
}
}
return ErrorCode.NONE;
}
}

View File

@ -1,27 +0,0 @@
package com.linxyun.core.common.enums;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public enum FileType {
IMAGE(Arrays.asList(".jpg", ".jpeg", ".png", ".gif", ".bmp")),
VIDEO(Arrays.asList(".mp4", ".mkv", ".mov", ".avi")),
AUDIO(Arrays.asList(".mp3", ".wav", ".ogg")),
DOCUMENT(Arrays.asList(".pdf", ".doc", ".docx", ".xls", ".xlsx")),
OTHER(new ArrayList<>());
private final List<String> extensions;
FileType(List<String> extensions) {
this.extensions = extensions;
}
public static FileType fromExtension(String fileName) {
String extension = fileName.substring(fileName.lastIndexOf(".")).toLowerCase();
return Arrays.stream(values())
.filter(type -> type.extensions.contains(extension))
.findFirst()
.orElse(OTHER);
}
}

View File

@ -1,8 +0,0 @@
package com.linxyun.core.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.stereotype.Component;
@ComponentScan(basePackages = "com.linxyun.core")
public class AutoConfiguration {
}

View File

@ -1,39 +0,0 @@
package com.linxyun.core.config;
import com.linxyun.core.interceptor.SecurityInterceptor;
import com.linxyun.core.properties.LinxyunProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.validation.constraints.NotNull;
import java.util.List;
@Slf4j
@Configuration
@RequiredArgsConstructor
public class WebInterceptorConfig implements WebMvcConfigurer {
private final LinxyunProperties linxyunProperties;
private final SecurityInterceptor securityInterceptor;
// 添加拦截器
@Override
public void addInterceptors(@NotNull InterceptorRegistry registry) {
log.info("添加鉴权拦截器Linxyun Security Interceptor");
List<String> whiteList = linxyunProperties.getWhiteList();
// 添加默认的静态资源路径排除
whiteList.add("/static/**");
whiteList.add("/public/**");
whiteList.add("/resources/**");
whiteList.add("/META-INF/resources/**");
whiteList.add("/webjars/**");
log.info("白名单:{}", whiteList);
registry.addInterceptor(securityInterceptor)
.excludePathPatterns(whiteList)
.addPathPatterns("/**");
}
}

View File

@ -1,117 +0,0 @@
package com.linxyun.core.interceptor;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.linxyun.core.common.entity.Result;
import com.linxyun.core.common.entity.UserAuth;
import com.linxyun.core.common.enums.ErrorCode;
import com.linxyun.core.properties.LinxyunProperties;
import com.linxyun.core.utils.ApiUtils;
import com.linxyun.core.utils.URLUtils;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Slf4j
@Component
@RequiredArgsConstructor
public class SecurityInterceptor implements HandlerInterceptor {
private final LinxyunProperties linxyunProperties;
private Pattern pattern = Pattern.compile("LoginID_\\d{14}_\\d{6}");
// 生命周期 拦截器在请求处理之前调用只有返回true才会继续调用下一个拦截器或者处理器否则不会调用
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 跨域请求会首先发送一个OPTIONS请求这里我们给OPTIONS请求直接返回正常状态
if (request.getMethod().equals("OPTIONS")) return true;
log.info("鉴权拦截:{} {}", request.getMethod(), request.getRequestURI());
// 获取请求头上的Token
String token = request.getHeader("Token");
if (StringUtils.isEmpty(token)) {
token = request.getParameter("Token");
}
log.info("请求头中Token{}", token);
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
Result<JSONObject> result;
if (StringUtils.isEmpty(token)) {
log.info("请求头中无 Token 信息");
result = Result.error(ErrorCode.USER_NOT_LOGGED_IN);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
if (token.startsWith("Session")) {
Matcher matcher = pattern.matcher(token);
if (matcher.find()) {
token = matcher.group();
}
}
Result<UserAuth> authRt = ApiUtils.getUserAuth(token);
assert authRt != null; // 断言
if (!authRt.isSuccess()) {
response.getWriter().write(JSON.toJSONString(authRt));
return false;
}
UserAuth userAuth = authRt.getData();
String userRole = userAuth.getUserRoles();
if (StringUtils.isEmpty(userRole)) {
log.info("用户权限为空:{}", userAuth.getUserRoles());
result = Result.error(ErrorCode.USER_NO_AUTHORITY);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
if (!linxyunProperties.getEntCode().equals(userAuth.getEntCode())) {
log.info("用户企业编码错误:{}", userAuth.getEntCode());
result = Result.error(ErrorCode.LOGIN_VALIDATION_ERROR);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
// 验证用户是否拥有权限
Map<String, List<String>> roleMap = linxyunProperties.getRole();
if (!roleMap.containsKey(userAuth.getUserRoles())) {
log.info("用户权限未在系统权限中:{}", userAuth.getUserRoles());
result = Result.error(ErrorCode.USER_NO_AUTHORITY);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
List<String> pathList = roleMap.get(userRole);
// 是否有权限访问该路径
if (!URLUtils.isUriAuthorized(request.getRequestURI(), pathList)) {
log.info("用户无权访问该路径:{}", request.getRequestURI());
result = Result.error(ErrorCode.USER_NO_AUTHORITY);
response.getWriter().write(JSON.toJSONString(result));
return false;
}
log.info("鉴权通过:{} {}", token, request.getRequestURI());
return true;
}
// 生命周期 拦截器在请求处理之后调用但是此时还没有返回视图只有返回true才会继续调用下一个拦截器或者处理器否则不会调用
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
// 生命周期 拦截器在视图渲染之后调用即在视图渲染完成之后页面响应给客户端之前只有返回true才会继续调用下一个拦截器或者处理器否则不会调用
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}

View File

@ -1,88 +0,0 @@
package com.linxyun.core.properties;
import lombok.Data;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;
import org.springframework.validation.annotation.Validated;
import org.springframework.boot.context.properties.ConfigurationProperties;
import javax.annotation.PostConstruct;
import javax.validation.constraints.NotNull;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "linxyun")
@Validated
public class LinxyunProperties implements ApplicationContextAware {
private static ApplicationContext context;
/**
* url
*/
private String url = "http://www.linxyun.com";
/**
* 企业编码
*/
@NotNull(message = "The 'entCode' property is mandatory")
private String entCode;
/**
* 项目名称
*/
@NotNull(message = "The 'project' property is mandatory")
private String project;
/**
* 角色权限
*/
private Map<String, List<String>> role = new HashMap<>();
/**
* 白名单
*/
private List<String> whiteList = new ArrayList<>();
/**
* 上传配置
*/
private Upload upload = new Upload();
@Data
public static class Upload {
private Float imgQuality = 1.0f;
}
@Value("${server.servlet.context-path}")
private String contextPath;
@PostConstruct
public void init() {
// 解决 springboot 设置 context-path导致权限路由验证失效
if (contextPath != null && !contextPath.isEmpty()) {
for (String key : role.keySet()) {
List<String> paths = role.get(key);
for (int i = 0; i < paths.size(); i++) {
String path = paths.get(i);
if (!path.startsWith(contextPath)) {
paths.set(i, contextPath + path);
}
}
}
}
}
// 静态方法来访问实例
public static LinxyunProperties getInstance() {
return context.getBean(LinxyunProperties.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
context = applicationContext;
}
}

View File

@ -1,142 +0,0 @@
package com.linxyun.core.utils;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import cn.hutool.http.body.RequestBody;
import com.alibaba.fastjson2.JSONObject;
import com.linxyun.core.common.entity.Result;
import com.linxyun.core.common.entity.UserAuth;
import com.linxyun.core.common.enums.ErrorCode;
import com.linxyun.core.properties.LinxyunProperties;
import lombok.extern.slf4j.Slf4j;
import net.jodah.expiringmap.ExpirationPolicy;
import net.jodah.expiringmap.ExpiringMap;
import org.springframework.util.StringUtils;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
@Slf4j
public class ApiUtils {
private final static ExpiringMap<String, UserAuth> USER_AUTH_MAP = ExpiringMap.builder()
// 设置最大值,添加第11个entry时会导致第1个立马过期(即使没到过期时间)默认 Integer.MAX_VALUE
.maxSize(10)
// 允许 Map 元素具有各自的到期时间并允许更改到期时间
.variableExpiration()
// 设置过期时间如果key不设置过期时间key永久有效
.expiration(1, TimeUnit.DAYS)
.asyncExpirationListener((key, value) -> log.info("USER_AUTH_MAP key数据被删除了 -> key={}, value={}", key, value))
//设置 Map 的过期策略
.expirationPolicy(ExpirationPolicy.CREATED)
.build();
private static final LinxyunProperties linxyunProperties = LinxyunProperties.getInstance();
private static final String url = linxyunProperties.getUrl();
private static final String projectName = linxyunProperties.getProject();
/**
* 单点登录
*/
public static Result<UserAuth> userLoginAuth(String token) {
try {
if (StringUtils.isEmpty(token)) {
return Result.error(ErrorCode.USER_NOT_LOGGED_IN);
}
String url = getApiUrl("userLoginAuth");
JSONObject body = new JSONObject();
body.put("LoginID", token);
JSONObject result = HttpUtils.post(url, body);
if (result == null) {
log.error("LinxyunUtils-userLoginAuth result is null");
return Result.error(ErrorCode.REQUEST_FAILED);
}
log.info("LinxyunUtils-userLoginAuth result: {}", result);
if (!result.getBoolean("success")) {
String code = result.getString("code");
String msg = result.getString("msg");
log.error("LinxyunUtils-userLoginAuth result is not success: {}", msg);
return Result.error(ErrorCode.getErrorCodeByCode(code));
}
String data = result.getString("data");
if (StringUtils.isEmpty(data)) {
log.error("LinxyunUtils-userLoginAuth result.data is null");
return Result.error(ErrorCode.OPERATION_ERROR);
}
UserAuth userAuth = JSONObject.parseObject(data, UserAuth.class);
USER_AUTH_MAP.put(token, userAuth);
return Result.ok(userAuth);
} catch (Exception e) {
log.error("linxyunUtils.userLoginAuth error: {}", e.getMessage());
return Result.error(ErrorCode.TIMEOUT_ERROR);
}
}
public static Result<UserAuth> getUserAuth(String token) {
if (StringUtils.isEmpty(token)) {
return null;
}
boolean isExist = USER_AUTH_MAP.containsKey(token);
if (isExist) {
// 存在直接获取缓存的数据
UserAuth userAuth = USER_AUTH_MAP.get(token);
return Result.ok(userAuth);
}
return userLoginAuth(token);
}
public static String getApiUrl(String path) {
String baseUrl = url + "/" + projectName;
if (StringUtils.isEmpty(path)) {
return baseUrl;
}
if (!path.startsWith("/")) {
path = "/" + path;
}
if (!path.endsWith(".action")) {
path += ".action";
}
return baseUrl + path;
}
/**
* 使用 OkHttp 上传文件
*
* @param fileBytes 文件字节数组
* @param fileName 文件名
* @return 上传成功时返回服务器响应的 "data" 字段否则返回 null
*/
public static String uploadFile(byte[] fileBytes, String fileName) {
String uploadUrl = url + "/eslithe/uploadFile.action";
// 创建请求
HttpRequest request = HttpUtil.createPost(uploadUrl);
request.form("file", fileBytes, fileName);
// 执行请求
try (HttpResponse response = request.execute()) {
if (response.isOk() && response.body() != null) {
String responseBody = response.body();
log.info("FileUploadTool uploadFile result: {}", responseBody);
// 解析响应
JSONObject jsonObject = JSONObject.parseObject(responseBody);
if (jsonObject != null
&& jsonObject.getBoolean("success") != null
&& jsonObject.getBoolean("success")
&& jsonObject.containsKey("data")) {
return jsonObject.getString("data");
} else {
log.error("FileUploadTool uploadFile failed: {}", responseBody);
return null;
}
} else {
log.error("FileUploadTool uploadFile failed, HTTP code: {}", response.getStatus());
return null;
}
}
}
}

View File

@ -1,94 +0,0 @@
package com.linxyun.core.utils;
import cn.hutool.http.HttpResponse;
import com.linxyun.core.common.enums.FileType;
import com.linxyun.core.properties.LinxyunProperties;
import lombok.extern.slf4j.Slf4j;
import net.coobird.thumbnailator.Thumbnails;
import java.io.*;
@Slf4j
public class FileUtils {
private static final LinxyunProperties linxyunProperties = LinxyunProperties.getInstance();
/**
* 下载文件到指定目录
* @param fileUrl 文件 URL
* @param targetDir 本地目标目录
* @param fileName 保存的文件名
* @throws IOException 如果发生 IO 异常
*/
public static void downloadFile(String fileUrl, String targetDir, String fileName) {
// 执行请求
try (HttpResponse response = HttpUtils.getResp(fileUrl)) {
if (!response.isOk()) {
log.info("文件下载失败, HTTP 响应码: {}", response.getStatus());
return;
}
// 确保目标目录存在
File directory = new File(targetDir);
if (!directory.exists() && !directory.mkdirs()) {
log.error("无法创建目标目录: {}", targetDir);
return;
}
// 获取输入流并保存文件
byte[] bytes = response.body().getBytes();
FileOutputStream outputStream = new FileOutputStream(new File(directory, fileName));
outputStream.write(bytes);
log.info("文件下载完成: {}", targetDir + File.separator + fileName);
} catch (FileNotFoundException e) {
log.error("文件不存在", e);
} catch (IOException e) {
log.error("文件下载异常", e);
}
}
/**
* 下载指定 URL 的文件并将内容转化为字节数组
*
* @param fileUrl 文件的 URL
* @param fileName 文件名可以用来关联下载到本地的文件
* @return 文件内容的字节数组
*/
public static String uploadFileByUrl(String fileUrl, String fileName) {
FileType fileType = FileType.fromExtension(fileName);
// 执行请求
try (HttpResponse response = HttpUtils.getResp(fileUrl)) {
if (!response.isOk()) {
log.error("Failed to download file: {}", response.body());
return null;
}
// 获取文件内容作为字节数组
try (InputStream inputStream = response.bodyStream()) {
// 如果是图片进行压缩
if (fileType == FileType.IMAGE) {
try (ByteArrayOutputStream compressedOutputStream = new ByteArrayOutputStream()) {
log.info("压缩图片:{} 图片质量: {}", fileUrl, linxyunProperties.getUpload().getImgQuality());
// 压缩图片
Thumbnails.of(inputStream)
.outputQuality(linxyunProperties.getUpload().getImgQuality()) // 设置输出质量 (0~1)
.scale(1.0) // 保持原尺寸如需缩放可调整 scale 值或使用 size 方法
.toOutputStream(compressedOutputStream);
// 日志输出原始大小和压缩后大小
log.info("图片原始大小: {} 字节", response.contentLength());
log.info("图片压缩后大小: {} 字节", compressedOutputStream.size());
// 上传压缩后的图片
return ApiUtils.uploadFile(compressedOutputStream.toByteArray(), fileName);
}
} else {
// 非图片直接上传
log.info("上传其它文件:{}", fileUrl);
byte[] fileBytes = StreamUtils.readAllBytes(inputStream);
return ApiUtils.uploadFile(fileBytes, fileName);
}
}
} catch (IOException e) {
log.error("FileUploadTool uploadFileByUrl exception: ", e);
return null;
}
}
}

View File

@ -1,84 +0,0 @@
package com.linxyun.core.utils;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.http.HttpUtil;
import com.alibaba.fastjson2.JSONObject;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.util.Map;
@Slf4j
public class HttpUtils {
// 通用请求方法处理所有的 GET POST 请求
private static JSONObject executeRequest(HttpRequest request) {
try (HttpResponse response = request.execute()) {
log.info("HttpRequest{} {}", request.getUrl(), request.getMethod());
if (!response.isOk()) {
log.error("HttpRequest failed: {}", response.getStatus());
return null;
}
if (response.body() == null) {
return null;
}
return JSONObject.parseObject(response.body());
}
}
// GET 请求
public static JSONObject get(String url) {
return executeRequest(HttpUtil.createGet(url));
}
public static HttpResponse getResp(String url) throws IOException {
return HttpUtil.createGet(url).execute();
}
public static HttpResponse postResp(String url, JSONObject body) throws IOException {
return HttpUtil.createPost(url).body(body.toJSONString()).execute();
}
public static JSONObject get(String url, Map<String, String> params) {
String urlWithParams = buildUrlWithParams(url, params);
return get(urlWithParams);
}
public static JSONObject get(String url, Map<String, String> params, Map<String, String> headers) throws IOException {
String urlWithParams = buildUrlWithParams(url, params);
return get(urlWithParams, headers);
}
// POST 请求
public static JSONObject post(String url, JSONObject body) {
HttpRequest request = HttpUtil.createPost(url).body(body.toJSONString());
return executeRequest(request);
}
public static JSONObject post(String url, JSONObject body, Map<String, String> params) {
String urlWithParams = buildUrlWithParams(url, params);
return post(urlWithParams, body);
}
public static JSONObject post(String url, Map<String, String> params, JSONObject body, Map<String, String> headers) {
String urlWithParams = buildUrlWithParams(url, params);
HttpRequest request = HttpUtil.createPost(urlWithParams).body(body.toJSONString()).addHeaders(headers);
return executeRequest(request);
}
// 构建带参数的 URL
private static String buildUrlWithParams(String url, Map<String, String> params) {
if (params == null || params.isEmpty()) {
return url;
}
StringBuilder sb = new StringBuilder(url);
sb.append("?");
for (Map.Entry<String, String> entry : params.entrySet()) {
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
}
return sb.substring(0, sb.length() - 1);
}
}

View File

@ -1,19 +0,0 @@
package com.linxyun.core.utils;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
public class StreamUtils {
public static byte[] readAllBytes(InputStream inputStream) throws IOException {
try (ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream()) {
byte[] buffer = new byte[1024];
int bytesRead;
while ((bytesRead = inputStream.read(buffer)) != -1) {
byteArrayOutputStream.write(buffer, 0, bytesRead);
}
return byteArrayOutputStream.toByteArray();
}
}
}

View File

@ -1,25 +0,0 @@
package com.linxyun.core.utils;
import lombok.extern.slf4j.Slf4j;
import java.text.SimpleDateFormat;
import java.util.Date;
@Slf4j
public class TimeUtils {
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyyMMddHHmmss"); // 定义日期格式
public static Date toDate(String dateStr) {
try {
return dateFormat.parse(dateStr);
} catch (Exception e) {
log.error("时间转换异常", e);
}
return null;
}
public static String getCurrentTime() {
Date now = new Date();
return dateFormat.format(now);
}
}

View File

@ -1,35 +0,0 @@
package com.linxyun.core.utils;
import org.springframework.util.AntPathMatcher;
import java.util.List;
public class URLUtils {
private static final AntPathMatcher pathMatcher = new AntPathMatcher();
/**
* 检查请求的 URI 是否符合角色对应的路径列表
*
* @param requestUri 需要匹配的 URI
* @param pathList 角色对应的路径列表
* @return true 如果匹配否则 false
*/
public static boolean isUriAuthorized(String requestUri, List<String> pathList) {
// 获取请求的 URI
if (requestUri == null || requestUri.isEmpty()) {
return false;
}
if (pathList == null) {
return false;
}
// 遍历路径列表检查是否有匹配项
for (String pathPattern : pathList) {
if (pathMatcher.match(pathPattern, requestUri)) {
return true;
}
}
// 如果没有匹配到返回 false
return false;
}
}

View File

@ -1 +0,0 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=com.linxyun.core.config.AutoConfiguration