还是溢出
This commit is contained in:
parent
06aaead053
commit
07c2d803b5
@ -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));
|
||||
}
|
||||
}
|
||||
@ -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();
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user