mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-07 23:51:17 +08:00
205 lines
11 KiB
Java
205 lines
11 KiB
Java
package com.openisle.config;
|
|
|
|
import com.openisle.service.JwtService;
|
|
import com.openisle.service.UserVisitService;
|
|
import com.openisle.repository.UserRepository;
|
|
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.authentication.UsernamePasswordAuthenticationToken;
|
|
import org.springframework.security.config.Customizer;
|
|
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
|
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
|
import org.springframework.security.config.http.SessionCreationPolicy;
|
|
import org.springframework.security.core.userdetails.UserDetails;
|
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
|
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.access.AccessDeniedHandler;
|
|
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
|
|
import org.springframework.web.filter.OncePerRequestFilter;
|
|
import org.springframework.web.cors.CorsConfiguration;
|
|
import org.springframework.web.cors.CorsConfigurationSource;
|
|
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import java.util.List;
|
|
|
|
import jakarta.servlet.FilterChain;
|
|
import jakarta.servlet.ServletException;
|
|
import jakarta.servlet.http.HttpServletRequest;
|
|
import jakarta.servlet.http.HttpServletResponse;
|
|
import java.io.IOException;
|
|
|
|
@Configuration
|
|
@RequiredArgsConstructor
|
|
public class SecurityConfig {
|
|
private final JwtService jwtService;
|
|
private final UserRepository userRepository;
|
|
private final AccessDeniedHandler customAccessDeniedHandler;
|
|
private final UserVisitService userVisitService;
|
|
@Value("${app.website-url}")
|
|
private String websiteUrl;
|
|
|
|
@Bean
|
|
public PasswordEncoder passwordEncoder() {
|
|
return new BCryptPasswordEncoder();
|
|
}
|
|
|
|
@Bean
|
|
public UserDetailsService userDetailsService() {
|
|
return username -> userRepository.findByUsername(username)
|
|
.<UserDetails>map(user -> org.springframework.security.core.userdetails.User
|
|
.withUsername(user.getUsername())
|
|
.password(user.getPassword())
|
|
.authorities(user.getRole().name())
|
|
.build())
|
|
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
|
|
}
|
|
|
|
@Bean
|
|
public AuthenticationManager authenticationManager(HttpSecurity http, PasswordEncoder passwordEncoder, UserDetailsService userDetailsService) throws Exception {
|
|
return http.getSharedObject(AuthenticationManagerBuilder.class)
|
|
.userDetailsService(userDetailsService)
|
|
.passwordEncoder(passwordEncoder)
|
|
.and()
|
|
.build();
|
|
}
|
|
|
|
@Bean
|
|
public CorsConfigurationSource corsConfigurationSource() {
|
|
CorsConfiguration cfg = new CorsConfiguration();
|
|
cfg.setAllowedOrigins(List.of(
|
|
"http://127.0.0.1:8080",
|
|
"http://127.0.0.1:3000",
|
|
"http://127.0.0.1:3001",
|
|
"http://127.0.0.1",
|
|
"http://localhost:8080",
|
|
"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.", "://")
|
|
));
|
|
cfg.setAllowedMethods(List.of("GET","POST","PUT","DELETE","OPTIONS"));
|
|
cfg.setAllowedHeaders(List.of("*"));
|
|
cfg.setAllowCredentials(true);
|
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
|
source.registerCorsConfiguration("/api/**", cfg);
|
|
return source;
|
|
}
|
|
|
|
@Bean
|
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
|
http.csrf(csrf -> csrf.disable())
|
|
.cors(Customizer.withDefaults())
|
|
.headers(h -> h.frameOptions(f -> f.sameOrigin()))
|
|
.sessionManagement(sm -> sm.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
|
.exceptionHandling(eh -> eh.accessDeniedHandler(customAccessDeniedHandler))
|
|
.authorizeHttpRequests(auth -> auth
|
|
.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll()
|
|
.requestMatchers("/api/ws/**", "/api/sockjs/**").permitAll()
|
|
.requestMatchers(HttpMethod.POST, "/api/auth/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/posts/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/comments/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/categories/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/tags/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/config/**").permitAll()
|
|
.requestMatchers(HttpMethod.POST,"/api/auth/google").permitAll()
|
|
.requestMatchers(HttpMethod.POST,"/api/auth/reason").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/search/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/users/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/medals/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/push/public-key").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
|
|
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
|
|
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
|
|
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
|
|
.requestMatchers(HttpMethod.POST, "/api/tags/**").authenticated()
|
|
.requestMatchers(HttpMethod.DELETE, "/api/categories/**").hasAuthority("ADMIN")
|
|
.requestMatchers(HttpMethod.DELETE, "/api/tags/**").hasAuthority("ADMIN")
|
|
.requestMatchers(HttpMethod.GET, "/api/stats/**").hasAuthority("ADMIN")
|
|
.requestMatchers("/api/admin/**").hasAuthority("ADMIN")
|
|
.anyRequest().authenticated()
|
|
)
|
|
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class)
|
|
.addFilterAfter(userVisitFilter(), UsernamePasswordAuthenticationFilter.class);
|
|
return http.build();
|
|
}
|
|
|
|
@Bean
|
|
public OncePerRequestFilter jwtAuthenticationFilter() {
|
|
return new OncePerRequestFilter() {
|
|
@Override
|
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
|
// 让预检请求直接通过
|
|
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
|
|
filterChain.doFilter(request, response);
|
|
return;
|
|
}
|
|
String authHeader = request.getHeader("Authorization");
|
|
String uri = request.getRequestURI();
|
|
|
|
boolean publicGet = "GET".equalsIgnoreCase(request.getMethod()) &&
|
|
(uri.startsWith("/api/posts") || uri.startsWith("/api/comments") ||
|
|
uri.startsWith("/api/categories") || uri.startsWith("/api/tags") ||
|
|
uri.startsWith("/api/search") || uri.startsWith("/api/users") ||
|
|
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
|
|
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
|
|
uri.startsWith("/api/point-goods") || uri.startsWith("/api/channels") ||
|
|
uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
|
|
uri.startsWith("/api/rss"));
|
|
|
|
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
|
String token = authHeader.substring(7);
|
|
try {
|
|
String username = jwtService.validateAndGetSubject(token);
|
|
UserDetails userDetails = userDetailsService().loadUserByUsername(username);
|
|
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
|
|
userDetails, null, userDetails.getAuthorities());
|
|
org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(authToken);
|
|
} catch (Exception e) {
|
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
response.setContentType("application/json");
|
|
response.getWriter().write("{\"error\": \"Invalid or expired token\"}");
|
|
return;
|
|
}
|
|
} else if (!uri.startsWith("/api/auth") && !publicGet
|
|
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")) {
|
|
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
|
|
response.setContentType("application/json");
|
|
response.getWriter().write("{\"error\": \"Missing token\"}");
|
|
return;
|
|
}
|
|
|
|
filterChain.doFilter(request, response);
|
|
}
|
|
};
|
|
}
|
|
|
|
@Bean
|
|
public OncePerRequestFilter userVisitFilter() {
|
|
return new OncePerRequestFilter() {
|
|
@Override
|
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
|
var auth = org.springframework.security.core.context.SecurityContextHolder.getContext().getAuthentication();
|
|
if (auth != null && auth.isAuthenticated() && !(auth instanceof org.springframework.security.authentication.AnonymousAuthenticationToken)) {
|
|
userVisitService.recordVisit(auth.getName());
|
|
}
|
|
filterChain.doFilter(request, response);
|
|
}
|
|
};
|
|
}
|
|
}
|