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