初版代码提交
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
package fun.nojava.framework.aspect;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class LogAspect {
|
||||
|
||||
@Around("@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
|
||||
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
|
||||
"@annotation(org.springframework.web.bind.annotation.DeleteMapping) || " +
|
||||
"@annotation(org.springframework.web.bind.annotation.PatchMapping)")
|
||||
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
|
||||
HttpServletRequest request = attributes.getRequest();
|
||||
String method = request.getMethod();
|
||||
String uri = request.getRequestURI();
|
||||
String ip = getClientIp(request);
|
||||
|
||||
log.info("Request: {} {} from IP: {}", method, uri, ip);
|
||||
|
||||
try {
|
||||
Object result = joinPoint.proceed();
|
||||
log.info("Response: {} {} - success", method, uri);
|
||||
return result;
|
||||
} catch (Exception e) {
|
||||
log.error("Response: {} {} - error: {}", method, uri, e.getMessage());
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
private String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package fun.nojava.framework.aspect;
|
||||
|
||||
import fun.nojava.common.annotation.RateLimit;
|
||||
import fun.nojava.common.exception.BusinessException;
|
||||
import fun.nojava.common.exception.ErrorCode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.aspectj.lang.ProceedingJoinPoint;
|
||||
import org.aspectj.lang.annotation.Around;
|
||||
import org.aspectj.lang.annotation.Aspect;
|
||||
import org.aspectj.lang.reflect.MethodSignature;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.data.redis.core.script.DefaultRedisScript;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Collections;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Slf4j
|
||||
@Aspect
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RateLimitAspect {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
private static final String LUA_SCRIPT =
|
||||
"local current = redis.call('INCR', KEYS[1]) " +
|
||||
"if current == 1 then " +
|
||||
" redis.call('EXPIRE', KEYS[1], ARGV[2]) " +
|
||||
"end " +
|
||||
"if current > tonumber(ARGV[1]) then " +
|
||||
" return 0 " +
|
||||
"end " +
|
||||
"return 1";
|
||||
|
||||
@Around("@annotation(fun.nojava.common.annotation.RateLimit)")
|
||||
public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
|
||||
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
|
||||
Method method = signature.getMethod();
|
||||
RateLimit rateLimit = method.getAnnotation(RateLimit.class);
|
||||
|
||||
String key = "rate_limit:" + rateLimit.key();
|
||||
long duration = rateLimit.unit().toSeconds(rateLimit.duration());
|
||||
|
||||
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
|
||||
Long result = redisTemplate.execute(
|
||||
redisScript,
|
||||
Collections.singletonList(key),
|
||||
String.valueOf(rateLimit.maxCount()),
|
||||
String.valueOf(duration)
|
||||
);
|
||||
|
||||
if (result == null || result == 0) {
|
||||
log.warn("Rate limit exceeded for key: {}", key);
|
||||
throw new BusinessException(ErrorCode.RATE_LIMITED);
|
||||
}
|
||||
|
||||
return joinPoint.proceed();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package fun.nojava.framework.config;
|
||||
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
@Data
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "jwt")
|
||||
public class JwtProperties {
|
||||
|
||||
private String secret;
|
||||
private long accessTokenExpire = 7200L;
|
||||
private long refreshTokenExpire = 86400L;
|
||||
private long refreshTokenExpireRemember = 1209600L;
|
||||
|
||||
public SecretKey getSigningKey() {
|
||||
byte[] keyBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||
return Keys.hmacShaKeyFor(keyBytes);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
package fun.nojava.framework.config;
|
||||
|
||||
import fun.nojava.framework.security.JwtAuthenticationFilter;
|
||||
import fun.nojava.framework.security.JwtAuthenticationEntryPoint;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@RequiredArgsConstructor
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
||||
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(AbstractHttpConfigurer::disable)
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.exceptionHandling(ex -> ex.authenticationEntryPoint(jwtAuthenticationEntryPoint))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.requestMatchers("/api/public/**").permitAll()
|
||||
.requestMatchers("/api/oauth/**").permitAll()
|
||||
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||
.requestMatchers("/static/**", "/index.html", "/").permitAll()
|
||||
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
||||
.requestMatchers("/api/admin/**").hasRole("ADMIN")
|
||||
.requestMatchers("/api/app/**").hasAnyRole("USER", "ADMIN")
|
||||
.anyRequest().permitAll()
|
||||
)
|
||||
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration config = new CorsConfiguration();
|
||||
config.setAllowedOriginPatterns(List.of("*"));
|
||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||
config.setAllowedHeaders(List.of("*"));
|
||||
config.setAllowCredentials(true);
|
||||
config.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", config);
|
||||
return source;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
|
||||
return config.getAuthenticationManager();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package fun.nojava.framework.config;
|
||||
|
||||
import fun.nojava.framework.security.JwtTokenProvider;
|
||||
import fun.nojava.framework.filter.XssFilter;
|
||||
import org.springframework.boot.web.servlet.FilterRegistrationBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Configuration
|
||||
public class WebConfig {
|
||||
|
||||
@Bean
|
||||
public FilterRegistrationBean<XssFilter> xssFilterRegistration() {
|
||||
FilterRegistrationBean<XssFilter> registration = new FilterRegistrationBean<>();
|
||||
registration.setFilter(new XssFilter());
|
||||
registration.addUrlPatterns("/api/*");
|
||||
registration.setOrder(1);
|
||||
return registration;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package fun.nojava.framework.filter;
|
||||
|
||||
import fun.nojava.framework.wrapper.XssHttpServletRequestWrapper;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class XssFilter extends OncePerRequestFilter {
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
filterChain.doFilter(new XssHttpServletRequestWrapper(request), response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package fun.nojava.framework.handler;
|
||||
|
||||
import fun.nojava.common.exception.BusinessException;
|
||||
import fun.nojava.common.exception.ErrorCode;
|
||||
import fun.nojava.common.model.ApiResult;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
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 java.util.stream.Collectors;
|
||||
|
||||
@Slf4j
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BusinessException.class)
|
||||
public ApiResult<Void> handleBusinessException(BusinessException e) {
|
||||
log.warn("Business exception: code={}, message={}", e.getCode(), e.getMessage());
|
||||
return ApiResult.error(e.getCode(), e.getMessage());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
@ResponseStatus(HttpStatus.BAD_REQUEST)
|
||||
public ApiResult<Void> handleValidationException(MethodArgumentNotValidException e) {
|
||||
String message = e.getBindingResult().getFieldErrors().stream()
|
||||
.map(FieldError::getDefaultMessage)
|
||||
.collect(Collectors.joining("; "));
|
||||
log.warn("Validation error: {}", message);
|
||||
return ApiResult.error(ErrorCode.BAD_REQUEST.getCode(), message);
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
@ResponseStatus(HttpStatus.FORBIDDEN)
|
||||
public ApiResult<Void> handleAccessDeniedException(AccessDeniedException e) {
|
||||
log.warn("Access denied: {}", e.getMessage());
|
||||
return ApiResult.error(ErrorCode.FORBIDDEN);
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
|
||||
public ApiResult<Void> handleException(Exception e) {
|
||||
log.error("Unexpected error", e);
|
||||
return ApiResult.error(ErrorCode.INTERNAL_ERROR);
|
||||
}
|
||||
}
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
package fun.nojava.framework.security;
|
||||
|
||||
import fun.nojava.common.model.ApiResult;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException {
|
||||
log.warn("Unauthorized access: {} {}", request.getMethod(), request.getRequestURI());
|
||||
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
|
||||
response.setCharacterEncoding("UTF-8");
|
||||
|
||||
ApiResult<?> result = ApiResult.error(401, "未认证,请先登录");
|
||||
response.getWriter().write(objectMapper.writeValueAsString(result));
|
||||
}
|
||||
}
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
package fun.nojava.framework.security;
|
||||
|
||||
import fun.nojava.common.constant.SecurityConstants;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
private final TokenBlacklistService tokenBlacklistService;
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
|
||||
FilterChain filterChain) throws ServletException, IOException {
|
||||
String token = resolveToken(request);
|
||||
|
||||
if (StringUtils.hasText(token) && jwtTokenProvider.validateToken(token)) {
|
||||
if (tokenBlacklistService.isAccessTokenBlacklisted(token)) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
Long userId = jwtTokenProvider.getUserIdFromToken(token);
|
||||
String role = jwtTokenProvider.getRoleFromToken(token);
|
||||
|
||||
UsernamePasswordAuthenticationToken authentication =
|
||||
new UsernamePasswordAuthenticationToken(
|
||||
userId,
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_" + role))
|
||||
);
|
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String resolveToken(HttpServletRequest request) {
|
||||
String bearerToken = request.getHeader(SecurityConstants.TOKEN_HEADER);
|
||||
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(SecurityConstants.TOKEN_PREFIX)) {
|
||||
return bearerToken.substring(SecurityConstants.TOKEN_PREFIX.length());
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package fun.nojava.framework.security;
|
||||
|
||||
import fun.nojava.common.constant.SecurityConstants;
|
||||
import fun.nojava.framework.config.JwtProperties;
|
||||
import io.jsonwebtoken.*;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Date;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class JwtTokenProvider {
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
|
||||
public String generateAccessToken(Long userId, String role) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + jwtProperties.getAccessTokenExpire() * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.claim("role", role)
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(jwtProperties.getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(Long userId) {
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + jwtProperties.getRefreshTokenExpire() * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.claim("type", "refresh")
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(jwtProperties.getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateRefreshToken(Long userId, boolean rememberMe) {
|
||||
long expireSeconds = rememberMe
|
||||
? jwtProperties.getRefreshTokenExpireRemember()
|
||||
: jwtProperties.getRefreshTokenExpire();
|
||||
Date now = new Date();
|
||||
Date expiryDate = new Date(now.getTime() + expireSeconds * 1000);
|
||||
|
||||
return Jwts.builder()
|
||||
.subject(String.valueOf(userId))
|
||||
.claim("type", "refresh")
|
||||
.issuedAt(now)
|
||||
.expiration(expiryDate)
|
||||
.signWith(jwtProperties.getSigningKey())
|
||||
.compact();
|
||||
}
|
||||
|
||||
public Long getUserIdFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return Long.parseLong(claims.getSubject());
|
||||
}
|
||||
|
||||
public String getRoleFromToken(String token) {
|
||||
Claims claims = parseToken(token);
|
||||
return claims.get("role", String.class);
|
||||
}
|
||||
|
||||
public boolean validateToken(String token) {
|
||||
try {
|
||||
parseToken(token);
|
||||
return true;
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
log.warn("JWT validation failed: {}", e.getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private Claims parseToken(String token) {
|
||||
return Jwts.parser()
|
||||
.verifyWith(jwtProperties.getSigningKey())
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package fun.nojava.framework.security;
|
||||
|
||||
import fun.nojava.common.constant.SecurityConstants;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class TokenBlacklistService {
|
||||
|
||||
private final StringRedisTemplate redisTemplate;
|
||||
|
||||
public void blacklistAccessToken(String token, long remainingSeconds) {
|
||||
String key = "blacklist:access:" + token;
|
||||
redisTemplate.opsForValue().set(key, "1", remainingSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public void storeRefreshToken(Long userId, String refreshToken, long expireSeconds) {
|
||||
String key = SecurityConstants.REFRESH_TOKEN_KEY + userId;
|
||||
redisTemplate.opsForValue().set(key, refreshToken, expireSeconds, TimeUnit.SECONDS);
|
||||
}
|
||||
|
||||
public boolean isRefreshTokenValid(Long userId, String refreshToken) {
|
||||
String key = SecurityConstants.REFRESH_TOKEN_KEY + userId;
|
||||
String stored = redisTemplate.opsForValue().get(key);
|
||||
return refreshToken.equals(stored);
|
||||
}
|
||||
|
||||
public boolean isAccessTokenBlacklisted(String token) {
|
||||
String key = "blacklist:access:" + token;
|
||||
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
|
||||
}
|
||||
|
||||
public void removeRefreshToken(Long userId) {
|
||||
String key = SecurityConstants.REFRESH_TOKEN_KEY + userId;
|
||||
redisTemplate.delete(key);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package fun.nojava.framework.util;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
public final class WebUtils {
|
||||
|
||||
private WebUtils() {}
|
||||
|
||||
public static String getClientIp() {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return "unknown";
|
||||
}
|
||||
return getClientIp(attributes.getRequest());
|
||||
}
|
||||
|
||||
public static String getClientIp(HttpServletRequest request) {
|
||||
String ip = request.getHeader("X-Forwarded-For");
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getHeader("X-Real-IP");
|
||||
}
|
||||
if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
|
||||
ip = request.getRemoteAddr();
|
||||
}
|
||||
if (ip != null && ip.contains(",")) {
|
||||
ip = ip.split(",")[0].trim();
|
||||
}
|
||||
return ip;
|
||||
}
|
||||
|
||||
public static String getUserAgent() {
|
||||
ServletRequestAttributes attributes =
|
||||
(ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attributes == null) {
|
||||
return "";
|
||||
}
|
||||
return attributes.getRequest().getHeader("User-Agent");
|
||||
}
|
||||
|
||||
public static String generateDeviceFingerprint(HttpServletRequest request) {
|
||||
String ua = request.getHeader("User-Agent");
|
||||
String accept = request.getHeader("Accept-Language");
|
||||
return String.valueOf((ua + accept).hashCode());
|
||||
}
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package fun.nojava.framework.wrapper;
|
||||
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletRequestWrapper;
|
||||
import org.jsoup.Jsoup;
|
||||
import org.jsoup.safety.Safelist;
|
||||
|
||||
public class XssHttpServletRequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
public XssHttpServletRequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getParameter(String name) {
|
||||
String value = super.getParameter(name);
|
||||
return cleanXss(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] getParameterValues(String name) {
|
||||
String[] values = super.getParameterValues(name);
|
||||
if (values == null) {
|
||||
return null;
|
||||
}
|
||||
String[] cleaned = new String[values.length];
|
||||
for (int i = 0; i < values.length; i++) {
|
||||
cleaned[i] = cleanXss(values[i]);
|
||||
}
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHeader(String name) {
|
||||
String value = super.getHeader(name);
|
||||
return cleanXss(value);
|
||||
}
|
||||
|
||||
private String cleanXss(String value) {
|
||||
if (value == null || value.isEmpty()) {
|
||||
return value;
|
||||
}
|
||||
return Jsoup.clean(value, Safelist.basic());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user