Compare commits

...

12 Commits

Author SHA1 Message Date
zkh
7b346802e0 feat(security): 更新安全配置并升级框架版本
- 将父项目版本从 1.5.7 升级到 1.5.8
- 在 WebSecurityConfig 中注入 SecurityProps 配置
- 修改 CustomAuthenticationEntryPoint 构造函数以接受 securityProps 参数
- 将硬编码的访问令牌持续时间替换为 securityProps 配置值
- 将认证失败响应状态码从 401 更改为 40
- 为 zkh-web 模块添加 spring-boot-starter-aop 依赖
2026-02-04 09:48:51 +08:00
zkh
f152b1e655 feat(security): 配置安全过滤器链支持异步请求
- 添加 DispatcherType.ASYNC 类型匹配以允许异步请求通过
- 添加 Order 注解确保安全过滤器链正确排序
- 导入必要的 jakarta.servlet.DispatcherType 和 org.springframework.core.annotation.Order 依赖
2026-02-04 01:15:08 +08:00
zkh
01d29e6ec3 feat(security): 更新安全配置并升级框架版本
- 将父项目版本从 1.5.7 升级到 1.5.8
- 在 WebSecurityConfig 中注入 SecurityProps 配置
- 修改 CustomAuthenticationEntryPoint 构造函数以接受 securityProps 参数
- 将硬编码的访问令牌持续时间替换为 securityProps 配置值
- 将认证失败响应状态码从 401 更改为 40
- 为 zkh-web 模块添加 spring-boot-starter-aop 依赖
2026-01-21 12:04:00 +08:00
zkh
06b5258824 refactor(error): 移除全局异常处理器中的重复状态码注解
- 移除 MethodArgumentNotValidException 处理器的 @ResponseStatus 注解
- 移除 ConstraintViolationException 处理器的 @ResponseStatus 注解
- 统一通过返回结果对象控制响应状态码
- 简化异常处理逻辑,提高代码一致性
2026-01-17 12:25:28 +08:00
zkh
3d4bec6e96 feat(web): 添加全局异常处理器支持请求参数验证
- 新增 MethodArgumentNotValidException 处理器,用于处理 @RequestBody + @Valid 校验失败
- 新增 ConstraintViolationException 处理器,用于处理 @RequestParam/@PathVariable 校验失败
- 实现了统一的参数验证错误响应格式
- 添加了详细的字段错误信息提取和返回机制
- 集成了日志记录功能以跟踪验证失败情况
- 更新了项目版本从 1.5.6 到 1.5.7
2026-01-17 12:17:55 +08:00
zkh
eb82090586 feat(common): 添加 R2dbcBaseEntity 基础实体类并升级版本依赖
- 在 zkh-common 模块中新增 R2dbcBaseEntity 基础实体类
- 集成 Spring Data JPA 和 R2DBC 相关注解支持
- 添加审计功能支持创建时间和更新时间自动管理
- 实现基础字段如 ID、创建者、更新者等属性定义
- 将父项目及所有子模块版本从 1.5.5 升级至 1.5.6
- 在 zkh-common 中添加 spring-data-relational 依赖支持
2026-01-12 10:11:50 +08:00
zkh
753a07f71d chore(release): 更新框架版本到 1.5.5
- 更新 zkh-common 模块父版本到 1.5.5
- 更新 zkh-data 模块父版本到 1.5.5
- 更新 zkh-file 模块父版本到 1.5.5
- 更新 zkh-log 模块父版本到 1.5.5
- 更新 zkh-web 模块父版本到 1.5.5
- 更新主 pom.xml 版本到 1.5.5
2025-12-31 12:14:56 +08:00
zkh
209769d024 feat(jackson): 添加 LocalDateTime 和 LocalDate 的 JSON 序列化支持
- 引入 LocalDateTimeSerializer 和 LocalDateTimeDeserializer 处理 LocalDateTime 类型
- 添加 LocalDateSerializer 和 LocalDateDeserializer 处理 LocalDate 类型
- 配置 Jackson 使用自定义的时间格式化器
- 扩展 ObjectMapper 配置以支持新的序列化器和反序列化器
- 实现时间类型的空值安全处理
- 统一时间格式为 "yyyy-MM-dd HH:mm:ss" 和 "yyyy-MM-dd"
2025-12-31 12:13:32 +08:00
zkh
88afa8b47e refactor(jackson): 优化Long类型序列化器和反序列化器的类型处理
- 为LongSerializer添加handledType方法重写,明确指定处理Long类型
- 为LongDeserializer添加handledType方法重写,明确指定处理Long类型
- 提升Jackson配置中Long类型处理的准确性和性能
2025-12-31 12:07:24 +08:00
zkh
ab39c0f9b2 feat(base): 为 BaseEntity 添加序列化接口并配置 Jackson 时间和 Long 类型处理
- 为 BaseEntity 类实现 Serializable 接口以支持序列化
- 新增 JacksonConfig 配置类处理时间格式和 Long 类型精度问题
- 配置时区为 Asia/Shanghai 和中国地区格式
- 添加 Long 类型的序列化和反序列化器避免前端精度丢失
- 将项目版本从 1.5.3 升级到 1.5.4
2025-12-31 12:03:26 +08:00
zkh
eeac5b430c fix(web): 修复全局异常处理器并更新版本号
- 添加 ValidationException 异常处理支持
- 扩展异常处理器以捕获 RuntimeException
- 为 BindException 添加文档注释
- 新增 ValidationException 专门处理方法
- 更新父项目及所有子模块版本从 1.5.2 到 1.5.3
2025-12-31 11:53:04 +08:00
zkh
a0cb0cb6b7 chore(deps): 更新框架版本到1.5.2并改进安全配置日志
- 将zkh-framework父项目版本从1.5.1更新到1.5.2
- 同步更新所有子模块(zkh-common, zkh-data, zkh-file, zkh-log, zkh-web)的父版本
- 在WebSecurityConfig中改进认证失败日志记录,添加请求URI信息
2025-12-31 11:34:57 +08:00
11 changed files with 498 additions and 222 deletions

