还是溢出

This commit is contained in:
dengqichen 2024-11-27 17:53:36 +08:00
parent 06aaead053
commit 07c2d803b5
5 changed files with 86 additions and 162 deletions

View File

@ -1,54 +0,0 @@
package com.qqchen.deploy.backend.common.interceptor;
import com.qqchen.deploy.backend.common.context.TenantContext;
import com.qqchen.deploy.backend.common.enums.ResponseCode;
import com.qqchen.deploy.backend.common.exception.BusinessException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Arrays;
import java.util.List;
@Component
public class TenantInterceptor implements HandlerInterceptor {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
// 不需要进行租户隔离的路径
private final List<String> excludePaths = Arrays.asList(
"/api/v1/users/login",
"/api/system/tenants/list",
"/swagger-ui/**",
"/v3/api-docs/**"
);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String path = request.getRequestURI();
// 如果是白名单路径不进行租户校验
if (isExcludePath(path)) {
TenantContext.clear();
return true;
}
// 获取租户ID
String tenantId = request.getHeader("X-Tenant-ID");
if (tenantId == null || tenantId.trim().isEmpty()) {
throw new BusinessException(ResponseCode.TENANT_NOT_FOUND);
}
TenantContext.setTenantId(tenantId);
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
TenantContext.clear();
}
private boolean isExcludePath(String path) {
return excludePaths.stream()
.anyMatch(pattern -> pathMatcher.match(pattern, path));
}
}

View File

@ -2,6 +2,7 @@ package com.qqchen.deploy.backend.common.security.config;
import com.qqchen.deploy.backend.common.security.filter.JwtAuthenticationFilter;
import com.qqchen.deploy.backend.common.security.handler.CustomAuthenticationEntryPoint;
import com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@ -29,39 +30,31 @@ import java.util.Arrays;
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
private final UserDetailsService userDetailsService;
private final JwtTokenUtil jwtTokenUtil;
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtTokenUtil, userDetailsService);
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(csrf -> csrf.disable()) // 禁用CSRF
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.exceptionHandling(exceptions -> exceptions
.authenticationEntryPoint(authenticationEntryPoint)
)
.formLogin(form -> form.disable()) // 禁用form登录
.httpBasic(basic -> basic.disable()) // 禁用basic认证
.logout(logout -> logout.disable()) // 禁用logout
.anonymous(anonymous -> {
}) // 允许匿名访问
.authenticationEntryPoint(authenticationEntryPoint))
.authorizeHttpRequests(auth -> auth
// 公开接口
// .requestMatchers("/api/v1/users/register").permitAll()
// .requestMatchers("/api/v1/users/login").permitAll()
// // Swagger相关接口
// .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// // 健康检查接口
// .requestMatchers("/actuator/**").permitAll()
// 开发阶段可以暂时允许所有请求
.anyRequest().permitAll()
// 生产环境建议改为需要认证
//.anyRequest().authenticated()
).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
.requestMatchers("/api/v1/users/login", "/api/v1/users/register").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.requestMatchers("/actuator/health").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@ -80,7 +73,8 @@ public class SecurityConfig {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Tenant-ID"));
configuration.setExposedHeaders(Arrays.asList("Authorization", "X-Tenant-ID"));
configuration.setMaxAge(3600L);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();

View File

@ -1,5 +1,6 @@
package com.qqchen.deploy.backend.common.security.filter;
import com.qqchen.deploy.backend.common.context.TenantContext;
import com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
@ -20,19 +21,18 @@ import java.util.Arrays;
import java.util.List;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenUtil jwtTokenUtil;
private final UserDetailsService userDetailsService;
// 添加白名单路径
private static final List<String> WHITELIST = Arrays.asList(
"/api/v1/users/login",
"/api/v1/users/register",
"/swagger-ui/**",
"/v3/api-docs/**"
"/v3/api-docs/**",
"/actuator/health"
);
@Override
@ -43,29 +43,40 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
throws ServletException, IOException {
try {
String authHeader = request.getHeader("Authorization");
String token = null;
String username = null;
if (authHeader != null && authHeader.startsWith("Bearer ")) {
token = authHeader.substring(7);
username = jwtTokenUtil.getUsernameFromToken(token);
String tenantId = request.getHeader("X-Tenant-ID");
if (tenantId != null && !tenantId.isEmpty()) {
TenantContext.setTenantId(tenantId);
}
String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
chain.doFilter(request, response);
return;
}
String token = authHeader.substring(7);
String username = jwtTokenUtil.getUsernameFromToken(token);
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(request, response);
} catch (Exception e) {
log.error("Cannot set user authentication", e);
chain.doFilter(request, response);
log.error("JWT authentication failed", e);
SecurityContextHolder.clearContext();
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
} finally {
TenantContext.clear();
}
}
}

View File

@ -11,6 +11,9 @@ import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Slf4j
@Component
@ -21,12 +24,11 @@ public class JwtTokenUtil {
@Value("${jwt.expiration}")
private Long expiration;
private SecretKey key;
private SecretKey getKey() {
if (key == null) {
// 确保密钥长度至少为 256
byte[] keyBytes = new byte[32];
byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8);
System.arraycopy(secretBytes, 0, keyBytes, 0, Math.min(secretBytes.length, keyBytes.length));
@ -35,44 +37,51 @@ public class JwtTokenUtil {
return key;
}
public String generateToken(UserDetails userDetails) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return doGenerateToken(claims, userDetails.getUsername());
}
private String doGenerateToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.subject(userDetails.getUsername())
.issuedAt(now)
.expiration(expiryDate)
.claims(claims)
.subject(subject)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
.signWith(getKey())
.compact();
}
public String getUsernameFromToken(String token) {
public Boolean validateToken(String token, UserDetails userDetails) {
try {
Claims claims = Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
return claims.getSubject();
} catch (Exception e) {
log.error("Error getting username from token", e);
return null;
}
}
public boolean validateToken(String token, UserDetails userDetails) {
try {
String username = getUsernameFromToken(token);
Claims claims = Jwts.parser()
.verifyWith(getKey())
.build()
.parseSignedClaims(token)
.getPayload();
boolean isTokenExpired = claims.getExpiration().before(new Date());
return (username.equals(userDetails.getUsername()) && !isTokenExpired);
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
} catch (Exception e) {
log.error("Error validating token", e);
return false;

View File

@ -1,36 +0,0 @@
package com.qqchen.deploy.backend.service.impl;
import com.qqchen.deploy.backend.entity.User;
import com.qqchen.deploy.backend.repository.IUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Collections;
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final IUserRepository userRepository; // 直接使用Repository
@Override
@Transactional(readOnly = true)
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 直接查询数据库不要通过Controller或其他Service
return userRepository.findByUsernameAndDeletedFalse(username)
.map(this::createUserDetails)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
private UserDetails createUserDetails(User user) {
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(Collections.emptyList()) // 或者从数据库加载权限
.build();
}
}