Merge branch 'master' of gitee.com:youlaiorg/youlai-boot
This commit is contained in:
14
README.md
14
README.md
@@ -45,17 +45,22 @@ youlai-boot
|
||||
├── mysql5 # MySQL5 脚本
|
||||
├── mysql8 # MySQL8 脚本
|
||||
├── src # 源码目录
|
||||
├── base # 核心模块
|
||||
├── mybatisplus # Mybatis-Plus 配置和插件
|
||||
├── security # Spring Security 安全配置和扩展
|
||||
├── common # 公共模块
|
||||
├── config # 自动装配配置
|
||||
├── CorsConfig # 跨域共享配置
|
||||
├── RedisConfig # Redis 配置
|
||||
├── MybatisConfig # Mybatis 自动装配配置
|
||||
├── RedisCacheConfig # Redis 缓存自动装配配置
|
||||
├── RedisConfig # Redis 自动装配配置
|
||||
├── SecurityConfig # Spring Security 自动装配配置
|
||||
├── SwaggerConfig # API 接口文档配置
|
||||
├── WebMvcConfig # WebMvc 配置
|
||||
├── WebSocketConfig # WebSocket 自动装配配置
|
||||
├── XxlJobConfig # XXL-JOB 自动装配配置
|
||||
├── controller # 控制层
|
||||
├── converter # MapStruct转换器
|
||||
├── core # 核心模块
|
||||
├── security # Spring Security 安全配置和扩展
|
||||
├── mybatis # Mybatis-Plus 配置和插件
|
||||
├── filter # 过滤器
|
||||
├── RequestLogFilter # 请求日志过滤器
|
||||
├── VerifyCodeFilter # 验证码过滤器
|
||||
@@ -68,6 +73,7 @@ youlai-boot
|
||||
├── vo # 视图对象
|
||||
├── mapper # 数据库访问层
|
||||
├── plugin # 插件(可选)
|
||||
├── captcha # 验证码插件,用于生成验证码
|
||||
├── dupsubmit # 防重提交插件,用于防止表单重复提交
|
||||
├── easyexcel # EasyExcel 插件,Excel 文件的读写
|
||||
├── rabbitmq # RabbitMQ 插件,消息队列交互
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.common.constant.SystemConstants;
|
||||
import com.youlai.system.base.security.model.SysUserDetails;
|
||||
import com.youlai.system.core.security.model.SysUserDetails;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package com.youlai.system.base.mybatisplus.config;
|
||||
package com.youlai.system.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.core.config.GlobalConfig;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.DataPermissionInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import com.youlai.system.base.mybatisplus.handler.MyDataPermissionHandler;
|
||||
import com.youlai.system.base.mybatisplus.handler.MyMetaObjectHandler;
|
||||
import com.youlai.system.core.mybatis.handler.MyDataPermissionHandler;
|
||||
import com.youlai.system.core.mybatis.handler.MyMetaObjectHandler;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
@@ -19,7 +19,7 @@ import org.springframework.transaction.annotation.EnableTransactionManagement;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableTransactionManagement
|
||||
public class MybatisPlusConfig {
|
||||
public class MybatisConfig {
|
||||
|
||||
/**
|
||||
* 分页插件和数据权限插件
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.youlai.system.base.security.config;
|
||||
package com.youlai.system.config;
|
||||
|
||||
import com.youlai.system.common.constant.SecurityConstants;
|
||||
import com.youlai.system.base.security.exception.MyAccessDeniedHandler;
|
||||
import com.youlai.system.base.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.youlai.system.base.security.jwt.JwtTokenFilter;
|
||||
import com.youlai.system.core.security.exception.MyAccessDeniedHandler;
|
||||
import com.youlai.system.core.security.exception.MyAuthenticationEntryPoint;
|
||||
import com.youlai.system.core.security.jwt.JwtTokenFilter;
|
||||
import com.youlai.system.filter.VerifyCodeFilter;
|
||||
import com.youlai.system.base.security.jwt.JwtTokenProvider;
|
||||
import com.youlai.system.core.security.jwt.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -5,9 +5,12 @@ import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import io.swagger.v3.oas.models.security.SecurityRequirement;
|
||||
import io.swagger.v3.oas.models.security.SecurityScheme;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springdoc.core.customizers.GlobalOpenApiCustomizer;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
|
||||
/**
|
||||
@@ -19,19 +22,26 @@ import org.springframework.http.HttpHeaders;
|
||||
* @since 2023/2/17
|
||||
*/
|
||||
@Configuration
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
public class SwaggerConfig {
|
||||
|
||||
private final Environment environment;
|
||||
|
||||
/**
|
||||
* 接口信息
|
||||
*/
|
||||
@Bean
|
||||
public OpenAPI openApi() {
|
||||
|
||||
String appVersion = environment.getProperty("project.version", "1.0.0");
|
||||
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("系统接口文档")
|
||||
.version("2.4.0")
|
||||
.version(appVersion)
|
||||
)
|
||||
// 全局安全校验项,也可以在对应的controller上加注解SecurityRequirement
|
||||
// 配置全局鉴权参数-Authorize
|
||||
.components(new Components()
|
||||
.addSecuritySchemes(HttpHeaders.AUTHORIZATION,
|
||||
new SecurityScheme()
|
||||
@@ -41,17 +51,34 @@ public class SwaggerConfig {
|
||||
.scheme("Bearer")
|
||||
.bearerFormat("JWT")
|
||||
)
|
||||
)
|
||||
.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION)) ;
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 全局自定义扩展
|
||||
* <p>
|
||||
* 在OpenAPI规范中,Operation 是一个表示 API 端点(Endpoint)或操作的对象。
|
||||
* 每个路径(Path)对象可以包含一个或多个 Operation 对象,用于描述与该路径相关联的不同 HTTP 方法(例如 GET、POST、PUT 等)。
|
||||
*/
|
||||
@Bean
|
||||
public GlobalOpenApiCustomizer globalOpenApiCustomizer() {
|
||||
return openApi -> openApi.getPaths().values()
|
||||
.stream()
|
||||
.flatMap(pathItem -> pathItem.readOperations().stream())
|
||||
.forEach(operation -> operation.security(openApi.getSecurity()));
|
||||
return openApi -> {
|
||||
// 全局添加鉴权参数
|
||||
if (openApi.getPaths() != null) {
|
||||
openApi.getPaths().forEach((s, pathItem) -> {
|
||||
// 登录接口/验证码不需要添加鉴权参数
|
||||
if (s.equals("/api/v1/auth/login") || s.equals("/api/v1/auth/captcha")) {
|
||||
return;
|
||||
}
|
||||
// 接口添加鉴权参数
|
||||
pathItem.readOperations()
|
||||
.forEach(operation ->
|
||||
operation.addSecurityItem(new SecurityRequirement().addList(HttpHeaders.AUTHORIZATION))
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
104
src/main/java/com/youlai/system/config/WebSocketConfig.java
Normal file
104
src/main/java/com/youlai/system/config/WebSocketConfig.java
Normal file
@@ -0,0 +1,104 @@
|
||||
package com.youlai.system.config;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.core.security.jwt.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker // 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
/**
|
||||
* 注册一个端点,客户端通过这个端点进行连接
|
||||
*/
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/ws") // 注册了一个 /ws 的端点
|
||||
.setAllowedOriginPatterns("*") // 允许跨域的 WebSocket 连接
|
||||
.withSockJS(); // 启用 SockJS (浏览器不支持WebSocket,SockJS 将会提供兼容性支持)
|
||||
registry.addEndpoint("/ws-app").setAllowedOriginPatterns("*"); // 注册了一个 /ws-app 的端点,支持 uni-app 的 ws 连接协议
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 客户端发送消息的请求前缀
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
|
||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
|
||||
// 服务端通知客户端的前缀,可以不设置,默认为user
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置客户端入站通道拦截器
|
||||
* <p>
|
||||
* 添加 ChannelInterceptor 拦截器,用于在消息发送前,从请求头中获取 token 并解析出用户信息(username),用于点对点发送消息给指定用户
|
||||
*
|
||||
* @param registration 通道注册器
|
||||
*/
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.interceptors(new ChannelInterceptor() {
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
// 如果是连接请求(CONNECT 命令),从请求头中取出 token 并设置到认证信息中
|
||||
if (accessor != null && StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
// 从连接头中提取授权令牌
|
||||
String bearerToken = accessor.getFirstNativeHeader(HttpHeaders.AUTHORIZATION);
|
||||
|
||||
// 验证令牌格式并提取用户信息
|
||||
if (StrUtil.isNotBlank(bearerToken) && bearerToken.startsWith("Bearer ")) {
|
||||
try {
|
||||
// 移除 "Bearer " 前缀,从令牌中提取用户信息(username), 并设置到认证信息中
|
||||
String tokenWithoutPrefix = bearerToken.substring(7);
|
||||
String username = jwtTokenProvider.getUsername(tokenWithoutPrefix);
|
||||
|
||||
if (StrUtil.isNotBlank(username)) {
|
||||
accessor.setUser(() -> username);
|
||||
return message;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("Failed to process authentication token.", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 不是连接请求,直接放行
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.plugin.xxljob;
|
||||
package com.youlai.system.config;
|
||||
|
||||
import com.xxl.job.core.executor.impl.XxlJobSpringExecutor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.system.controller;
|
||||
|
||||
import com.youlai.system.model.dto.SocketMessage;
|
||||
import com.youlai.system.model.dto.ChatMessage;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.messaging.handler.annotation.DestinationVariable;
|
||||
@@ -48,11 +48,14 @@ public class WebsocketController {
|
||||
* @param message 消息内容
|
||||
*/
|
||||
@MessageMapping("/sendToUser/{username}")
|
||||
//@SendToUser(value = "/queue/greeting")
|
||||
public void sendToUser(Principal principal, @DestinationVariable String username, String message) {
|
||||
log.info("sender:{};receiver:{}", principal.getName(), username);
|
||||
messagingTemplate.convertAndSendToUser(username, "/queue/greeting", new SocketMessage(principal.getName(), message));
|
||||
/// return "Hello, " + message;
|
||||
|
||||
String sender = principal.getName(); // 发送人
|
||||
String receiver = username; // 接收人
|
||||
|
||||
log.info("发送人:{}; 接收人:{}", sender, receiver);
|
||||
// 发送消息给指定用户,拼接后路径 /user/{receiver}/queue/greeting
|
||||
messagingTemplate.convertAndSendToUser(receiver, "/queue/greeting", new ChatMessage(sender, message));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.mybatisplus.annotation;
|
||||
package com.youlai.system.core.mybatis.annotation;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.youlai.system.base.mybatisplus.handler;
|
||||
package com.youlai.system.core.mybatis.handler;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.baomidou.mybatisplus.core.toolkit.StringPool;
|
||||
import com.baomidou.mybatisplus.extension.plugins.handler.DataPermissionHandler;
|
||||
import com.youlai.system.base.mybatisplus.annotation.DataPermission;
|
||||
import com.youlai.system.core.mybatis.annotation.DataPermission;
|
||||
import com.youlai.system.common.base.IBaseEnum;
|
||||
import com.youlai.system.common.enums.DataScopeEnum;
|
||||
import com.youlai.system.common.util.SecurityUtils;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.mybatisplus.handler;
|
||||
package com.youlai.system.core.mybatis.handler;
|
||||
|
||||
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||
import org.apache.ibatis.reflection.MetaObject;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.security.exception;
|
||||
package com.youlai.system.core.security.exception;
|
||||
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.security.exception;
|
||||
package com.youlai.system.core.security.exception;
|
||||
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.security.jwt;
|
||||
package com.youlai.system.core.security.jwt;
|
||||
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.util.ResponseUtils;
|
||||
@@ -1,8 +1,8 @@
|
||||
package com.youlai.system.base.security.jwt;
|
||||
package com.youlai.system.core.security.jwt;
|
||||
|
||||
import cn.hutool.core.convert.Convert;
|
||||
import com.youlai.system.common.constant.JwtClaimConstants;
|
||||
import com.youlai.system.base.security.model.SysUserDetails;
|
||||
import com.youlai.system.core.security.model.SysUserDetails;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.security.model;
|
||||
package com.youlai.system.core.security.model;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
@@ -1,4 +1,4 @@
|
||||
package com.youlai.system.base.security.service;
|
||||
package com.youlai.system.core.security.service;
|
||||
|
||||
import cn.hutool.core.collection.CollectionUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
@@ -1,6 +1,6 @@
|
||||
package com.youlai.system.base.security.service;
|
||||
package com.youlai.system.core.security.service;
|
||||
|
||||
import com.youlai.system.base.security.model.SysUserDetails;
|
||||
import com.youlai.system.core.security.model.SysUserDetails;
|
||||
import com.youlai.system.model.dto.UserAuthInfo;
|
||||
import com.youlai.system.service.SysUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
@@ -3,7 +3,7 @@ package com.youlai.system.mapper;
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Constants;
|
||||
import com.youlai.system.base.mybatisplus.annotation.DataPermission;
|
||||
import com.youlai.system.core.mybatis.annotation.DataPermission;
|
||||
import com.youlai.system.model.entity.SysDept;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
@@ -2,7 +2,7 @@ package com.youlai.system.mapper;
|
||||
|
||||
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.youlai.system.base.mybatisplus.annotation.DataPermission;
|
||||
import com.youlai.system.core.mybatis.annotation.DataPermission;
|
||||
import com.youlai.system.model.bo.UserBO;
|
||||
import com.youlai.system.model.entity.SysUser;
|
||||
import com.youlai.system.model.dto.UserAuthInfo;
|
||||
|
||||
@@ -10,7 +10,7 @@ import lombok.NoArgsConstructor;
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class SocketMessage {
|
||||
public class ChatMessage {
|
||||
|
||||
/**
|
||||
* 发送者
|
||||
@@ -0,0 +1,55 @@
|
||||
package com.youlai.system.plugin.captcha;
|
||||
|
||||
import cn.hutool.captcha.*;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.awt.*;
|
||||
|
||||
/**
|
||||
* 验证码自动装配配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaConfig {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 验证码文字生成器
|
||||
*
|
||||
* @return CodeGenerator
|
||||
*/
|
||||
@Bean
|
||||
public CodeGenerator codeGenerator() {
|
||||
String codeType = captchaProperties.getCode().getType();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
if ("math".equalsIgnoreCase(codeType)) {
|
||||
return new MathGenerator(codeLength);
|
||||
} else if ("random".equalsIgnoreCase(codeType)) {
|
||||
return new RandomGenerator(codeLength);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha generator type: " + codeType);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码字体
|
||||
*/
|
||||
@Bean
|
||||
public Font captchaFont() {
|
||||
String fontName = captchaProperties.getFont().getName();
|
||||
int fontSize = captchaProperties.getFont().getSize();
|
||||
int fontWight = captchaProperties.getFont().getWeight();
|
||||
return new Font(fontName, fontWight, fontSize);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
package com.youlai.system.plugin.captcha;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CircleCaptcha;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.captcha.generator.MathGenerator;
|
||||
import cn.hutool.captcha.generator.RandomGenerator;
|
||||
import com.youlai.system.model.dto.CaptchaResult;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* 验证码自动装配配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/11/24
|
||||
*/
|
||||
@Configuration
|
||||
public class CaptchaGenerator {
|
||||
|
||||
@Autowired
|
||||
private CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
* 验证码文字生成器
|
||||
*
|
||||
* @return CodeGenerator
|
||||
*/
|
||||
@Bean
|
||||
public CodeGenerator codeGenerator() {
|
||||
String codeType = captchaProperties.getCode().getType();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
if ("math".equalsIgnoreCase(codeType)) {
|
||||
return new MathGenerator(codeLength);
|
||||
} else if ("random".equalsIgnoreCase(codeType)) {
|
||||
return new RandomGenerator(codeLength);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha generator type: " + codeType);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 生成验证码
|
||||
*
|
||||
* @return CaptchaModel 验证码
|
||||
*/
|
||||
public CaptchaModel generate() {
|
||||
AbstractCaptcha captcha = getCaptcha();
|
||||
captcha.createCode();
|
||||
return new CaptchaModel(captcha.getCode(), captcha.getImageBase64Data());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码类
|
||||
*
|
||||
* @return AbstractCaptcha
|
||||
*/
|
||||
public AbstractCaptcha getCaptcha() {
|
||||
AbstractCaptcha captcha = null;
|
||||
|
||||
String type = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
int height = captchaProperties.getHeight();
|
||||
int interfereCount = captchaProperties.getInterfereCount();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
|
||||
|
||||
if ("circle".equalsIgnoreCase(type)) {
|
||||
captcha = new CircleCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if ("gif".equalsIgnoreCase(type)) {
|
||||
return null;
|
||||
} else if ("line".equalsIgnoreCase(type)) {
|
||||
return null;
|
||||
} else if ("shear".equalsIgnoreCase(type)) {
|
||||
return null;
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha type: " + type);
|
||||
}
|
||||
|
||||
captcha.setGenerator(codeGenerator());
|
||||
return captcha;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
package com.youlai.system.plugin.captcha;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 验证码对象
|
||||
*/
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public class CaptchaModel {
|
||||
/**
|
||||
* 验证码编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 验证码图片Base64
|
||||
*/
|
||||
private String base64;
|
||||
|
||||
}
|
||||
@@ -34,6 +34,11 @@ public class CaptchaProperties {
|
||||
*/
|
||||
private int interfereCount;
|
||||
|
||||
/**
|
||||
* 文本透明度
|
||||
*/
|
||||
private Float textAlpha;
|
||||
|
||||
/**
|
||||
* 验证码过期时间,单位:秒
|
||||
*/
|
||||
|
||||
@@ -4,7 +4,7 @@ import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.plugin.dupsubmit.annotation.PreventDuplicateSubmit;
|
||||
import com.youlai.system.common.result.ResultCode;
|
||||
import com.youlai.system.common.exception.BusinessException;
|
||||
import com.youlai.system.base.security.jwt.JwtTokenProvider;
|
||||
import com.youlai.system.core.security.jwt.JwtTokenProvider;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@@ -1,63 +0,0 @@
|
||||
package com.youlai.system.plugin.websocket;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket 配置
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker // 启用WebSocket消息代理功能和配置STOMP协议,实现实时双向通信和消息传递
|
||||
@RequiredArgsConstructor
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
private final WebsocketChannelInterceptor websocketChannelInterceptor;
|
||||
|
||||
/**
|
||||
* 注册一个端点,客户端通过这个端点进行连接
|
||||
*/
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry
|
||||
.addEndpoint("/ws") // 注册了一个 /ws 的端点
|
||||
.setAllowedOriginPatterns("*") // 允许跨域的 WebSocket 连接
|
||||
.withSockJS(); // 启用 SockJS (浏览器不支持WebSocket,SockJS 将会提供兼容性支持)
|
||||
registry.addEndpoint("/ws-app").setAllowedOriginPatterns("*"); // 注册了一个 /ws-app 的端点,支持 uni-app 的 ws 连接协议
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置消息代理
|
||||
*/
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
// 客户端发送消息的请求前缀
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
|
||||
// 客户端订阅消息的请求前缀,topic一般用于广播推送,queue用于点对点推送
|
||||
registry.enableSimpleBroker("/topic", "/queue");
|
||||
|
||||
// 服务端通知客户端的前缀,可以不设置,默认为user
|
||||
registry.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 配置客户端入站通道拦截器
|
||||
*
|
||||
* @param registration 通道注册器
|
||||
*/
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
registration.interceptors(websocketChannelInterceptor);
|
||||
}
|
||||
}
|
||||
@@ -1,68 +0,0 @@
|
||||
package com.youlai.system.plugin.websocket;
|
||||
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.context.event.EventListener;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.messaging.SessionConnectedEvent;
|
||||
import org.springframework.web.socket.messaging.SessionDisconnectEvent;
|
||||
import org.springframework.web.socket.messaging.SessionSubscribeEvent;
|
||||
import org.springframework.web.socket.messaging.SessionUnsubscribeEvent;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Websocket 客户端事件监听器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2023/10/10
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class WebSocketEventListener {
|
||||
|
||||
/**
|
||||
* 监听客户端连接事件
|
||||
*
|
||||
* @param event 连接事件对象
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketConnectListener(SessionConnectedEvent event) {
|
||||
Principal user = event.getUser();
|
||||
|
||||
log.info("客户端连接成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端断开连接事件
|
||||
*
|
||||
* @param event 断开连接事件对象
|
||||
*/
|
||||
@EventListener
|
||||
public void handleWebSocketDisconnectListener(SessionDisconnectEvent event) {
|
||||
log.info("客户端断开连接");
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 监听客户端订阅事件
|
||||
*
|
||||
* @param event 订阅事件对象
|
||||
*/
|
||||
@EventListener
|
||||
public void handleSubscription(SessionSubscribeEvent event) {
|
||||
log.info("客户端订阅:{}", JSONUtil.toJsonStr(event.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 监听客户端取消订阅事件
|
||||
*
|
||||
* @param event 取消订阅事件对象
|
||||
*/
|
||||
@EventListener
|
||||
public void handleUnSubscription(SessionUnsubscribeEvent event) {
|
||||
log.info("客户端取消订阅:{}", JSONUtil.toJsonStr(event.getMessage()));
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package com.youlai.system.plugin.websocket;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.base.security.jwt.JwtTokenProvider;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.messaging.Message;
|
||||
import org.springframework.messaging.MessageChannel;
|
||||
import org.springframework.messaging.simp.stomp.StompCommand;
|
||||
import org.springframework.messaging.simp.stomp.StompHeaderAccessor;
|
||||
import org.springframework.messaging.support.ChannelInterceptor;
|
||||
import org.springframework.messaging.support.MessageHeaderAccessor;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* Websocket 连接认证拦截器
|
||||
*
|
||||
* @author haoxr
|
||||
* @since 2.4.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class WebsocketChannelInterceptor implements ChannelInterceptor {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
/**
|
||||
* 连接前监听
|
||||
*
|
||||
* @param message 消息
|
||||
* @param channel 通道
|
||||
* @return
|
||||
*/
|
||||
@Override
|
||||
public Message<?> preSend(Message<?> message, MessageChannel channel) {
|
||||
StompHeaderAccessor accessor = MessageHeaderAccessor.getAccessor(message, StompHeaderAccessor.class);
|
||||
assert accessor != null;
|
||||
|
||||
if (StompCommand.CONNECT.equals(accessor.getCommand())) {
|
||||
String bearerToken = accessor.getFirstNativeHeader("Authorization");
|
||||
if (StrUtil.isNotBlank(bearerToken)) {
|
||||
bearerToken = bearerToken.substring(7); // remove "Bearer "
|
||||
String username = jwtTokenProvider.getUsername(bearerToken);
|
||||
if (StrUtil.isNotBlank(username)) {
|
||||
Principal principal = () -> username;
|
||||
accessor.setUser(principal);
|
||||
return message;
|
||||
}
|
||||
}
|
||||
}
|
||||
return ChannelInterceptor.super.preSend(message, channel);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,14 +1,14 @@
|
||||
package com.youlai.system.service.impl;
|
||||
|
||||
import cn.hutool.captcha.AbstractCaptcha;
|
||||
import cn.hutool.captcha.CaptchaUtil;
|
||||
import cn.hutool.captcha.generator.CodeGenerator;
|
||||
import cn.hutool.core.util.IdUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.youlai.system.common.constant.CacheConstants;
|
||||
import com.youlai.system.base.security.jwt.JwtTokenProvider;
|
||||
import com.youlai.system.core.security.jwt.JwtTokenProvider;
|
||||
import com.youlai.system.model.dto.CaptchaResult;
|
||||
import com.youlai.system.model.dto.LoginResult;
|
||||
import com.youlai.system.plugin.captcha.CaptchaGenerator;
|
||||
import com.youlai.system.plugin.captcha.CaptchaModel;
|
||||
import com.youlai.system.plugin.captcha.CaptchaProperties;
|
||||
import com.youlai.system.service.AuthService;
|
||||
import io.jsonwebtoken.Claims;
|
||||
@@ -23,6 +23,7 @@ import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import java.awt.*;
|
||||
import java.util.Date;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@@ -39,7 +40,8 @@ public class AuthServiceImpl implements AuthService {
|
||||
private final AuthenticationManager authenticationManager;
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final CaptchaGenerator captchaGenerator;
|
||||
private final CodeGenerator codeGenerator;
|
||||
private final Font captchaFont;
|
||||
private final CaptchaProperties captchaProperties;
|
||||
|
||||
/**
|
||||
@@ -89,16 +91,40 @@ public class AuthServiceImpl implements AuthService {
|
||||
*/
|
||||
@Override
|
||||
public CaptchaResult getCaptcha() {
|
||||
CaptchaModel captchaModel = captchaGenerator.generate();
|
||||
|
||||
String type = captchaProperties.getType();
|
||||
int width = captchaProperties.getWidth();
|
||||
int height = captchaProperties.getHeight();
|
||||
int interfereCount = captchaProperties.getInterfereCount();
|
||||
int codeLength = captchaProperties.getCode().getLength();
|
||||
|
||||
AbstractCaptcha captcha;
|
||||
if ("circle".equalsIgnoreCase(type)) {
|
||||
captcha = CaptchaUtil.createCircleCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if ("gif".equalsIgnoreCase(type)) {
|
||||
captcha = CaptchaUtil.createGifCaptcha(width, height, codeLength);
|
||||
} else if ("line".equalsIgnoreCase(type)) {
|
||||
captcha = CaptchaUtil.createLineCaptcha(width, height, codeLength, interfereCount);
|
||||
} else if ("shear".equalsIgnoreCase(type)) {
|
||||
captcha = CaptchaUtil.createShearCaptcha(width, height, codeLength, interfereCount);
|
||||
} else {
|
||||
throw new IllegalArgumentException("Invalid captcha type: " + type);
|
||||
}
|
||||
captcha.setGenerator(codeGenerator);
|
||||
captcha.setTextAlpha(captchaProperties.getTextAlpha());
|
||||
captcha.setFont(captchaFont);
|
||||
|
||||
String captchaCode = captcha.getCode();
|
||||
String imageBase64Data = captcha.getImageBase64Data();
|
||||
|
||||
// 验证码文本缓存至Redis,用于登录校验
|
||||
String captchaKey = IdUtil.fastSimpleUUID();
|
||||
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey, captchaModel.getCode(),
|
||||
redisTemplate.opsForValue().set(CacheConstants.CAPTCHA_CODE_PREFIX + captchaKey,captchaCode,
|
||||
captchaProperties.getExpireSeconds(), TimeUnit.SECONDS);
|
||||
|
||||
return CaptchaResult.builder()
|
||||
.captchaKey(captchaKey)
|
||||
.captchaBase64(captchaModel.getBase64())
|
||||
.captchaBase64(imageBase64Data)
|
||||
.build();
|
||||
}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.youlai.system.common.constant.SystemConstants;
|
||||
import com.youlai.system.common.model.Option;
|
||||
import com.youlai.system.converter.RoleConverter;
|
||||
import com.youlai.system.base.security.service.PermissionService;
|
||||
import com.youlai.system.core.security.service.PermissionService;
|
||||
import com.youlai.system.mapper.SysRoleMapper;
|
||||
import com.youlai.system.model.entity.SysRole;
|
||||
import com.youlai.system.model.entity.SysRoleMenu;
|
||||
|
||||
@@ -136,10 +136,12 @@ captcha:
|
||||
# 验证码高度
|
||||
height: 40
|
||||
# 验证码干扰元素个数
|
||||
interfere-count: 3
|
||||
interfere-count: 4
|
||||
# 文本透明度(0.0-1.0)
|
||||
text-alpha: 0.8
|
||||
# 验证码字符配置
|
||||
code:
|
||||
# 验证码字符类型 math-算术 |random-随机字符
|
||||
# 验证码字符类型 math-算术|random-随机字符
|
||||
type: math
|
||||
# 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数
|
||||
length: 1
|
||||
@@ -150,7 +152,7 @@ captcha:
|
||||
# 字体样式 0-普通|1-粗体|2-斜体
|
||||
weight: 1
|
||||
# 字体大小
|
||||
size: 18
|
||||
size: 30
|
||||
# 验证码有效期(秒)
|
||||
expire-seconds: 120
|
||||
|
||||
|
||||
@@ -136,10 +136,12 @@ captcha:
|
||||
# 验证码高度
|
||||
height: 40
|
||||
# 验证码干扰元素个数
|
||||
interfere-count: 3
|
||||
interfere-count: 4
|
||||
# 文本透明度(0.0-1.0)
|
||||
text-alpha: 0.8
|
||||
# 验证码字符配置
|
||||
code:
|
||||
# 验证码字符类型 math-算术 |random-随机字符
|
||||
# 验证码字符类型 math-算术|random-随机字符
|
||||
type: math
|
||||
# 验证码字符长度,type=算术时,表示运算位数(1:个位数运算 2:十位数运算);type=随机字符时,表示字符个数
|
||||
length: 1
|
||||
@@ -150,7 +152,7 @@ captcha:
|
||||
# 字体样式 0-普通|1-粗体|2-斜体
|
||||
weight: 1
|
||||
# 字体大小
|
||||
size: 18
|
||||
size: 30
|
||||
# 验证码有效期(秒)
|
||||
expire-seconds: 120
|
||||
|
||||
|
||||
Reference in New Issue
Block a user