View File

@ -6,7 +6,7 @@
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
<packaging>pom</packaging>
<name>ZKH Framework</name>
<description>A Java framework for ZKH applications</description>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
</parent>
<artifactId>zkh-common</artifactId>
@ -38,6 +38,10 @@
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-relational</artifactId>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>

View File

@ -8,72 +8,73 @@ import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.io.Serializable;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
public class BaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column
@CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Column
@CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Column
@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Column
@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Column
@CreatedBy
private String createBy;
@Column
@CreatedBy
private String createBy;
@Column
@LastModifiedBy
private String updateBy;
@Column
@LastModifiedBy
private String updateBy;
public Long getId() {
return id;
}
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public String getCreateBy() {
return createBy;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
public String getUpdateBy() {
return updateBy;
}
public String getUpdateBy() {
return updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
public void setUpdateBy(String updateBy) {
this.updateBy = updateBy;
}
}

View File

@ -0,0 +1,72 @@
package vip.jcfd.common.core;
import com.fasterxml.jackson.annotation.JsonFormat;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.MappedSuperclass;
import org.springframework.data.annotation.*;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import org.springframework.data.relational.core.mapping.Column;
import java.io.Serializable;
import java.time.LocalDateTime;
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class R2dbcBaseEntity implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column("create_time")
@CreatedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime createTime;
@Column("update_time")
@LastModifiedDate
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private LocalDateTime updateTime;
@Column("create_by")
@CreatedBy
private String createBy;
@Column("update_by")
@LastModifiedBy
private String updateBy;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public LocalDateTime getCreateTime() {
return createTime;
}
public void setCreateTime(LocalDateTime createTime) {
this.createTime = createTime;
}
public LocalDateTime getUpdateTime() {
return updateTime;
}
public void setUpdateTime(LocalDateTime updateTime) {
this.updateTime = updateTime;
}
public String getCreateBy() {
return createBy;
}
public void setCreateBy(String createBy) {
this.createBy = createBy;
}
}

View File

@ -6,7 +6,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
</parent>
<artifactId>zkh-data</artifactId>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
</parent>
<artifactId>zkh-file</artifactId>

View File

@ -6,7 +6,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
</parent>
<artifactId>zkh-log</artifactId>

View File

