init
This commit is contained in:
parent
16bb8fac3f
commit
d15443d0e3
@ -0,0 +1,21 @@
|
||||
package top.biwin.xinayu.common.dto.response;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 10:48
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder
|
||||
public class FileUploadResponse extends BaseResponse{
|
||||
|
||||
@JsonProperty("image_url")
|
||||
private String imageUrl;
|
||||
}
|
||||
@ -42,101 +42,102 @@ import top.biwin.xinayu.server.security.JwtAuthenticationFilter;
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity(prePostEnabled = true)
|
||||
public class SecurityConfig {
|
||||
@Autowired
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
@Autowired
|
||||
private JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
@Autowired
|
||||
private UserDetailsService userDetailsService;
|
||||
|
||||
/**
|
||||
* 配置安全过滤器链
|
||||
* <p>
|
||||
* 配置项:
|
||||
* 1. 禁用 CSRF(JWT 无状态认证不需要)
|
||||
* 2. 配置授权规则:
|
||||
* - /auth/** 接口允许匿名访问(登录、刷新令牌)
|
||||
* - 其他所有接口需要认证
|
||||
* 3. 配置无状态会话管理
|
||||
* 4. 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前)
|
||||
*
|
||||
* @param http HttpSecurity 对象
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用 CSRF(JWT 场景下不需要)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
/**
|
||||
* 配置安全过滤器链
|
||||
* <p>
|
||||
* 配置项:
|
||||
* 1. 禁用 CSRF(JWT 无状态认证不需要)
|
||||
* 2. 配置授权规则:
|
||||
* - /auth/** 接口允许匿名访问(登录、刷新令牌)
|
||||
* - 其他所有接口需要认证
|
||||
* 3. 配置无状态会话管理
|
||||
* 4. 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前)
|
||||
*
|
||||
* @param http HttpSecurity 对象
|
||||
* @return SecurityFilterChain
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用 CSRF(JWT 场景下不需要)
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 白名单:允许 /auth/** 接口匿名访问
|
||||
.requestMatchers("/auth/**",
|
||||
"/geetest/**",
|
||||
"system-settings/public")
|
||||
.permitAll()
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
// 白名单:允许 /auth/** 接口匿名访问
|
||||
.requestMatchers("/auth/**",
|
||||
"/geetest/**",
|
||||
"/system-settings/public",
|
||||
"/static/**")
|
||||
.permitAll()
|
||||
|
||||
// 其他所有接口都需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
// 其他所有接口都需要认证
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
|
||||
// 配置无状态会话管理(JWT 无状态认证)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
// 配置无状态会话管理(JWT 无状态认证)
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
)
|
||||
|
||||
// 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
// 添加 JWT 认证过滤器(在 UsernamePasswordAuthenticationFilter 之前执行)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码编码器 Bean
|
||||
* 使用 BCrypt 算法,强度为 12(2^12 = 4096 次哈希)
|
||||
* <p>
|
||||
* BCrypt 优势:
|
||||
* - 自动加盐
|
||||
* - 计算密集,防暴力破解
|
||||
* - 业界标准
|
||||
*
|
||||
* @return PasswordEncoder
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12);
|
||||
}
|
||||
/**
|
||||
* 密码编码器 Bean
|
||||
* 使用 BCrypt 算法,强度为 12(2^12 = 4096 次哈希)
|
||||
* <p>
|
||||
* BCrypt 优势:
|
||||
* - 自动加盐
|
||||
* - 计算密集,防暴力破解
|
||||
* - 业界标准
|
||||
*
|
||||
* @return PasswordEncoder
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder(12);
|
||||
}
|
||||
|
||||
/**
|
||||
* 认证管理器 Bean
|
||||
* Spring Security 用于处理认证请求
|
||||
*
|
||||
* @param authenticationConfiguration 认证配置
|
||||
* @return AuthenticationManager
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
/**
|
||||
* 认证管理器 Bean
|
||||
* Spring Security 用于处理认证请求
|
||||
*
|
||||
* @param authenticationConfiguration 认证配置
|
||||
* @return AuthenticationManager
|
||||
* @throws Exception 配置异常
|
||||
*/
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(
|
||||
AuthenticationConfiguration authenticationConfiguration) throws Exception {
|
||||
return authenticationConfiguration.getAuthenticationManager();
|
||||
}
|
||||
|
||||
/**
|
||||
* DAO 认证提供者 Bean
|
||||
* 连接 UserDetailsService 和 PasswordEncoder
|
||||
* <p>
|
||||
* 工作流程:
|
||||
* 1. 从 UserDetailsService 加载用户信息
|
||||
* 2. 使用 PasswordEncoder 验证密码
|
||||
*
|
||||
* @return DaoAuthenticationProvider
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
/**
|
||||
* DAO 认证提供者 Bean
|
||||
* 连接 UserDetailsService 和 PasswordEncoder
|
||||
* <p>
|
||||
* 工作流程:
|
||||
* 1. 从 UserDetailsService 加载用户信息
|
||||
* 2. 使用 PasswordEncoder 验证密码
|
||||
*
|
||||
* @return DaoAuthenticationProvider
|
||||
*/
|
||||
@Bean
|
||||
public DaoAuthenticationProvider authenticationProvider() {
|
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
|
||||
authProvider.setUserDetailsService(userDetailsService);
|
||||
authProvider.setPasswordEncoder(passwordEncoder());
|
||||
return authProvider;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,117 @@
|
||||
package top.biwin.xinayu.server.controller;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import top.biwin.xinayu.common.dto.response.FileUploadResponse;
|
||||
import top.biwin.xinayu.server.util.ImageUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* 辅助功能控制器
|
||||
* 提供文件上传等辅助接口哦~ (〃^ω^〃)
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 10:46
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
public class AssistController {
|
||||
|
||||
@Value("${assistant.static.uploads.images}")
|
||||
private String uploadDir;
|
||||
|
||||
/**
|
||||
* 获取图片内容
|
||||
* 会直接返回处理后的 JPEG 二进制流哦!📸✨
|
||||
*
|
||||
* @param filename 文件名
|
||||
* @return 图片二进制流
|
||||
*/
|
||||
@GetMapping(value = "${assistant.static.uploads.images}{filename:.+}", produces = MediaType.IMAGE_JPEG_VALUE)
|
||||
public ResponseEntity<byte[]> getImage(@PathVariable String filename) {
|
||||
try {
|
||||
Path path = Paths.get(uploadDir, filename);
|
||||
if (Files.exists(path)) {
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.IMAGE_JPEG)
|
||||
.body(Files.readAllBytes(path));
|
||||
} else {
|
||||
log.warn("图片不存在呢... (´;︵;`) : {}", filename);
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("读取图片失败: {}", filename, e);
|
||||
return ResponseEntity.internalServerError().build();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传图片并处理
|
||||
*
|
||||
* @param file 图片文件
|
||||
* @return 上传结果,包含图片相对路径
|
||||
*/
|
||||
@PostMapping("/upload-image")
|
||||
public ResponseEntity<FileUploadResponse> uploadImage(@RequestParam("image") MultipartFile file) {
|
||||
if (file.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(FileUploadResponse.builder()
|
||||
.success(false)
|
||||
.message("文件不能为空哦!(っ °Д °;)っ")
|
||||
.build());
|
||||
}
|
||||
|
||||
try {
|
||||
// 1. 调用工具类处理图片
|
||||
byte[] processedImage = ImageUtils.processImage(file.getBytes());
|
||||
|
||||
// 2. 准备保存路径(主动创建目录哦!)
|
||||
Path path = Paths.get(uploadDir);
|
||||
if (Files.notExists(path)) {
|
||||
log.info("目录不存在,小码酱正在为主官主动创建: {}", uploadDir);
|
||||
Files.createDirectories(path);
|
||||
}
|
||||
File directory = path.toFile();
|
||||
|
||||
// 3. 生成新文件名
|
||||
String fileName = UUID.randomUUID().toString() + ".jpg";
|
||||
File destFile = new File(directory, fileName);
|
||||
|
||||
// 4. 保存文件
|
||||
try (FileOutputStream fos = new FileOutputStream(destFile)) {
|
||||
fos.write(processedImage);
|
||||
}
|
||||
|
||||
// 5. 返回相对路径
|
||||
String relativePath = uploadDir + fileName;
|
||||
log.info("图片上传成功: {}", relativePath);
|
||||
|
||||
return ResponseEntity.ok(FileUploadResponse.builder()
|
||||
.success(true)
|
||||
.message("上传成功!✨")
|
||||
.imageUrl(relativePath)
|
||||
.build());
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("图片上传处理失败", e);
|
||||
return ResponseEntity.status(500).body(FileUploadResponse.builder()
|
||||
.success(false)
|
||||
.message("图片处理失败了呢... " + e.getMessage())
|
||||
.build());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,144 @@
|
||||
package top.biwin.xinayu.server.util;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.imageio.IIOImage;
|
||||
import javax.imageio.ImageIO;
|
||||
import javax.imageio.ImageWriteParam;
|
||||
import javax.imageio.ImageWriter;
|
||||
import javax.imageio.stream.ImageOutputStream;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Iterator;
|
||||
|
||||
/**
|
||||
* 图片处理工具类
|
||||
* 完美复现 Master 的 Python 逻辑哦!(。◕‿◕。)
|
||||
*
|
||||
* @author wangli
|
||||
* @since 2026-01-30 11:03
|
||||
*/
|
||||
@Slf4j
|
||||
public class ImageUtils {
|
||||
|
||||
private static final int MAX_OUTPUT_DIMENSION = 2048;
|
||||
private static final float JPEG_QUALITY = 0.85f;
|
||||
|
||||
/**
|
||||
* 处理图片:转换模式、调整尺寸、JPEG 压缩
|
||||
*
|
||||
* @param imageData 原始图片数据
|
||||
* @return 处理后的 JPEG 图片数据
|
||||
* @throws IOException 如果图片处理失败
|
||||
*/
|
||||
public static byte[] processImage(byte[] imageData) throws IOException {
|
||||
try (ByteArrayInputStream bais = new ByteArrayInputStream(imageData)) {
|
||||
BufferedImage img = ImageIO.read(bais);
|
||||
if (img == null) {
|
||||
throw new IOException("无法识别的图片格式 (っ °Д °;)っ");
|
||||
}
|
||||
|
||||
// 1. 转换为 RGB 模式(处理透明度/索引颜色)
|
||||
img = convertToRgb(img);
|
||||
|
||||
// 2. 调整尺寸(如果需要)
|
||||
img = resizeIfNecessary(img);
|
||||
|
||||
// 3. 保存为 JPEG 并压缩
|
||||
return compressToJpeg(img);
|
||||
} catch (Exception e) {
|
||||
log.error("图片处理失败: {}", e.getMessage(), e);
|
||||
// 如果处理失败,根据提示返回原始数据(或者抛出异常)
|
||||
// 这里我们遵循 Python 逻辑,失败则尝试返回原始数据
|
||||
return imageData;
|
||||
}
|
||||
}
|
||||
|
||||
private static BufferedImage convertToRgb(BufferedImage source) {
|
||||
int width = source.getWidth();
|
||||
int height = source.getHeight();
|
||||
|
||||
// 检查是否需要转换:如果有透明通道或者是索引颜色
|
||||
// Python: if img.mode in ('RGBA', 'LA', 'P'):
|
||||
boolean hasAlpha = source.getTransparency() != Transparency.OPAQUE;
|
||||
|
||||
// 创建白色背景的 RGB 图像
|
||||
BufferedImage rgbImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = rgbImage.createGraphics();
|
||||
|
||||
try {
|
||||
// 设置背景为白色
|
||||
g2d.setColor(Color.WHITE);
|
||||
g2d.fillRect(0, 0, width, height);
|
||||
|
||||
// 绘制原图(自动处理透明混合)
|
||||
g2d.drawImage(source, 0, 0, null);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
return rgbImage;
|
||||
}
|
||||
|
||||
private static BufferedImage resizeIfNecessary(BufferedImage img) {
|
||||
int width = img.getWidth();
|
||||
int height = img.getHeight();
|
||||
|
||||
if (width > MAX_OUTPUT_DIMENSION || height > MAX_OUTPUT_DIMENSION) {
|
||||
double ratio = Math.min((double) MAX_OUTPUT_DIMENSION / width, (double) MAX_OUTPUT_DIMENSION / height);
|
||||
int newWidth = (int) (width * ratio);
|
||||
int newHeight = (int) (height * ratio);
|
||||
|
||||
Image scaledImage = img.getScaledInstance(newWidth, newHeight, Image.SCALE_SMOOTH);
|
||||
BufferedImage resized = new BufferedImage(newWidth, newHeight, BufferedImage.TYPE_INT_RGB);
|
||||
Graphics2D g2d = resized.createGraphics();
|
||||
|
||||
try {
|
||||
// 使用高质量插值算法
|
||||
g2d.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BICUBIC);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
|
||||
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
|
||||
g2d.drawImage(scaledImage, 0, 0, null);
|
||||
} finally {
|
||||
g2d.dispose();
|
||||
}
|
||||
|
||||
log.info("图片已调整尺寸: {}x{} -> {}x{}", width, height, newWidth, newHeight);
|
||||
return resized;
|
||||
} else {
|
||||
log.info("图片尺寸合适,无需调整: {}x{}", width, height);
|
||||
return img;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] compressToJpeg(BufferedImage img) throws IOException {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
|
||||
// 获取 JPEG 写入器
|
||||
Iterator<ImageWriter> writers = ImageIO.getImageWritersByFormatName("jpg");
|
||||
if (!writers.hasNext()) {
|
||||
throw new IOException("找不到 JPEG 写入器 (´;︵;`) ");
|
||||
}
|
||||
|
||||
ImageWriter writer = writers.next();
|
||||
try (ImageOutputStream ios = ImageIO.createImageOutputStream(baos)) {
|
||||
writer.setOutput(ios);
|
||||
|
||||
ImageWriteParam param = writer.getDefaultWriteParam();
|
||||
if (param.canWriteCompressed()) {
|
||||
param.setCompressionMode(ImageWriteParam.MODE_EXPLICIT);
|
||||
param.setCompressionType("JPEG");
|
||||
param.setCompressionQuality(JPEG_QUALITY);
|
||||
}
|
||||
|
||||
writer.write(null, new IIOImage(img, null, null), param);
|
||||
} finally {
|
||||
writer.dispose();
|
||||
}
|
||||
|
||||
return baos.toByteArray();
|
||||
}
|
||||
}
|
||||
@ -66,6 +66,11 @@ goofish:
|
||||
hostUrl: https://h5api.m.goofish.com/h5/
|
||||
appKey: 34839810
|
||||
|
||||
assistant:
|
||||
static:
|
||||
uploads:
|
||||
images : static/uploads/images/
|
||||
|
||||
browser:
|
||||
# chrome 浏览器软件安装位置
|
||||
location: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
||||
|
||||
Loading…
Reference in New Issue
Block a user