Compare commits
6 Commits
33fbee9a00
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 5c2bec0bcc | |||
| 4f03889412 | |||
| 31599928a9 | |||
|
|
eb75bee40f | ||
|
|
ce5d7686c2 | ||
|
|
a8403613fa |
BIN
docs/images/qr/wechat-app.jpg
Normal file
BIN
docs/images/qr/wechat-app.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 43 KiB |
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
||||
|
||||
<groupId>com.youlai</groupId>
|
||||
<artifactId>youlai-boot</artifactId>
|
||||
<version>4.3.0</version>
|
||||
<version>4.3.1</version>
|
||||
<description>基于 Java 17 + SpringBoot 4 + Spring Security 构建的权限管理系统。</description>
|
||||
|
||||
<parent>
|
||||
|
||||
@@ -22,7 +22,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@Tag(name = "01.认证中心")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/auth/app")
|
||||
@RequestMapping("/api/v1/app/auth/")
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class AppAuthController {
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.youlai.boot.app.controller;
|
||||
|
||||
import com.youlai.boot.app.model.vo.AppFileInfo;
|
||||
import com.youlai.boot.app.service.AppFileService;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.SneakyThrows;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 文件控制层
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2022/10/16
|
||||
*/
|
||||
@Tag(name = "10.文件接口")
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/app/files")
|
||||
@RequiredArgsConstructor
|
||||
public class AppFileController {
|
||||
|
||||
private final AppFileService fileService;
|
||||
|
||||
@PostMapping
|
||||
@Operation(summary = "文件上传")
|
||||
public Result<AppFileInfo> uploadFile(
|
||||
@Parameter(
|
||||
name = "file",
|
||||
description = "表单文件对象",
|
||||
required = true,
|
||||
in = ParameterIn.DEFAULT,
|
||||
schema = @Schema(name = "file", format = "binary")
|
||||
)
|
||||
@RequestPart(value = "file") MultipartFile file
|
||||
) {
|
||||
Assert.isTrue(!file.isEmpty(), "上传文件不能为空文件");
|
||||
AppFileInfo fileInfo = fileService.uploadFile(file);
|
||||
return Result.success(fileInfo);
|
||||
}
|
||||
|
||||
@DeleteMapping
|
||||
@Operation(summary = "文件删除")
|
||||
@SneakyThrows
|
||||
public Result<?> deleteFile(
|
||||
@Parameter(description = "文件路径") @RequestParam String filePath
|
||||
) {
|
||||
boolean result = fileService.deleteFile(filePath);
|
||||
return Result.judge(result);
|
||||
}
|
||||
}
|
||||
23
src/main/java/com/youlai/boot/app/model/vo/AppFileInfo.java
Normal file
23
src/main/java/com/youlai/boot/app/model/vo/AppFileInfo.java
Normal file
@@ -0,0 +1,23 @@
|
||||
package com.youlai.boot.app.model.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
|
||||
/**
|
||||
* 文件信息对象
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 1.0.0
|
||||
*/
|
||||
@Schema(description = "文件对象")
|
||||
@Data
|
||||
public class AppFileInfo {
|
||||
|
||||
@Schema(description = "文件名称")
|
||||
private String name;
|
||||
|
||||
@Schema(description = "文件URL")
|
||||
private String url;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.youlai.boot.app.service;
|
||||
|
||||
import com.youlai.boot.app.model.vo.AppFileInfo;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
/**
|
||||
* 对象存储服务接口层
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2022/11/19
|
||||
*/
|
||||
public interface AppFileService {
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
* @param file 表单文件对象
|
||||
* @return 文件信息
|
||||
*/
|
||||
AppFileInfo uploadFile(MultipartFile file);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param filePath 文件完整URL
|
||||
* @return 删除结果
|
||||
*/
|
||||
boolean deleteFile(String filePath);
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.youlai.boot.app.service.impl;
|
||||
|
||||
import cn.hutool.core.date.DatePattern;
|
||||
import cn.hutool.core.date.DateUtil;
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import com.youlai.boot.app.model.vo.AppFileInfo;
|
||||
import com.youlai.boot.app.service.AppFileService;
|
||||
import com.youlai.boot.file.service.FileService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 本地存储服务类
|
||||
*
|
||||
* @author Theo
|
||||
* @since 2024-12-09 17:11
|
||||
*/
|
||||
@Data
|
||||
@Slf4j
|
||||
@Component
|
||||
//@ConditionalOnProperty(value = "oss.type", havingValue = "local")
|
||||
//@ConfigurationProperties(prefix = "oss.local")
|
||||
@RequiredArgsConstructor
|
||||
public class LocalAppFileService implements AppFileService {
|
||||
|
||||
@Value("${oss.local.storage-path}")
|
||||
private String storagePath;
|
||||
|
||||
/**
|
||||
* 上传文件方法
|
||||
*
|
||||
* @param file 表单文件对象
|
||||
* @return 文件信息
|
||||
*/
|
||||
@Override
|
||||
public AppFileInfo uploadFile(MultipartFile file) {
|
||||
// 获取文件名
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
// 获取文件后缀
|
||||
String suffix = FileUtil.getSuffix(originalFilename);
|
||||
// 生成uuid
|
||||
String fileName = IdUtil.simpleUUID()+ "." + suffix;;
|
||||
// 生成文件名(日期文件夹)
|
||||
String folder = DateUtil.format(LocalDateTime.now(), DatePattern.PURE_DATE_PATTERN);
|
||||
String filePrefix = storagePath.endsWith(File.separator) ? storagePath : storagePath + File.separator;
|
||||
// try-with-resource 语法糖自动释放流
|
||||
try (InputStream inputStream = file.getInputStream()) {
|
||||
// 上传文件
|
||||
FileUtil.writeFromStream(inputStream, filePrefix + folder + File.separator + fileName);
|
||||
} catch (Exception e) {
|
||||
log.error("文件上传失败", e);
|
||||
throw new RuntimeException("文件上传失败");
|
||||
}
|
||||
// 获取文件访问路径,因为这里是本地存储,所以直接返回文件的相对路径,需要前端自行处理访问前缀
|
||||
String fileUrl = File.separator + folder + File.separator + fileName;
|
||||
AppFileInfo fileInfo = new AppFileInfo();
|
||||
fileInfo.setName(originalFilename);
|
||||
fileInfo.setUrl(fileUrl);
|
||||
return fileInfo;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
* @param filePath 文件完整URL
|
||||
* @return 是否删除成功
|
||||
*/
|
||||
@Override
|
||||
public boolean deleteFile(String filePath) {
|
||||
//判断文件是否为空
|
||||
if (filePath == null || filePath.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
// 判断filepath是否为文件夹
|
||||
if (FileUtil.isDirectory(storagePath + filePath)) {
|
||||
// 禁止删除文件夹
|
||||
return false;
|
||||
}
|
||||
// 删除文件
|
||||
return FileUtil.del(storagePath + filePath);
|
||||
}
|
||||
}
|
||||
@@ -71,7 +71,7 @@ public class SecurityConfig {
|
||||
|
||||
// 移动设备专用接口路径(需要设备签名验证,但不需要用户登录)
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/sn/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/auth/app/**").permitAll();
|
||||
requestMatcherRegistry.requestMatchers("/api/v1/app/**").permitAll();
|
||||
// 其他所有请求需登录后访问
|
||||
requestMatcherRegistry.anyRequest().authenticated();
|
||||
}
|
||||
|
||||
@@ -3,13 +3,13 @@ package com.youlai.boot.framework.security.exception;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
/**
|
||||
* 验证码校验异常
|
||||
* 短信验证码异常
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 2025/3/1
|
||||
*/
|
||||
public class CaptchaValidationException extends AuthenticationException {
|
||||
public CaptchaValidationException(String msg) {
|
||||
public class SmsCaptchaException extends AuthenticationException {
|
||||
public SmsCaptchaException(String msg) {
|
||||
super(msg);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.youlai.boot.framework.security.exception;
|
||||
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import lombok.Getter;
|
||||
|
||||
/**
|
||||
* Token 无效异常(access_token 或 refresh_token 过期/无效)
|
||||
*
|
||||
* @author Ray.Hao
|
||||
* @since 4.3.1
|
||||
*/
|
||||
@Getter
|
||||
public class TokenInvalidException extends RuntimeException {
|
||||
|
||||
private final ResultCode resultCode;
|
||||
|
||||
public TokenInvalidException(ResultCode resultCode) {
|
||||
super(resultCode.getMsg());
|
||||
this.resultCode = resultCode;
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@ package com.youlai.boot.framework.security.provider;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.framework.security.exception.CaptchaValidationException;
|
||||
import com.youlai.boot.framework.security.exception.SmsCaptchaException;
|
||||
import com.youlai.boot.framework.security.model.SmsAuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
import com.youlai.boot.framework.security.model.UserAuthInfo;
|
||||
@@ -62,11 +62,11 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
// 参数校验
|
||||
if (StrUtil.isBlank(mobile)) {
|
||||
log.warn("短信验证码登录失败:手机号为空");
|
||||
throw new CaptchaValidationException("手机号不能为空");
|
||||
throw new SmsCaptchaException("手机号不能为空");
|
||||
}
|
||||
if (StrUtil.isBlank(inputVerifyCode)) {
|
||||
log.warn("短信验证码登录失败:验证码为空,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码不能为空");
|
||||
throw new SmsCaptchaException("验证码不能为空");
|
||||
}
|
||||
|
||||
// 根据手机号获取用户信息
|
||||
@@ -89,12 +89,12 @@ public class SmsAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
if (cachedVerifyCode == null) {
|
||||
log.warn("短信验证码登录失败:验证码已过期,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码已过期,请重新获取");
|
||||
throw new SmsCaptchaException("验证码已过期,请重新获取");
|
||||
}
|
||||
|
||||
if (!StrUtil.equals(inputVerifyCode, cachedVerifyCode)) {
|
||||
log.warn("短信验证码登录失败:验证码错误,手机号={}", mobile);
|
||||
throw new CaptchaValidationException("验证码错误");
|
||||
throw new SmsCaptchaException("验证码错误");
|
||||
}
|
||||
|
||||
// 验证成功后删除验证码,防止重复使用
|
||||
|
||||
@@ -12,9 +12,9 @@ import cn.hutool.jwt.JWTUtil;
|
||||
import com.youlai.boot.common.constant.JwtClaimConstants;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.RoleDataScope;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -300,7 +300,7 @@ public class JwtTokenManager implements TokenManager {
|
||||
public AuthenticationToken refreshToken(String refreshToken) {
|
||||
boolean isValid = validateRefreshToken(refreshToken);
|
||||
if (!isValid) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
Authentication authentication = parseToken(refreshToken);
|
||||
int accessTokenExpiration = securityProperties.getSession().getAccessTokenTimeToLive();
|
||||
|
||||
@@ -5,9 +5,9 @@ import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.boot.common.constant.RedisConstants;
|
||||
import com.youlai.boot.common.constant.SecurityConstants;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.config.SecurityProperties;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import com.youlai.boot.framework.security.model.AuthenticationToken;
|
||||
import com.youlai.boot.framework.security.model.UserSession;
|
||||
import com.youlai.boot.framework.security.model.SysUserDetails;
|
||||
@@ -147,7 +147,7 @@ public class RedisTokenManager implements TokenManager {
|
||||
UserSession userSession = (UserSession) redisTemplate.opsForValue()
|
||||
.get(StrUtil.format(RedisConstants.Auth.REFRESH_TOKEN_USER, refreshToken));
|
||||
if (userSession == null) {
|
||||
throw new BusinessException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
throw new TokenInvalidException(ResultCode.REFRESH_TOKEN_INVALID);
|
||||
}
|
||||
Object oldAccessTokenValue = redisTemplate.opsForValue().get(StrUtil.format(RedisConstants.Auth.USER_ACCESS_TOKEN, userSession.getUserId()));
|
||||
// 删除旧的访问令牌记录
|
||||
|
||||
@@ -5,6 +5,7 @@ import tools.jackson.core.JacksonException;
|
||||
import com.youlai.boot.common.exception.BusinessException;
|
||||
import com.youlai.boot.common.result.Result;
|
||||
import com.youlai.boot.common.result.ResultCode;
|
||||
import com.youlai.boot.framework.security.exception.TokenInvalidException;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.validation.ConstraintViolation;
|
||||
import jakarta.validation.ConstraintViolationException;
|
||||
@@ -222,6 +223,17 @@ public class GlobalExceptionHandler {
|
||||
return Result.failed(ResultCode.INTEGRITY_CONSTRAINT_VIOLATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 Token 无效异常
|
||||
* <p>
|
||||
* 当 access_token 或 refresh_token 过期/无效时,返回 401。
|
||||
*/
|
||||
@ExceptionHandler(TokenInvalidException.class)
|
||||
@ResponseStatus(HttpStatus.UNAUTHORIZED)
|
||||
public <T> Result<T> handleTokenInvalidException(TokenInvalidException e) {
|
||||
return Result.failed(e.getResultCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理业务异常
|
||||
* <p>
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
#else
|
||||
<if test="queryParams.${fieldConfig.fieldName} != null">
|
||||
#end
|
||||
#set ($queryType = ${fieldConfig.queryType}.name())
|
||||
#set ($queryType = $fieldConfig.queryType.name())
|
||||
#if($queryType == "EQ")
|
||||
AND ${fieldConfig.columnName} = #{queryParams.${fieldConfig.fieldName}}
|
||||
#elseif($queryType == "LIKE")
|
||||
|
||||
Reference in New Issue
Block a user