我有一个 Spring boot 项目,带有 Spring security 和 spring session。我想将会话保存在数据库中,因此我在 application.properties 中指定了它:
spring.session.store-type=jdbc
我还想通过 REST API 登录我的应用程序(因为我使用 React 来创建页面)。 我就是这样做的:
@RestController
@RequestMapping("/api/users")
public class UserController {
private final UserService userService;
private final AuthenticationManager authenticationManager;
private final SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry;
private final SecurityContextRepository securityContextRepository;
@Autowired
public UserController(UserService userService, AuthenticationManager authenticationManager, SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry,
SecurityContextRepository securityContextRepository) {
this.userService = userService;
this.authenticationManager = authenticationManager;
this.sessionRegistry = sessionRegistry;
this.securityContextRepository = securityContextRepository;
}
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest, HttpServletRequest request, HttpServletResponse response) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getEmail(),
loginRequest.getPassword()
)
);
SecurityContext securityContext = SecurityContextHolder.getContext();
securityContext.setAuthentication(authentication);
securityContextRepository.saveContext(securityContext, request, response);
sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal());
return ResponseEntity.ok("User authenticated successfully");
}
当我使用有效凭据发出此 POST 请求时,一切都会按预期进行。我可以在数据库(spring_session - 表)中看到一条记录,其中principal_name(列)对应于刚刚经过身份验证的用户。
在 spring 文档之后,我发现了 MaximumSessions() 方法。我在代码中使用了它(值为 1),但它不起作用。我可以在一个浏览器上登录,然后在另一个浏览器上再次登录。 这是我的安全配置:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsServiceImpl;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
private final CustomOAuth2UserService customOAuth2UserService;
private final JdbcIndexedSessionRepository jdbcIndexedSessionRepository;
public SecurityConfig(UserDetailsServiceImpl userDetailsServiceImpl, BCryptPasswordEncoder bCryptPasswordEncoder,
CustomOAuth2UserService customOAuth2UserService, JdbcIndexedSessionRepository jdbcIndexedSessionRepository) {
this.userDetailsServiceImpl = userDetailsServiceImpl;
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
this.customOAuth2UserService = customOAuth2UserService;
this.jdbcIndexedSessionRepository = jdbcIndexedSessionRepository;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/users/login").permitAll()
.requestMatchers("/api/users/register").permitAll()
.requestMatchers("/api/users/current/**").permitAll()
.requestMatchers("/api/users").hasRole("ADMIN")
.requestMatchers("/api/tasks").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
)
.securityContext((securityContext) -> securityContext
.securityContextRepository(securityContextRepository())
)
.anonymous(AbstractHttpConfigurer::disable)
.logout((logout) -> logout
.logoutUrl("/api/users/logout")
.logoutSuccessHandler((request, response, authentication) -> response.setStatus(HttpStatus.OK.value()))
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("SESSION")
.permitAll()
)
.requestCache((cache) -> cache.requestCache(new NullRequestCache()))
.cors((cors) -> cors
.configurationSource(corsConfigurationSource())
)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
@Bean
public SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(jdbcIndexedSessionRepository);
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsServiceImpl);
authenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
我认为我的配置不太好。
我一直在互联网上查找类似的问题,但没有任何帮助。 我已经重写了 User 模型和 UserDetails 中的 equals() 和 hashCode() 方法。我已经测试过了。如果电子邮件相同,则用户是平等的。哈希码是使用电子邮件字段生成的。
我认为我的登录端点出了问题。也许 sessionRegistry 与 securityContextRepository 无法很好地通信。 我尝试过遵循 spring security 文档,但遇到了一些问题,上面的代码是我想出的。
-----更新-----
我刚刚发现问题的根源在于SecurityFilterChain中的securityConfig。存储在那里的配置不足以自动执行诸如 MaximumSessions(1) 之类的操作。 在控制器中,我手动提供 AuthenticationManager,而它应该在 SecurityFilterChain 中声明,如下所示:
.authenticationManager(authenticationManager())
它仍然没有解决我的问题,但我相信这是向前迈出的一小步
放弃控制器并将逻辑移至过滤器,并让该过滤器扩展
AbstractAuthenticationProcessingFilter
。然后,这将与 Spring Security 基础设施正确集成。当然,除非您想手动完成所有操作。
@Component
public class LoginFilter extends AbstractAuthenticationProcessingFilter {
private final ObjectMapper mapper;
public LoginFilter(ObjectMapper mapper) {
super(new AntPathRequestMatcher("/api/users/login", "POST");
this.mapper = mapper;
}
public abstract Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
LoginRequest loginRequest = mapper.readValue(request.getInputStream(), LoginRequest.class);
UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(loginRequest.getEmail(),
loginRequest.getPassword());
// Allow subclasses to set the "details" property
setDetails(request, authRequest);
return authenticationManager.authenticate(authRequest);
}
protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
}
类似这样的事情。现在我们需要将其添加到安全过滤器链中。
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsService userDetailsService;
private final PasswordEncoder passwordEncoder;
private final CustomOAuth2UserService customOAuth2UserService;
private final JdbcIndexedSessionRepository jdbcIndexedSessionRepository;
private final LoginFilter loginFilter;
public SecurityConfig(UserDetailsService userDetailsService, PasswordEncoder passwordEncoder,
CustomOAuth2UserService customOAuth2UserService, JdbcIndexedSessionRepository jdbcIndexedSessionRepository, LoginFilter loginFilter) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
this.customOAuth2UserService = customOAuth2UserService;
this.jdbcIndexedSessionRepository = jdbcIndexedSessionRepository;
this.loginFilter = loginFilter;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
addFilterBefore(loginFilter, UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/api/users/login").permitAll()
.requestMatchers("/api/users/register").permitAll()
.requestMatchers("/api/users/current/**").permitAll()
.requestMatchers("/api/users").hasRole("ADMIN")
.requestMatchers("/api/tasks").hasRole("ADMIN")
.anyRequest().authenticated()
)
.sessionManagement((sessionManagement) -> sessionManagement
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.maximumSessions(1)
.maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry())
)
.securityContext((securityContext) -> securityContext
.securityContextRepository(securityContextRepository())
)
.anonymous(AbstractHttpConfigurer::disable)
.logout((logout) -> logout
.logoutUrl("/api/users/logout")
.logoutSuccessHandler((request, response, authentication) -> response.setStatus(HttpStatus.OK.value()))
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("SESSION")
.permitAll()
)
.requestCache((cache) -> cache.requestCache(new NullRequestCache()))
.cors((cors) -> cors
.configurationSource(corsConfigurationSource())
)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
@Bean
public SecurityContextRepository securityContextRepository() {
return new HttpSessionSecurityContextRepository();
}
@Bean
public SpringSessionBackedSessionRegistry<? extends Session> sessionRegistry() {
return new SpringSessionBackedSessionRegistry<>(jdbcIndexedSessionRepository);
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
@Bean
public AuthenticationManager authenticationManager() {
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(userDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder);
ProviderManager providerManager = new ProviderManager(authenticationProvider);
providerManager.setEraseCredentialsAfterAuthentication(false);
return providerManager;
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000"));
configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE"));
configuration.setAllowedHeaders(Collections.singletonList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
// Needed to prevent Spring Boot from automatically registering this filter in the regular filter chain.
@Bean
public FilterRegistrationBean<LoginFilter> loginFilterRegistration(LoginFilter loginFilter) {
FilterRegistrationBean<LoginFilter> registration = new FilterRegistrationBean<LoginFilter>(loginFilter);
registration.setEnabled(false);
return registration;
}
}
这会将过滤器添加到链中。
注意: 您可能需要添加一些额外的配置以将正确的会话上下文等注入自定义过滤器中。我不确定过滤器是否会自动注入正确的过滤器。