diff --git a/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java b/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java index 24a682d..1696d6e 100644 --- a/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java +++ b/zkh-web/src/main/java/vip/jcfd/web/config/WebSecurityConfig.java @@ -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,192 +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 auditorAware() { - return () -> Optional.ofNullable(SecurityContextHolder.getContext()) - .map(SecurityContext::getAuthentication) - .map(Authentication::getName) - .or(() -> Optional.of("system")); - } + @Bean + public AuditorAware 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, 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)); - }); + @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, - 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 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 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 data = new R<>(HttpServletResponse.SC_BAD_REQUEST, "用户名或密码错误", 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 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", - securityProps.getDuration().getSeconds(), // 30分钟,秒数 - authentication.getName() - ); + // 构造登录响应 + LoginResponse loginResponse = new LoginResponse( + accessToken, + refreshToken, + "Bearer", + securityProps.getDuration().getSeconds(), // 30分钟,秒数 + authentication.getName() + ); - response.setContentType("application/json;charset=UTF-8"); - R data = new R<>(HttpServletResponse.SC_OK, "登录成功", true, loginResponse); - objectMapper.writeValue(response.getWriter(), data); - } + response.setContentType("application/json;charset=UTF-8"); + R 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 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 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 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 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); + } + } + } }