@ -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 . accessDenied Handler ( 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 . exception Handling ( 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 ( " 访问 {} ,但是认证失败 " , request . getRequestURI ( ) , auth Exception) ;
R < Object > data = new R < > ( HttpServletResponse . SC_UNAUTHORIZED , " 未登录 " , false , null ) ;
response . setContentType ( " application/json;charset=UTF-8 " ) ;
objectMapper . writeValue ( response . g etWriter ( ) , 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 , Servlet Exception {
log . warn ( " 访问 {} ,但是认证失败 " , request . getRequestURI ( ) , authException ) ;
R < Object > data = new R < > ( HttpServletResponse . SC_UNAUTHORIZED , " 未登录 " , false , null ) ;
response . s etContentType ( " 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 ) ;
}
}
}
}