Merge branch 'master' of gitee.com:youlaiorg/youlai-boot

This commit is contained in:
hxr
2023-12-16 14:16:29 +08:00
33 changed files with 289 additions and 356 deletions

View File

@@ -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 插件,消息队列交互

View File

@@ -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;

View File

@@ -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 {
/**
* 分页插件和数据权限插件

View File

@@ -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;

View File

@@ -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))
);
});
}
};
}
}

View 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 (浏览器不支持WebSocketSockJS 将会提供兼容性支持)
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);
}
});
}
}

View File

@@ -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;

View File

@@ -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));
}
}

View File

@@ -1,4 +1,4 @@
package com.youlai.system.base.mybatisplus.annotation;
package com.youlai.system.core.mybatis.annotation;
import java.lang.annotation.*;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -10,7 +10,7 @@ import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class SocketMessage {
public class ChatMessage {
/**
* 发送者

View File

@@ -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);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -34,6 +34,11 @@ public class CaptchaProperties {
*/
private int interfereCount;
/**
* 文本透明度
*/
private Float textAlpha;
/**
* 验证码过期时间,单位:秒
*/

View File

@@ -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;

View File

@@ -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 (浏览器不支持WebSocketSockJS 将会提供兼容性支持)
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);
}
}

View File

@@ -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()));
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}

View File

@@ -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;

View File

@@ -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

View File

@@ -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