@ -7,7 +7,7 @@
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.5.1</version>
<version>1.5.9</version>
</parent>
<artifactId>zkh-web</artifactId>
@ -23,6 +23,10 @@
<groupId>vip.jcfd</groupId>
<artifactId>zkh-log</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -1,10 +1,16 @@
package vip.jcfd.web.config;
import jakarta.validation.ConstraintViolationException;
import jakarta.validation.ValidationException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.validation.BindException;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.resource.NoResourceFoundException;
import vip.jcfd.common.core.BizException;
@ -18,8 +24,8 @@ public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = Exception.class)
public R<String> handleException(Exception e) {
@ExceptionHandler(value = {Exception.class, RuntimeException.class})
public R<String> handleException(Throwable e) {
log.error("服务异常", e);
return R.serverError("服务器繁忙,请稍候重试");
}
@ -37,6 +43,9 @@ public class GlobalExceptionHandler {
}
/**
* Handles bind exceptions; logs and returns formatted field errors
*/
@ExceptionHandler(value = BindException.class)
public R<String> handleBindException(BindException e) {
log.error("接口入参校验失败", e);
@ -44,4 +53,42 @@ public class GlobalExceptionHandler {
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
return R.error(String.join("\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
}
@ExceptionHandler(value = ValidationException.class)
public R<String> handleValidationException(ValidationException e) {
log.error("接口入参校验失败", e);
return R.error(e.getMessage());
}
/**
* 处理 @RequestBody + @Valid 校验失败
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public R<?> handleMethodArgumentNotValid(MethodArgumentNotValidException ex) {
log.error("接口入参校验失败", ex);
BindingResult bindingResult = ex.getBindingResult();
String msg = bindingResult.getFieldErrors()
.stream()
.map(err -> err.getField() + ": " + err.getDefaultMessage())
.findFirst()
.orElse("参数错误");
return R.error(msg);
}
/**
* 处理 @RequestParam / @PathVariable 校验失败
*/
@ExceptionHandler(ConstraintViolationException.class)
public R<?> handleConstraintViolation(ConstraintViolationException ex) {
log.error("接口入参校验失败", ex);
String msg = ex.getConstraintViolations()
.stream()
.map(v -> v.getPropertyPath() + ": " + v.getMessage())
.findFirst()
.orElse("参数错误");
return R.error(msg);
}
}

View File

@ -0,0 +1,143 @@
package vip.jcfd.web.config;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.databind.DeserializationContext;
import com.fasterxml.jackson.databind.JsonDeserializer;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.io.IOException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import java.util.TimeZone;
@Configuration
public class JacksonConfig {
/**
* Configures Jackson mapper with locale, timezone, date format, serializer
*/
@Bean
public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
return jacksonObjectMapperBuilder -> {
jacksonObjectMapperBuilder.locale(Locale.CHINA);
jacksonObjectMapperBuilder.timeZone(TimeZone.getTimeZone(ZoneId.of("Asia/Shanghai")));
jacksonObjectMapperBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
jacksonObjectMapperBuilder.serializers(new LongSerializer(), new LocalDateTimeSerializer(), new LocalDateSerializer());
jacksonObjectMapperBuilder.deserializers(new LongDeserializer(), new LocalDateTimeDeserializer(), new LocalDateDeserializer());
};
}
public static class LocalDateTimeSerializer extends JsonSerializer<LocalDateTime> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(value.format(formatter));
}
@Override
public Class<LocalDateTime> handledType() {
return LocalDateTime.class;
}
}
public static class LocalDateTimeDeserializer extends JsonDeserializer<LocalDateTime> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Override
public LocalDateTime deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.getText() == null) {
return null;
}
return LocalDateTime.parse(p.getText(), formatter);
}
@Override
public Class<?> handledType() {
return LocalDateTime.class;
}
}
public static class LocalDateSerializer extends JsonSerializer<LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(value.format(formatter));
}
@Override
public Class<LocalDate> handledType() {
return LocalDate.class;
}
}
public static class LocalDateDeserializer extends JsonDeserializer<LocalDate> {
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@Override
public LocalDate deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.getText() == null) {
return null;
}
return LocalDate.parse(p.getText(), formatter);
}
@Override
public Class<?> handledType() {
return LocalDate.class;
}
}
public static class LongSerializer extends JsonSerializer<Long> {
@Override
public void serialize(Long value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
if (value == null) {
gen.writeNull();
return;
}
gen.writeString(String.valueOf(value));
}
@Override
public Class<Long> handledType() {
return Long.class;
}
}
public static class LongDeserializer extends JsonDeserializer<Long> {
@Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException {
if (p.getText() == null) {
return null;
}
return Long.parseLong(p.getText());
}
@Override
public Class<?> handledType() {
return Long.class;
}
}
}

View File

