使用 Spring Session Redis 时身份验证主体为空
Authentication Principal is empty while using Spring Session Redis
我正在使用 Spring Boot v1.3.3 构建 rest API。 API 受 Spring 安全保护。我已经实现了自定义用户详细信息服务以在身份验证上下文中具有自定义主体。
我需要与其他 Spring 应用程序共享 API 会话,因此我选择使用本教程 [在我的应用程序中实施 Spring 与 Redis 服务器的会话=59=].io/spring-session/docs/current/reference/html5/guides/security.html.不幸的是,它导致 Authentication Principal 停止工作。当我试图通过注释 @AuthenticationPrincipal CustomUserDetails user
或 SecurityContextHolder.getContext().getAuthentication().getPrincipal()
获取当前主体时,它 returns 我的自定义用户详细信息,但 Id = 0
并且所有字段设置为 null
(screen from debugging)。我什至无法从 SecurityContextHolder.getContext().getAuthentication().getName()
.
获取用户名
在我评论 Redis 代码和 Maven 依赖项后它起作用了 (see debug screen)。如何让它与 Spring Session 和 Redis 服务器一起工作?
这是应用程序中的一些代码:
检查委托人的一些示例方法
@RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(@AuthenticationPrincipal CustomUserDetails user) {
User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (user != null) {
String name = user.getUsername();
return new StatusData(name);
} else return new StatusData(null);
}
应用程序和 Redis 配置:
@Configuration
@EnableRedisHttpSession
public class AppConfig {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$");
return serializer;
}
@Bean
public ShaPasswordEncoder shaEncoder() {
return new ShaPasswordEncoder(256);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean(name = "messageSource")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasename("messages/messages");
return resourceBundleMessageSource;
}
@Bean
public Validator basicValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
public AppConfig() {
DateTimeZone.setDefault(DateTimeZone.UTC);
}
}
初始化器(用于Redis Session)
public class Initializer extends AbstractHttpSessionApplicationInitializer {
}
SecurityInitializer(用于 Redis 会话)
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(WebSecurityConfig.class, AppConfig.class);
}
}
WebSecurityConfig(Spring 安全配置)
@Configuration
@EnableWebSecurity
//@EnableWebMvcSecurity
@ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService customUserDetailsService;
@Autowired
private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;
@Autowired
private AuthSuccessHandler authSuccessHandler;
@Autowired
private AuthFailureHandler authFailureHandler;
@Autowired
private HttpLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* Persistent token repository stored in database. Used for remember me feature.
*/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
/**
* Enable always remember feature.
*/
@Bean
public AbstractRememberMeServices rememberMeServices() {
CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
rememberMeServices.setAlwaysRemember(true);
rememberMeServices.setTokenValiditySeconds(1209600);
return rememberMeServices;
}
/**
* Configure spring security to use in REST API.
* Set handlers to immediately return HTTP status codes.
* Enable remember me tokens.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(httpAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and()
.logout()
.permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
.and()
.rememberMe().rememberMeServices(rememberMeServices())
.and()
.headers()
.addHeaderWriter(new HeaderWriter() {
/**
* Header to allow access from javascript AJAX in chrome extension.
*/
@Override
public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
String corsUrl = "https://mail.google.com";
if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
}
}
});
}
/**
* Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
Maven 依赖关系
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>models</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.3.Final</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>3.1.0.CR1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.ganyo</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我解决了这个问题。原来是Spring-Session序列化了Principal对象。我对 UserDetails
的自定义实现是 Hibernate 模型 User
class 的子 class。我通过在我的自定义 UserDetails
、User
模型和该模型中使用的所有 classes 中实现 Serializable
接口来解决它。
为了让它在我的案例中起作用,我还必须确保 Servlet 过滤器的设置顺序正确。
对我来说是:
...
<filter-name>CharacterEncodingFilter</filter-name>
...
<filter-name>springSessionRepositoryFilter</filter-name>
...
<filter-name>springSecurityFilterChain</filter-name>
...
<filter-name>csrfFilter</filter-name>
...
在那之后,principal
不再是空的了。
正如@yglodt 所说,问题是过滤器在 spring 安全过滤器链中的顺序。
在Java配置方式中,只需将Redis配置设置为更高的优先级即可class
@Configuration
@EnableRedisHttpSession
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisConfig extends AbstractHttpSessionApplicationInitializer {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
我设置了最高的优先级,但也许较低的优先级就足够了。
现在应该正确填充主体。
HttpSecurity 链的顺序很重要:
不起作用,主体名称为空:
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.anyRequest().authenticated()
工作正确:
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/register").permitAll()
编辑:2022 这个答案已经过时,会根据@BendaThierry.com
抛出一个 IllegalStateException
我正在使用 Spring Boot v1.3.3 构建 rest API。 API 受 Spring 安全保护。我已经实现了自定义用户详细信息服务以在身份验证上下文中具有自定义主体。
我需要与其他 Spring 应用程序共享 API 会话,因此我选择使用本教程 [在我的应用程序中实施 Spring 与 Redis 服务器的会话=59=].io/spring-session/docs/current/reference/html5/guides/security.html.不幸的是,它导致 Authentication Principal 停止工作。当我试图通过注释 @AuthenticationPrincipal CustomUserDetails user
或 SecurityContextHolder.getContext().getAuthentication().getPrincipal()
获取当前主体时,它 returns 我的自定义用户详细信息,但 Id = 0
并且所有字段设置为 null
(screen from debugging)。我什至无法从 SecurityContextHolder.getContext().getAuthentication().getName()
.
在我评论 Redis 代码和 Maven 依赖项后它起作用了 (see debug screen)。如何让它与 Spring Session 和 Redis 服务器一起工作?
这是应用程序中的一些代码:
检查委托人的一些示例方法
@RequestMapping(value = "/status", method = RequestMethod.GET)
public StatusData status(@AuthenticationPrincipal CustomUserDetails user) {
User user2 = (CustomUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
if (user != null) {
String name = user.getUsername();
return new StatusData(name);
} else return new StatusData(null);
}
应用程序和 Redis 配置:
@Configuration
@EnableRedisHttpSession
public class AppConfig {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName("JSESSIONID");
serializer.setCookiePath("/");
serializer.setDomainNamePattern("^.+?\.(\w+\.[a-z]+)$");
return serializer;
}
@Bean
public ShaPasswordEncoder shaEncoder() {
return new ShaPasswordEncoder(256);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean(name = "messageSource")
public ResourceBundleMessageSource messageSource() {
ResourceBundleMessageSource resourceBundleMessageSource = new ResourceBundleMessageSource();
resourceBundleMessageSource.setBasename("messages/messages");
return resourceBundleMessageSource;
}
@Bean
public Validator basicValidator() {
LocalValidatorFactoryBean validator = new LocalValidatorFactoryBean();
validator.setValidationMessageSource(messageSource());
return validator;
}
public AppConfig() {
DateTimeZone.setDefault(DateTimeZone.UTC);
}
}
初始化器(用于Redis Session)
public class Initializer extends AbstractHttpSessionApplicationInitializer {
}
SecurityInitializer(用于 Redis 会话)
public class SecurityInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityInitializer() {
super(WebSecurityConfig.class, AppConfig.class);
}
}
WebSecurityConfig(Spring 安全配置)
@Configuration
@EnableWebSecurity
//@EnableWebMvcSecurity
@ComponentScan(basePackageClasses = {UserRepository.class, CustomUserDetailsService.class})
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Autowired
private UserDetailsService customUserDetailsService;
@Autowired
private HttpAuthenticationEntryPoint httpAuthenticationEntryPoint;
@Autowired
private AuthSuccessHandler authSuccessHandler;
@Autowired
private AuthFailureHandler authFailureHandler;
@Autowired
private HttpLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
/**
* Persistent token repository stored in database. Used for remember me feature.
*/
@Bean
public PersistentTokenRepository tokenRepository() {
JdbcTokenRepositoryImpl db = new JdbcTokenRepositoryImpl();
db.setDataSource(dataSource);
return db;
}
/**
* Enable always remember feature.
*/
@Bean
public AbstractRememberMeServices rememberMeServices() {
CustomTokenPersistentRememberMeServices rememberMeServices = new CustomTokenPersistentRememberMeServices("xxx", customUserDetailsService, tokenRepository());
rememberMeServices.setAlwaysRemember(true);
rememberMeServices.setTokenValiditySeconds(1209600);
return rememberMeServices;
}
/**
* Configure spring security to use in REST API.
* Set handlers to immediately return HTTP status codes.
* Enable remember me tokens.
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(httpAuthenticationEntryPoint)
.and()
.authorizeRequests()
.antMatchers("/cookie", "/register", "/redirect/**", "/track/**")
.permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.successHandler(authSuccessHandler)
.failureHandler(authFailureHandler)
.and()
.logout()
.permitAll().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler)
.and()
.rememberMe().rememberMeServices(rememberMeServices())
.and()
.headers()
.addHeaderWriter(new HeaderWriter() {
/**
* Header to allow access from javascript AJAX in chrome extension.
*/
@Override
public void writeHeaders(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse) {
String corsUrl = "https://mail.google.com";
if (httpServletRequest.getHeader("Origin") != null && httpServletRequest.getHeader("Origin").equals(corsUrl)) {
httpServletResponse.setHeader("Access-Control-Allow-Origin", "https://mail.google.com");
httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS");
httpServletResponse.setHeader("Access-Control-Allow-Credentials", "true");
httpServletResponse.setHeader("Access-Control-Expose-Headers", "Location");
}
}
});
}
/**
* Set custom user details service to allow for store custom user details and set password encoder to BCrypt.
*/
@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
auth
.userDetailsService(customUserDetailsService).passwordEncoder(bCryptPasswordEncoder);
}
}
Maven 依赖关系
<dependencies>
<dependency>
<groupId>${project.groupId}</groupId>
<artifactId>models</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-validator</artifactId>
<version>5.2.3.Final</version>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<dependency>
<groupId>org.jadira.usertype</groupId>
<artifactId>usertype.core</artifactId>
<version>3.1.0.CR1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.jaxrs</groupId>
<artifactId>jackson-jaxrs-json-provider</artifactId>
<version>2.2.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-joda</artifactId>
</dependency>
<dependency>
<groupId>com.maxmind.geoip2</groupId>
<artifactId>geoip2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>com.ganyo</groupId>
<artifactId>gcm-server</artifactId>
<version>1.0.2</version>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session</artifactId>
<version>1.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<version>4.0.4.RELEASE</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.jayway.jsonpath</groupId>
<artifactId>json-path</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
我解决了这个问题。原来是Spring-Session序列化了Principal对象。我对 UserDetails
的自定义实现是 Hibernate 模型 User
class 的子 class。我通过在我的自定义 UserDetails
、User
模型和该模型中使用的所有 classes 中实现 Serializable
接口来解决它。
为了让它在我的案例中起作用,我还必须确保 Servlet 过滤器的设置顺序正确。
对我来说是:
...
<filter-name>CharacterEncodingFilter</filter-name>
...
<filter-name>springSessionRepositoryFilter</filter-name>
...
<filter-name>springSecurityFilterChain</filter-name>
...
<filter-name>csrfFilter</filter-name>
...
在那之后,principal
不再是空的了。
正如@yglodt 所说,问题是过滤器在 spring 安全过滤器链中的顺序。
在Java配置方式中,只需将Redis配置设置为更高的优先级即可class
@Configuration
@EnableRedisHttpSession
@Order(Ordered.HIGHEST_PRECEDENCE)
public class RedisConfig extends AbstractHttpSessionApplicationInitializer {
@Bean
public JedisConnectionFactory connectionFactory() {
return new JedisConnectionFactory();
}
}
我设置了最高的优先级,但也许较低的优先级就足够了。
现在应该正确填充主体。
HttpSecurity 链的顺序很重要:
不起作用,主体名称为空:
.authorizeRequests()
.antMatchers("/api/register").permitAll()
.anyRequest().authenticated()
工作正确:
.authorizeRequests()
.anyRequest().authenticated()
.antMatchers("/api/register").permitAll()
编辑:2022 这个答案已经过时,会根据@BendaThierry.com
抛出一个IllegalStateException