feat(log): 新增日志注解功能并集成到认证流程

- 新增 @Log 注解及其配置类 LogConfig
- 在 AuthController 的 refreshToken 方法上添加 @Log 注解
- 创建 zkh-log 模块并引入 spring-boot-starter-aop 依赖
- 在 zkh-web 模块中引入 zkh-log 依赖
- 调整 GlobalExceptionHandler 和 RedisConfig 类中的代码缩进格式
- 更新 pom.xml 文件,加入 zkh-log 模块并调整依赖版本声明位置
- 修改 SpringDocConfig、WebSecurityConfig 等配置类的 bean 名称以避免冲突
This commit is contained in:
zkh
2025-12-01 16:31:45 +08:00
parent a7e1c26853
commit fe2240e266
11 changed files with 317 additions and 271 deletions

View File

@ -38,6 +38,7 @@
<module>zkh-common</module>
<module>zkh-web</module>
<module>zkh-data</module>
<module>zkh-log</module>
</modules>
<properties>
@ -71,6 +72,11 @@
<artifactId>zkh-data</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-log</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-common</artifactId>

View File

@ -13,12 +13,6 @@
<name>ZKH Data</name>
<description>Data layer components for ZKH framework</description>
<properties>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>jakarta.persistence</groupId>

23
zkh-log/pom.xml Normal file
View File

@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-framework</artifactId>
<version>1.4</version>
</parent>
<artifactId>zkh-log</artifactId>
<name>ZKH log</name>
<description>Logging utilities for ZKH framework</description>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
</project>

View File

@ -0,0 +1,10 @@
package vip.jcfd.log.annotation;
import java.lang.annotation.*;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Log {
String value();
}

View File

@ -0,0 +1,7 @@
package vip.jcfd.log.annotation.config;
import org.springframework.context.annotation.Configuration;
@Configuration("_logConfiguration")
public class LogConfig {
}

View File

@ -19,6 +19,10 @@
<groupId>vip.jcfd</groupId>
<artifactId>zkh-common</artifactId>
</dependency>
<dependency>
<groupId>vip.jcfd</groupId>
<artifactId>zkh-log</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>

View File

@ -13,35 +13,35 @@ import vip.jcfd.common.core.R;
import java.util.List;
@RestControllerAdvice
@RestControllerAdvice("_globalExceptionHandler")
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
@ExceptionHandler(value = Exception.class)
public R<String> handleException(Exception e) {
log.error("服务异常", e);
return R.serverError("服务器繁忙,请稍候重试");
}
@ExceptionHandler(value = Exception.class)
public R<String> handleException(Exception e) {
log.error("服务异常", e);
return R.serverError("服务器繁忙,请稍候重试");
}
@ExceptionHandler(value = BizException.class)
public R<String> handleBizException(BizException e) {
log.error("业务异常", e);
return R.error(e.getMessage());
}
@ExceptionHandler(value = BizException.class)
public R<String> handleBizException(BizException e) {
log.error("业务异常", e);
return R.error(e.getMessage());
}
@ExceptionHandler(value = NoResourceFoundException.class)
public R<String> handleNotFoundException(NoResourceFoundException e) {
log.error("404异常", e);
return new R<>(404, "您访问的地址不存在", false, null);
}
@ExceptionHandler(value = NoResourceFoundException.class)
public R<String> handleNotFoundException(NoResourceFoundException e) {
log.error("404异常", e);
return new R<>(404, "您访问的地址不存在", false, null);
}
@ExceptionHandler(value = BindException.class)
public R<String> handleBindException(BindException e) {
log.error("接口入参校验失败", e);
@ExceptionHandler(value = BindException.class)
public R<String> handleBindException(BindException e) {
log.error("接口入参校验失败", e);
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
return R.error(String.join("\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
}
List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
return R.error(String.join("\n", fieldErrors.stream().map(FieldError::getDefaultMessage).toList()));
}
}

View File

@ -10,26 +10,26 @@ import org.springframework.data.redis.serializer.StringRedisSerializer;
import vip.jcfd.web.config.props.SecurityProps;
import vip.jcfd.web.redis.TokenRedisStorage;
@Configuration
@Configuration("_redisConfiguration")
public class RedisConfig {
private final SecurityProps securityProps;
private final SecurityProps securityProps;
public RedisConfig(SecurityProps securityProps) {
this.securityProps = securityProps;
}
public RedisConfig(SecurityProps securityProps) {
this.securityProps = securityProps;
}
@Bean
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
securityProps.getAccessTokenDuration(),
securityProps.getRefreshTokenDuration(),
stringRedisTemplate,
objectMapper
);
tokenRedisStorage.setConnectionFactory(factory);
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
return tokenRedisStorage;
}
@Bean
public TokenRedisStorage tokenRedisTemplate(RedisConnectionFactory factory, StringRedisTemplate stringRedisTemplate, ObjectMapper objectMapper) {
TokenRedisStorage tokenRedisStorage = new TokenRedisStorage(
securityProps.getAccessTokenDuration(),
securityProps.getRefreshTokenDuration(),
stringRedisTemplate,
objectMapper
);
tokenRedisStorage.setConnectionFactory(factory);
tokenRedisStorage.setValueSerializer(new JdkSerializationRedisSerializer());
tokenRedisStorage.setKeySerializer(new StringRedisSerializer());
return tokenRedisStorage;
}
}