@ -1,6 +1,7 @@
package vip.jcfd.web.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.DispatcherType;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
@ -10,6 +11,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
import org.springframework.http.HttpHeaders;
@ -60,191 +62,194 @@ import java.util.UUID;
@EnableScheduling
public class WebSecurityConfig {
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
private final SecurityProps securityProps;
private final ObjectMapper objectMapper;
private final TokenRedisStorage tokenRedisStorage;
private static final Logger log = LoggerFactory.getLogger(WebSecurityConfig.class);
private final SecurityProps securityProps;
private final ObjectMapper objectMapper;
private final TokenRedisStorage tokenRedisStorage;
public WebSecurityConfig(SecurityProps securityProps,
ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage,
AuthenticationManagerBuilder builder,
UserDetailsService userDetailsService) {
this.securityProps = securityProps;
this.objectMapper = objectMapper;
this.tokenRedisStorage = tokenRedisStorage;
builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService));
DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService);
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
builder.authenticationProvider(authenticationProvider);
}
public WebSecurityConfig(SecurityProps securityProps,
ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage,
AuthenticationManagerBuilder builder,
UserDetailsService userDetailsService) {
this.securityProps = securityProps;
this.objectMapper = objectMapper;
this.tokenRedisStorage = tokenRedisStorage;
builder.authenticationProvider(new RefreshTokenAuthProvider(userDetailsService));
DaoAuthenticationProvider authenticationProvider = new CustomDaoAuthenticationProvider(userDetailsService);
authenticationProvider.setPasswordEncoder(new BCryptPasswordEncoder());
builder.authenticationProvider(authenticationProvider);
}
@Scheduled(cron = "0 */30 * * * *")
@Async
public void scheduleClearExpiredTokens() {
tokenRedisStorage.clearExpiredTokens();
}
@Scheduled(cron = "0 */30 * * * *")
@Async
public void scheduleClearExpiredTokens() {
tokenRedisStorage.clearExpiredTokens();
}
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.or(() -> Optional.of("system"));
}
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.map(Authentication::getName)
.or(() -> Optional.of("system"));
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
@ConditionalOnMissingBean
public TokenFilter tokenFilter() {
return new TokenFilter(tokenRedisStorage);
}
@Bean
@ConditionalOnMissingBean
public TokenFilter tokenFilter() {
return new TokenFilter(tokenRedisStorage);
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
http.authorizeHttpRequests(config -> {
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
config.anyRequest().authenticated();
});
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage);
http.formLogin(config -> {
config.loginProcessingUrl("/login");
});
http.csrf(AbstractHttpConfigurer::disable);
http.logout(config -> {
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
});
http.rememberMe(AbstractHttpConfigurer::disable);
http.sessionManagement(AbstractHttpConfigurer::disable);
http.exceptionHandling(config -> {
config.authenticationEntryPoint(authenticationEntryPoint);
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
});
@Bean
@Order
public SecurityFilterChain security(HttpSecurity http, TokenFilter tokenFilter, AuthenticationManager authenticationManager) throws Exception {
http.authorizeHttpRequests(config -> {
config.dispatcherTypeMatchers(DispatcherType.ASYNC).permitAll();
config.requestMatchers(securityProps.getIgnoreUrls()).permitAll();
config.anyRequest().authenticated();
});
CustomAuthenticationEntryPoint authenticationEntryPoint = new CustomAuthenticationEntryPoint(objectMapper, tokenRedisStorage, securityProps);
http.formLogin(config -> {
config.loginProcessingUrl("/login");
});
http.csrf(AbstractHttpConfigurer::disable);
http.logout(config -> {
config.addLogoutHandler(new CustomLogoutSuccessHandler(objectMapper, tokenRedisStorage));
});
http.rememberMe(AbstractHttpConfigurer::disable);
http.sessionManagement(AbstractHttpConfigurer::disable);
http.exceptionHandling(config -> {
config.authenticationEntryPoint(authenticationEntryPoint);
config.accessDeniedHandler(new CustomAccessDeniedHandler(objectMapper));
});
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
filter.setAuthenticationFailureHandler(authenticationEntryPoint);
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
http.addFilterBefore(tokenFilter, UsernamePasswordAuthenticationFilter.class);
JsonUsernamePasswordAuthenticationFilter filter = new JsonUsernamePasswordAuthenticationFilter(objectMapper, authenticationManager);
filter.setAuthenticationSuccessHandler(authenticationEntryPoint);
filter.setAuthenticationFailureHandler(authenticationEntryPoint);
http.addFilterAt(filter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
private record CustomAuthenticationEntryPoint(
ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn("认证失败", authException);
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
private record CustomAuthenticationEntryPoint(
ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage,
SecurityProps securityProps) implements AuthenticationEntryPoint, AuthenticationFailureHandler, AuthenticationSuccessHandler {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
log.warn("访问 {} ,但是认证失败", request.getRequestURI(), authException);
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "未登录", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("登录失败", exception);
R<Object> data = new R<>(HttpServletResponse.SC_UNAUTHORIZED, "用户名或密码错误", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
log.warn("登录失败", exception);
R<Object> data = new R<>(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码错误", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户「{}」登录成功", authentication.getName());
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
log.info("用户「{}」登录成功", authentication.getName());
// 生成双重Token
String accessToken = UUID.randomUUID().toString();
String refreshToken = UUID.randomUUID().toString();
// 生成双重Token
String accessToken = UUID.randomUUID().toString();
String refreshToken = UUID.randomUUID().toString();
// 存储Access Token
tokenRedisStorage.putAccessToken(accessToken, authentication);
// 存储Access Token
tokenRedisStorage.putAccessToken(accessToken, authentication);
// 存储Refresh Token
String deviceId = extractDeviceId(request);
tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId);
// 存储Refresh Token
String deviceId = extractDeviceId(request);
tokenRedisStorage.putRefreshToken(refreshToken, authentication.getName(), deviceId);
// 构造登录响应
LoginResponse loginResponse = new LoginResponse(
accessToken,
refreshToken,
"Bearer",
1800, // 30分钟秒数
authentication.getName()
);
// 构造登录响应
LoginResponse loginResponse = new LoginResponse(
accessToken,
refreshToken,
"Bearer",
securityProps.getDuration().getSeconds(), // 30分钟秒数
authentication.getName()
);
response.setContentType("application/json;charset=UTF-8");
R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse);
objectMapper.writeValue(response.getWriter(), data);
}
response.setContentType("application/json;charset=UTF-8");
R<LoginResponse> data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse);
objectMapper.writeValue(response.getWriter(), data);
}
private String extractDeviceId(HttpServletRequest request) {
// 尝试从User-Agent提取设备信息
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
// 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法
if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) {
return "mobile-" + request.getRemoteAddr();
} else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) {
return "tablet-" + request.getRemoteAddr();
} else {
return "desktop-" + request.getRemoteAddr();
}
}
return "unknown-" + request.getRemoteAddr();
}
}
private String extractDeviceId(HttpServletRequest request) {
// 尝试从User-Agent提取设备信息
String userAgent = request.getHeader("User-Agent");
if (userAgent != null) {
// 简单的设备识别逻辑,生产环境可以使用更复杂的识别算法
if (userAgent.contains("Mobile") || userAgent.contains("Android") || userAgent.contains("iPhone")) {
return "mobile-" + request.getRemoteAddr();
} else if (userAgent.contains("Tablet") || userAgent.contains("iPad")) {
return "tablet-" + request.getRemoteAddr();
} else {
return "desktop-" + request.getRemoteAddr();
}
}
return "unknown-" + request.getRemoteAddr();
}
}
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
private record CustomAccessDeniedHandler(ObjectMapper objectMapper) implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.warn("访问被拒绝", accessDeniedException);
if (authentication.isAuthenticated()) {
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
} else {
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
}
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
}
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
log.warn("访问被拒绝", accessDeniedException);
if (authentication.isAuthenticated()) {
log.warn("用户「{}」访问「{}」被拒绝,因为:{}", authentication.getPrincipal(), request.getRequestURI(), accessDeniedException.getMessage());
} else {
log.warn("匿名用户访问「{}」被拒绝,因为:{}", request.getRequestURI(), accessDeniedException.getMessage());
}
R<Object> data = new R<>(HttpServletResponse.SC_FORBIDDEN, "无权限", false, null);
response.setContentType("application/json;charset=UTF-8");
objectMapper.writeValue(response.getWriter(), data);
}
}
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
private record CustomLogoutSuccessHandler(ObjectMapper objectMapper,
TokenRedisStorage tokenRedisStorage) implements LogoutHandler {
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
String header = request.getHeader(HttpHeaders.AUTHORIZATION);
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
authentication = tokenRedisStorage.get(token);
tokenRedisStorage.remove(token);
}
if (authentication != null) {
log.info("用户「{}」退出成功", authentication.getName());
String all = request.getParameter("all");
if ("true".equals(all)) {
tokenRedisStorage.removeByUserName(authentication.getName());
}
}
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
response.setContentType("application/json;charset=UTF-8");
try {
objectMapper.writeValue(response.getWriter(), data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
authentication = tokenRedisStorage.get(token);
tokenRedisStorage.remove(token);
}
if (authentication != null) {
log.info("用户「{}」退出成功", authentication.getName());
String all = request.getParameter("all");
if ("true".equals(all)) {
tokenRedisStorage.removeByUserName(authentication.getName());
}
}
R<Object> data = new R<>(HttpServletResponse.SC_OK, "退出成功", true, null);
response.setContentType("application/json;charset=UTF-8");
try {
objectMapper.writeValue(response.getWriter(), data);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}