feat:Websocket服务拆到单独服务,主后台保持单工通信

This commit is contained in:
zpaeng
2025-09-02 23:10:29 +08:00
parent c337195b16
commit 78a65c6afe
35 changed files with 1504 additions and 329 deletions

View File

@@ -0,0 +1,19 @@
package com.openisle.websocket.security;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.Collections;
@Configuration
@RequiredArgsConstructor
public class ApplicationConfig {
@Bean
public UserDetailsService userDetailsService() {
return username -> new User(username, "", Collections.emptyList());
}
}

View File

@@ -0,0 +1,98 @@
package com.openisle.websocket.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.function.Function;
@Service
public class JwtService {
private static final Logger logger = LoggerFactory.getLogger(JwtService.class);
@Value("${app.jwt.secret}")
private String secret;
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
public boolean isTokenValid(String token) {
try {
return !isTokenExpired(token);
} catch (Exception e) {
return false;
}
}
private boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}
private Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
private Claims extractAllClaims(String token) {
logger.debug("解析JWT token - secret长度: {}", secret != null ? secret.length() : "null");
try {
return Jwts
.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
} catch (Exception e) {
logger.error("JWT解析失败: {}", e.getMessage());
throw e;
}
}
private Key getSignInKey() {
// 使用与backend相同的密钥处理方式直接Base64解码
byte[] keyBytes;
try {
// 尝试Base64解码
keyBytes = java.util.Base64.getDecoder().decode(secret);
} catch (IllegalArgumentException e) {
// 如果不是Base64格式使用UTF-8字节
keyBytes = secret.getBytes(StandardCharsets.UTF_8);
// 确保密钥长度至少256位32字节
if (keyBytes.length < 32) {
MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
keyBytes = digest.digest(keyBytes);
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException("SHA-256 not available", ex);
}
}
}
return Keys.hmacShaKeyFor(keyBytes);
}
public String validateAndGetSubject(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSignInKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
}

View File

@@ -0,0 +1,71 @@
package com.openisle.websocket.security;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.List;
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
@Value("${app.website-url}")
private String websiteUrl;
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of(
"http://127.0.0.1:8080",
"http://127.0.0.1:8081",
"http://127.0.0.1:8082",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
"http://127.0.0.1",
"http://localhost:8080",
"http://localhost:8081",
"http://localhost:8082",
"http://localhost:3000",
"http://localhost:3001",
"http://localhost",
"http://30.211.97.238:3000",
"http://30.211.97.238",
"http://192.168.7.98",
"http://192.168.7.98:3000",
websiteUrl,
websiteUrl.replace("://www.", "://")
));
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(List.of("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.cors(Customizer.withDefaults())
.authorizeHttpRequests(auth -> auth
.requestMatchers("/**").permitAll() // Permit all HTTP requests
)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
return http.build();
}
}