View File

@ -10,63 +10,63 @@ import org.springdoc.core.customizers.OpenApiCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration("_springDocConfig")
@Configuration("_springDocConfiguration")
public class SpringDocConfig {
@Bean
public OpenApiCustomizer openApiCustomizer() {
return (openAPI) -> {
openAPI.path("/login", new PathItem()
.post(new Operation()
.summary("登录接口")
.description("用于用户登录,返回token")
.addTagsItem("认证管理")
.requestBody(new RequestBody()
.description("帐号密码")
.required(true)
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("username", new StringSchema().example("admin"))
.addProperty("password", new StringSchema().example("123456"))))))
.responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new JsonSchema()
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
.addProperty("tokenType", new StringSchema().example("Bearer"))
.addProperty("expiresIn", new NumberSchema().example(1800))
.addProperty("username", new StringSchema().example("admin"))
)
.addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登录成功"))
))))
.addApiResponse("失败", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(false))
.addProperty("code", new IntegerSchema().example(401))
.addProperty("message", new StringSchema().example("用户名或密码错误"))
))))
)));
openAPI.path("/logout", new PathItem()
.post(new Operation()
.summary("登出接口")
.description("用于用户登出")
.addTagsItem("认证管理")
.responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登出成功"))
)
)
)
)
)
));
};
}
@Bean
public OpenApiCustomizer openApiCustomizer() {
return (openAPI) -> {
openAPI.path("/login", new PathItem()
.post(new Operation()
.summary("登录接口")
.description("用于用户登录,返回token")
.addTagsItem("认证管理")
.requestBody(new RequestBody()
.description("帐号密码")
.required(true)
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("username", new StringSchema().example("admin"))
.addProperty("password", new StringSchema().example("123456"))))))
.responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new JsonSchema()
.addProperty("accessToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440000"))
.addProperty("refreshToken", new StringSchema().example("550e8400-e29b-41d4-a716-446655440001"))
.addProperty("tokenType", new StringSchema().example("Bearer"))
.addProperty("expiresIn", new NumberSchema().example(1800))
.addProperty("username", new StringSchema().example("admin"))
)
.addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登录成功"))
))))
.addApiResponse("失败", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(false))
.addProperty("code", new IntegerSchema().example(401))
.addProperty("message", new StringSchema().example("用户名或密码错误"))
))))
)));
openAPI.path("/logout", new PathItem()
.post(new Operation()
.summary("登出接口")
.description("用于用户登出")
.addTagsItem("认证管理")
.responses(new ApiResponses()
.addApiResponse("成功", new ApiResponse()
.content(new Content().addMediaType("application/json", new MediaType().schema(new Schema<>()
.addProperty("data", new StringSchema().example(null))
.addProperty("success", new BooleanSchema().example(true))
.addProperty("code", new IntegerSchema().example(200))
.addProperty("message", new StringSchema().example("登出成功"))
)
)
)
)
)
));
};
}
}

View File

@ -52,7 +52,7 @@ import java.io.IOException;
import java.util.Optional;
import java.util.UUID;
@Configuration
@Configuration("_webSecurityConfiguration")
@EnableWebSecurity
@ConfigurationPropertiesScan(basePackageClasses = {SecurityProps.class})
@EnableJpaAuditing
@ -60,191 +60,191 @@ 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
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));
});
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) 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);
}
@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_UNAUTHORIZED, "用户名或密码错误", 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",
1800, // 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);
}
}
}
}

View File

@ -15,6 +15,7 @@ import org.springframework.web.bind.annotation.*;
import vip.jcfd.common.core.R;
import vip.jcfd.common.dto.TokenRefreshRequest;
import vip.jcfd.common.dto.TokenRefreshResponse;
import vip.jcfd.log.annotation.Log;
import vip.jcfd.web.auth.RefreshTokenAuthenticationToken;
import vip.jcfd.web.redis.TokenRedisStorage;
@ -37,6 +38,7 @@ public class AuthController {
@PostMapping("/refresh-token")
@Operation(summary = "刷新Token", description = "使用Refresh Token获取新的Access Token和Refresh Token")
@Log("刷新了token")
public R<TokenRefreshResponse> refreshToken(
@Valid @RequestBody TokenRefreshRequest request) {