还是溢出
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.filter.JwtAuthenticationFilter;
|
||||||
import com.qqchen.deploy.backend.common.security.handler.CustomAuthenticationEntryPoint;
|
import com.qqchen.deploy.backend.common.security.handler.CustomAuthenticationEntryPoint;
|
||||||
|
import com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@ -29,39 +30,31 @@ import java.util.Arrays;
|
|||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtAuthenticationFilter jwtAuthenticationFilter;
|
|
||||||
|
|
||||||
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
|
private final CustomAuthenticationEntryPoint authenticationEntryPoint;
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
private final JwtTokenUtil jwtTokenUtil;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public JwtAuthenticationFilter jwtAuthenticationFilter() {
|
||||||
|
return new JwtAuthenticationFilter(jwtTokenUtil, userDetailsService);
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
http
|
http
|
||||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||||
.csrf(csrf -> csrf.disable()) // 禁用CSRF
|
.csrf(csrf -> csrf.disable())
|
||||||
.sessionManagement(session -> session
|
.sessionManagement(session -> session
|
||||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 禁用session
|
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.exceptionHandling(exceptions -> exceptions
|
.exceptionHandling(exceptions -> exceptions
|
||||||
.authenticationEntryPoint(authenticationEntryPoint)
|
.authenticationEntryPoint(authenticationEntryPoint))
|
||||||
)
|
|
||||||
.formLogin(form -> form.disable()) // 禁用form登录
|
|
||||||
.httpBasic(basic -> basic.disable()) // 禁用basic认证
|
|
||||||
.logout(logout -> logout.disable()) // 禁用logout
|
|
||||||
.anonymous(anonymous -> {
|
|
||||||
}) // 允许匿名访问
|
|
||||||
.authorizeHttpRequests(auth -> auth
|
.authorizeHttpRequests(auth -> auth
|
||||||
// 公开接口
|
.requestMatchers("/api/v1/users/login", "/api/v1/users/register").permitAll()
|
||||||
// .requestMatchers("/api/v1/users/register").permitAll()
|
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
||||||
// .requestMatchers("/api/v1/users/login").permitAll()
|
.requestMatchers("/actuator/health").permitAll()
|
||||||
// // Swagger相关接口
|
.anyRequest().authenticated()
|
||||||
// .requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
|
)
|
||||||
// // 健康检查接口
|
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
|
||||||
// .requestMatchers("/actuator/**").permitAll()
|
|
||||||
// 开发阶段可以暂时允许所有请求
|
|
||||||
.anyRequest().permitAll()
|
|
||||||
// 生产环境建议改为需要认证
|
|
||||||
//.anyRequest().authenticated()
|
|
||||||
|
|
||||||
).addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
|
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
}
|
}
|
||||||
@ -80,7 +73,8 @@ public class SecurityConfig {
|
|||||||
CorsConfiguration configuration = new CorsConfiguration();
|
CorsConfiguration configuration = new CorsConfiguration();
|
||||||
configuration.setAllowedOrigins(Arrays.asList("*"));
|
configuration.setAllowedOrigins(Arrays.asList("*"));
|
||||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
|
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);
|
configuration.setMaxAge(3600L);
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package com.qqchen.deploy.backend.common.security.filter;
|
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 com.qqchen.deploy.backend.common.security.util.JwtTokenUtil;
|
||||||
import jakarta.servlet.FilterChain;
|
import jakarta.servlet.FilterChain;
|
||||||
import jakarta.servlet.ServletException;
|
import jakarta.servlet.ServletException;
|
||||||
@ -20,19 +21,18 @@ import java.util.Arrays;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtTokenUtil jwtTokenUtil;
|
private final JwtTokenUtil jwtTokenUtil;
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
// 添加白名单路径
|
|
||||||
private static final List<String> WHITELIST = Arrays.asList(
|
private static final List<String> WHITELIST = Arrays.asList(
|
||||||
"/api/v1/users/login",
|
"/api/v1/users/login",
|
||||||
"/api/v1/users/register",
|
"/api/v1/users/register",
|
||||||
"/swagger-ui/**",
|
"/swagger-ui/**",
|
||||||
"/v3/api-docs/**"
|
"/v3/api-docs/**",
|
||||||
|
"/actuator/health"
|
||||||
);
|
);
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -43,29 +43,40 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
|
||||||
throws ServletException, IOException {
|
throws ServletException, IOException {
|
||||||
try {
|
try {
|
||||||
String authHeader = request.getHeader("Authorization");
|
String tenantId = request.getHeader("X-Tenant-ID");
|
||||||
String token = null;
|
if (tenantId != null && !tenantId.isEmpty()) {
|
||||||
String username = null;
|
TenantContext.setTenantId(tenantId);
|
||||||
|
|
||||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
||||||
token = authHeader.substring(7);
|
|
||||||
username = jwtTokenUtil.getUsernameFromToken(token);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
|
||||||
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
|
||||||
|
|
||||||
if (jwtTokenUtil.validateToken(token, userDetails)) {
|
if (jwtTokenUtil.validateToken(token, userDetails)) {
|
||||||
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
|
||||||
userDetails, null, userDetails.getAuthorities());
|
userDetails, null, userDetails.getAuthorities());
|
||||||
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
|
||||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
chain.doFilter(request, response);
|
chain.doFilter(request, response);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Cannot set user authentication", e);
|
log.error("JWT authentication failed", e);
|
||||||
chain.doFilter(request, response);
|
SecurityContextHolder.clearContext();
|
||||||
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
||||||
|
} finally {
|
||||||
|
TenantContext.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -11,6 +11,9 @@ import org.springframework.stereotype.Component;
|
|||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Component
|
@Component
|
||||||
@ -21,12 +24,11 @@ public class JwtTokenUtil {
|
|||||||
|
|
||||||
@Value("${jwt.expiration}")
|
@Value("${jwt.expiration}")
|
||||||
private Long expiration;
|
private Long expiration;
|
||||||
|
|
||||||
private SecretKey key;
|
private SecretKey key;
|
||||||
|
|
||||||
private SecretKey getKey() {
|
private SecretKey getKey() {
|
||||||
if (key == null) {
|
if (key == null) {
|
||||||
// 确保密钥长度至少为 256 位
|
|
||||||
byte[] keyBytes = new byte[32];
|
byte[] keyBytes = new byte[32];
|
||||||
byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8);
|
byte[] secretBytes = secret.getBytes(StandardCharsets.UTF_8);
|
||||||
System.arraycopy(secretBytes, 0, keyBytes, 0, Math.min(secretBytes.length, keyBytes.length));
|
System.arraycopy(secretBytes, 0, keyBytes, 0, Math.min(secretBytes.length, keyBytes.length));
|
||||||
@ -35,44 +37,51 @@ public class JwtTokenUtil {
|
|||||||
return key;
|
return key;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateToken(UserDetails userDetails) {
|
public String getUsernameFromToken(String token) {
|
||||||
Date now = new Date();
|
return getClaimFromToken(token, Claims::getSubject);
|
||||||
Date expiryDate = new Date(now.getTime() + expiration * 1000);
|
}
|
||||||
|
|
||||||
|
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()
|
return Jwts.builder()
|
||||||
.subject(userDetails.getUsername())
|
.claims(claims)
|
||||||
.issuedAt(now)
|
.subject(subject)
|
||||||
.expiration(expiryDate)
|
.issuedAt(new Date(System.currentTimeMillis()))
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + expiration * 1000))
|
||||||
.signWith(getKey())
|
.signWith(getKey())
|
||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getUsernameFromToken(String token) {
|
public Boolean validateToken(String token, UserDetails userDetails) {
|
||||||
try {
|
try {
|
||||||
Claims claims = Jwts.parser()
|
final String username = getUsernameFromToken(token);
|
||||||
.verifyWith(getKey())
|
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
|
||||||
.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);
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
log.error("Error validating token", e);
|
log.error("Error validating token", e);
|
||||||
return false;
|
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