SpringBoot Security--Session管理与RememberMe

摘要

  • 本文介绍在SpringBoot Security中的Session管理与RememberMe

  • 实现了基于内存、Jdbc和Redis三种配置方式

  • 本文基于SpringBoot-2.7.14和SpringBoot-3.1.2

Security配置类

SpringBoot-2.7.14

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package com.hanqf.config;

import com.hanqf.common.security.CP_UserDetailsService;
import com.hanqf.common.support.CP_ImageFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
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.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

/**
* <h1>springboot升级到2.7.x以后的配置方法</h1>
*/

@Slf4j
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringSecurityConfig {


/**
* session注册器,默认是基于内存的SessionRegistryImpl,也可以配置为jdbc或redis,下文会介绍
*/
@Autowired
private SessionRegistry sessionRegistry;

/**
* Token存储库,用于记录remember me的用户信息,默认是基于内存的InMemoryTokenRepositoryImpl,也可以配置为jdbc或redis,下文会介绍
*/
@Autowired
private PersistentTokenRepository persistentTokenRepository;

/**
* 验证码过滤器,负责登录时验证用户提交的验证码是否有效,本文对此不做介绍
*/
@Autowired
private CP_ImageFilter imageFilter;

/**
* 不需要进行验证的url数组
*/
private String[] ignorings = { "/login.do*", "/**/*.json*", "/**/*.xml*", "/druid/**",
"/forgotPassword.do", "/forgotPasswordEmail.do", "/resetPassword.do"
};

/**
* AuthenticationManager(认证管理器)
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
log.info("AuthenticationManager");
return authenticationConfiguration.getAuthenticationManager();
}

/**
* 定义一个能够与 HttpServletRequest 匹配的过滤器链。以确定它是否适用于该请求。
* springboot升级到2.7.x以后的配置方式
*/
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("HttpSecurity");
//解决不允许显示在iframe中的问题
http.headers().frameOptions().disable();

// 设置拦截规则
http.authorizeRequests()
// 不需要验证的url
.antMatchers(ignorings).permitAll()
// 登录即可访问的url
.antMatchers("/").authenticated()
.antMatchers("/index.do*").authenticated()
.antMatchers("/welcome.do*").authenticated()
// 自定义规则进行验证,基于权限管理模型的认证,public Boolean hasPerssion(HttpServletRequest request, Authentication authentication),本文不做介绍
.antMatchers("/**/*.do*").access("@rbacService.hasPerssion(request,authentication)")
.and()
// 登录成功但是权限验证失败后的跳转地址
.exceptionHandling().accessDeniedPage("/access/denied.do");


// 开启默认登录页面
// http.formLogin();

// 自定义登录页面
http.formLogin()
.loginPage("/login.do") // 登录页面
.failureUrl("/login.do?login_error=1") // 登录失败跳转页面
.defaultSuccessUrl("/index.do", true) // 登录成功默认跳转页面,这里设置true表示无论请求哪个地址,登录成功后都跳转到该页面
.loginProcessingUrl("/j_spring_security_check") // 登录页面中的提交登录验证url, 默认 /login
.usernameParameter("j_username") // 登录页面中的用户名参数,默认username
.passwordParameter("j_password") // 登录页面中的密码参数,默认password
.permitAll();

//关闭csrf,如果默认开启csrf,则在生成页面时会自动在每个form中增加一个隐藏属性<input type="hidden" name="_csrf" value="95e8706b-8d22-4d62-9a27-3da5993e0a7d">,
// 实际上就是<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />,js中如果需要使用时也可以使用该属性
//http.csrf().disable();

//开启csrf,默认开启,csrf不会拦截get请求
http.csrf()
//.csrfTokenRepository(new CookieCsrfTokenRepository()) //令牌存储方式,CookieCsrfTokenRepository或者HttpSessionCsrfTokenRepository,默认HttpSessionCsrfTokenRepository
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) //关闭仅支持http浏览器请求,这样ajax和postMan都可以访问,header需要带上X-CSRF-TOKEN
.ignoringAntMatchers("/**/json.do*", "/**/xml.do*"); //哪些不需要csrf,非浏览器直接访问的地址需要进行屏蔽,因为csrf标签只有页面会自动生成,另外认证接口也需要屏蔽,因为需要第一次获取CSRF-TOKEN

// 自定义注销,这里需要注意的是,如果启用了csrf(默认就是开启),则logout只能是post提交,如果要get提交,则必须如下配置
http.logout()
// 在注销时清除认证信息
.clearAuthentication(true)
.logoutRequestMatcher(new AntPathRequestMatcher("/logout.do")) //get
//.logoutUrl("/logout.do") //post
.logoutSuccessUrl("/login.do")
// 在注销时使HttpSession失效
.invalidateHttpSession(true);


// session管理
http.sessionManagement()
//默认开启session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
//每次登录验证将创建一个新的session
.sessionFixation().migrateSession()
//同一个用户最大的登录数量
.maximumSessions(1)
//true表示已经登录就不予许再次登录,false表示允许再次登录但是之前的登录会下线。
.maxSessionsPreventsLogin(false)
//session被下线(超时)之后的显示页面
.expiredUrl("/access/sameLogin.do")
// 会话注册器
.sessionRegistry(sessionRegistry);

// sessionRegistry是session注册器,默认是基于内存的SessionRegistryImpl,此时当用户注销时,Spring Security的默认行为是不会主动从SessionRegistry中移除相关的SessionInformation对象。这意味着注销后,SessionRegistryImpl中仍然保留该用户的会话信息。
// 但是SessionRegistryImpl里对session的销毁和改变进行了事件监听,我们只需要注册事件发布者HttpSessionEventPublisher即可实现session的自动清理。
//SessionRegistryImpl使用map来维护session信息,这样在分布式系统(多个副本)中获取会话数就不准确了,
// 所以这里我们可以使用基于SpringSession的SpringSessionBackedSessionRegistry,其可以绑定基于jdbc的JdbcIndexedSessionRepository或者基于redis的RedisIndexedSessionRepository
// 如果使用SpringSessionBackedSessionRegistry,这里就不需要HttpSessionEventPublisher的Bean,因为这里Session管理全部交由SpringSession去管理,也就是Session的相关清理工作会自动帮助我们完成。


// RemeberMe
//http.rememberMe().key("webmvc#FD637E6D9C0F1A5A67082AF56CE32485");
//两周内免登录
http.rememberMe()
.rememberMeParameter("_spring_security_remember_me") // 默认 remember-me
.rememberMeCookieName("remember-me-cookie") //保存在浏览器端的cookie的名称,如果不设置默认也是remember-me
.tokenValiditySeconds(60 * 60 * 24 * 14) // 单位秒 两周=60*60*24*14
.tokenRepository(persistentTokenRepository); //Token存储库,用于记录remember me的用户信息,默认内存,也可以配置为数据库或者redis;

// 验证码过滤器
http.addFilterBefore(imageFilter, UsernamePasswordAuthenticationFilter.class);
// 设置userDetailsService
http.userDetailsService(userDetailsService());
return http.build();
}


/**
* ignore的url
* 如果你需要忽略URL,请考虑通过HttpSecurity.authorizeHttpRequests的permitAll来实现。
*/
//@Bean
//public WebSecurityCustomizer webSecurityCustomizer() {
// return web -> web.ignoring().antMatchers(ignorings);
//}



/**
* 登录的时候需要获取用户信息,只有登录的时候使用,其余时候使用http.authorizeRequests()中配置的验证规则(验证时,用户名和权限都确定了)
*/
@Bean
public CP_UserDetailsService userDetailsService() {
log.info("CP_UserDetailsService");
CP_UserDetailsService userDetailsService = new CP_UserDetailsService();
return userDetailsService;
}

/**
* 认证日志
*/
@Bean
public org.springframework.security.authentication.event.LoggerListener loggerListener() {
log.info("org.springframework.security.authentication.event.LoggerListener");
org.springframework.security.authentication.event.LoggerListener loggerListener = new org.springframework.security.authentication.event.LoggerListener();
return loggerListener;
}
/**
* 请求日志
*/
@Bean
public org.springframework.security.access.event.LoggerListener eventLoggerListener() {
log.info("org.springframework.security.access.event.LoggerListener");
org.springframework.security.access.event.LoggerListener eventLoggerListener = new org.springframework.security.access.event.LoggerListener();
return eventLoggerListener;
}

/**
* 密码加密,在进行登录验证时会自动将页面提交的密码通过其进行加密
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}

}

SpringBoot-3.1.2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package com.hanqf.config;

import com.hanqf.support.security.CP_ImageFilter;
import com.hanqf.support.security.CP_RbacService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.event.LoggerListener;
import org.springframework.security.authorization.AuthorizationDecision;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.util.Arrays;
import java.util.List;

/**
* <h1>springboot升级到3.x.x以后的配置方法</h1>
*/

@Slf4j
@Configuration
@EnableMethodSecurity
public class SpringSecurityConfig {


@Autowired
private SessionRegistry sessionRegistry;

@Autowired
private PersistentTokenRepository persistentTokenRepository;

@Autowired
private CP_RbacService rbacService;

@Autowired
private CP_ImageFilter imageFilter;


private static String[] ignorings = {"/email/**", "/actuator*/**", "/admin*/**", "/logger**",
"/rabbitmq/**", "/checkcode/**", "/resource/**", "/**/*.jsp",
"/access/sameLogin.do", "/**/*.json*", "/**/*.xml*", "/druid/**",
"/forgotPassword.do", "/forgotPasswordEmail.do", "/resetPassword.do"
};

private static List<AntPathRequestMatcher> ignoringsMatcherList;

static {
ignoringsMatcherList = Arrays.stream(ignorings).map(AntPathRequestMatcher::antMatcher).toList();
}

/**
* 获取AuthenticationManager(认证管理器),登录时认证使用
*
* @param authenticationConfiguration
* @return
* @throws Exception
*/
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception {
log.info("AuthenticationManager");
return authenticationConfiguration.getAuthenticationManager();
}


@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
log.info("HttpSecurity");
//解决不允许显示在iframe的问题
http.headers(httpSecurityHeadersConfigurer -> httpSecurityHeadersConfigurer
.frameOptions(HeadersConfigurer.FrameOptionsConfig::disable));

// 设置拦截规则
http.authorizeHttpRequests(authorizationManagerRequestMatcherRegistry -> authorizationManagerRequestMatcherRegistry
.requestMatchers(ignoringsMatcherList.toArray(AntPathRequestMatcher[]::new)).permitAll()
.requestMatchers(AntPathRequestMatcher.antMatcher("/login.do*")).permitAll() // 登录请求不拦截
.requestMatchers(AntPathRequestMatcher.antMatcher("/index.do*")).authenticated()
.requestMatchers(AntPathRequestMatcher.antMatcher("/*.do*"), AntPathRequestMatcher.antMatcher("/**/*.do*")).access((authentication, context) ->
new AuthorizationDecision(rbacService.hasPerssion(context.getRequest(),authentication.get()))));
// 这种方式效果同上,都是开启自定义认证
// .requestMatchers(AntPathRequestMatcher.antMatcher("/*.do*"),AntPathRequestMatcher.antMatcher("/**/*.do*")).access(webExpressionAuthorizationManager()));

http.exceptionHandling(exceptionHandlingCustomizer -> exceptionHandlingCustomizer
.accessDeniedPage("/access/denied.do"));


// 开启默认登录页面
http.formLogin(httpSecurityFormLoginConfigurer -> httpSecurityFormLoginConfigurer
.loginPage("/login.do")
.failureUrl("/login.do?login_error=1")
.defaultSuccessUrl("/index.do", true)
.loginProcessingUrl("/j_spring_security_check") // 默认 /login
.usernameParameter("j_username") // 默认username
.passwordParameter("j_password") // 默认password
.permitAll());


//开启csrf,默认开启,csrf不会拦截get请求
http.csrf(httpSecurityCsrfConfigurer -> httpSecurityCsrfConfigurer
//关闭仅支持http浏览器请求,这样ajax和postMan都可以访问,header需要带上X-CSRF-TOKEN
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
//哪些不需要csrf,非浏览器直接访问的地址需要进行屏蔽,因为csrf标签只有页面会自动生成,另外认证接口也需要屏蔽,因为需要第一次获取CSRF-TOKEN
.ignoringRequestMatchers(AntPathRequestMatcher.antMatcher("/**/json.do*"), AntPathRequestMatcher.antMatcher("/**/xml.do*")));

// 自定义注销,这里需要注意的是,如果启用了csrf(默认就是开启),则logout只能是post提交,如果要get提交,则必须如下配置
http.logout(httpSecurityLogoutConfigurer -> httpSecurityLogoutConfigurer
// 在注销时清除认证信息
.clearAuthentication(true)
.logoutRequestMatcher(AntPathRequestMatcher.antMatcher("/logout.do")) //get
//.logoutUrl("/logout.do") //post
.logoutSuccessUrl("/login.do")
// 在注销时使HttpSession失效
.invalidateHttpSession(true));


// session管理
http.sessionManagement(httpSecuritySessionManagementConfigurer -> httpSecuritySessionManagementConfigurer
//默认开启session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
//每次登录验证将创建一个新的session
.sessionFixation().migrateSession()
//同一个用户最大的登录数量
.maximumSessions(1)
//true表示已经登录就不予许再次登录,false表示允许再次登录但是之前的登录会下线。
.maxSessionsPreventsLogin(false)
//session被下线(超时)之后的显示页面
.expiredUrl("/access/sameLogin.do")
.sessionRegistry(sessionRegistry));


// RemeberMe 两周内免登录
http.rememberMe(httpSecurityRememberMeConfigurer -> httpSecurityRememberMeConfigurer
.rememberMeParameter("_spring_security_remember_me") // 默认 remember-me
.rememberMeCookieName("remember-me-cookie") //保存在浏览器端的cookie的名称,如果不设置默认也是remember-me
.tokenValiditySeconds(60 * 60 * 24 * 14)
.tokenRepository(persistentTokenRepository));

//设置userDetailsService
http.userDetailsService(userDetailsService());

// 设置过滤器,这里是验证码过滤器
http.addFilterBefore(imageFilter, UsernamePasswordAuthenticationFilter.class);

return http.build();
}

/**
* web表达式认证管理器,支持自定义认证
*/
// private WebExpressionAuthorizationManager webExpressionAuthorizationManager() {
// final var expressionHandler = new DefaultHttpSecurityExpressionHandler();
// expressionHandler.setApplicationContext(applicationContext);
// final var authorizationManager = new WebExpressionAuthorizationManager("@rbacService.hasPerssion(request,authentication)");
// // 一定要设置expressionHandler,否则不生效
// authorizationManager.setExpressionHandler(expressionHandler);
// return authorizationManager;
// }


/**
* 基于内存的userDetailsService
*/
@Bean
public InMemoryUserDetailsManager userDetailsService() {
UserDetails user = User.builder()
.username("user")
// 123456
.password("$2a$10$JHj.XB.5RtpY60JEuTTGjuIT4m.hYT1yWoevJ6inU6Q7JE1qcvTYC")
.roles("USER")
.build();

UserDetails admin = User.builder()
.username("admin")
// 123456
.password("$2a$10$JHj.XB.5RtpY60JEuTTGjuIT4m.hYT1yWoevJ6inU6Q7JE1qcvTYC")
.roles("ADMIN")
.build();

return new InMemoryUserDetailsManager(user, admin);
}

/**
* 认证日志
*/
@Bean
public LoggerListener loggerListener() {
log.info("org.springframework.security.authentication.event.LoggerListener");
return new LoggerListener();
}

/**
* 密码加密策略
*/
@Bean
public PasswordEncoder passwordEncoder() {
log.info("BCryptPasswordEncoder");
return new BCryptPasswordEncoder();
}

}

SessionRegistry–Session注册器

基于内存–SessionRegistryImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.hanqf.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.web.session.HttpSessionEventPublisher;


/**
* <h1>CommonSessionSpringSecurityConfig</h1>
*/

@Configuration
@Slf4j
public class CommonSessionSpringSecurityConfig {

/**
* 用于跟踪用户的会话信息,包括已经认证的用户和它们的会话(Session)。
* 每当用户成功进行身份认证并建立了一个新的会话时,SessionRegistry将负责将该会话添加到其内部的数据结构中。
*/
@Bean
public SessionRegistry sessionRegistry() {
log.info("CommonSessionRegistry");
return new SessionRegistryImpl();
}

/**
* session事件发布者
* 如果使用SpringSessionBackedSessionRegistry,这里就不需要HttpSessionEventPublisher的Bean,而是交由SpringSession来管理
*/
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher() {
return new HttpSessionEventPublisher();
}
}

基于Spring-Session的jdbc

  • maven依赖

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-jdbc</artifactId>
</dependency>
  • 建表语句
    建表语句可在spring-session-jdbc-[version].jarorg.springframework.session.jdbc包路径中查看

1
2
3
4
5
6
7
8
9
10
11
12
package com.hanqf.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
/**
* <h1>jdbc-session</h1>
*/

@Configuration
@EnableJdbcHttpSession
public class JdbcSessionConfig {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.hanqf.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.session.jdbc.JdbcIndexedSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;

/**
* <h1>JdbcSessionSpringSecurityConfig</h1>
*/
@Slf4j
@Configuration
@AutoConfigureAfter(JdbcSessionConfig.class)
public class JdbcSessionSpringSecurityConfig {

@Autowired
private JdbcIndexedSessionRepository sessionRepository;

@Bean
public SessionRegistry springSessionBackedSessionRegistry() {
log.info("JdbcSessionRegistry");
return new SpringSessionBackedSessionRegistry<>(sessionRepository);
}
}

基于Spring-Session的redis

1
2
3
4
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
1
2
3
4
5
6
7
8
9
10
11
package com.hanqf.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
/**
* RedisSessionConfig
*/
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.hanqf.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.session.security.SpringSessionBackedSessionRegistry;

/**
* <h1>RedisSessionSpringSecurityConfig</h1>
*/
@Slf4j
@Configuration
@AutoConfigureAfter(RedisSessionConfig.class)
public class RedisSessionSpringSecurityConfig {

@Autowired
private RedisIndexedSessionRepository sessionRepository;

@Bean
public SessionRegistry springSessionBackedSessionRegistry() {
log.info("RedisSessionRegistry");
return new SpringSessionBackedSessionRegistry<>(sessionRepository);
}

}

获取当前所有登录用户信息

1
2
3
4
5
6
7
8
9
10
11
package com.hanqf.common.session;

import java.util.Date;
import java.util.Map;

/**
* <h1>HttpSession服务类</h1>
*/
public interface HttpSessionService {
Map<String, Date> getAllPrincipals();
}

基于内存–SessionRegistryImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.hanqf.config;

import com.hanqf.common.session.HttpSessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.session.SessionInformation;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

@Component
public class CommonHttpSessionService implements HttpSessionService {

@Autowired
SessionRegistry sessionRegistry;

@Override
public Map<String, Date> getAllPrincipals() {
Map<String, Date> lastActivityDates = new HashMap<>();
for (Object principal : sessionRegistry.getAllPrincipals()) {
String username = "";
if (principal instanceof User) {
username = ((User) principal).getUsername();
}
// a principal may have multiple active sessions
for (SessionInformation session : sessionRegistry.getAllSessions(
principal, false)) {
// no last activity stored
if (lastActivityDates.get(username) == null) {
lastActivityDates.put(username, session.getLastRequest());
} else {
// check to see if this session is newer than the last
// stored
Date prevLastRequest = lastActivityDates.get(username);
if (session.getLastRequest().after(prevLastRequest)) {
// update if so
lastActivityDates.put(username, session.getLastRequest());
}
}
}
}
return lastActivityDates;
}
}

基于Spring-Session的jdbc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
package com.hanqf.config;

import com.hanqf.common.session.HttpSessionService;
import com.hanqf.function.session.dao.SpringSessionJpaRepository;
import com.hanqf.function.session.model.SpringSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Component
public class JdbcHttpSessionService implements HttpSessionService {

/**
* 基于JPA的dao对象,对应表为spring_session
*/
@Autowired
private SpringSessionJpaRepository springSessionJpaRepository;

@Override
public Map<String, Date> getAllPrincipals() {
Map<String, Date> map = new HashMap<>();

try {
final List<SpringSession> sessionList = springSessionJpaRepository.findSpringSessionsByExpiryTimeAfter(new Date().getTime());
for (SpringSession springSession : sessionList) {
map.put(springSession.getPrincipalName(), new Date(springSession.getLastAccessTime()));
}

} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}

基于Spring-Session的redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
package com.hanqf.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.hanqf.common.session.HttpSessionService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.session.data.redis.RedisIndexedSessionRepository;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;

@Component
public class RedisHttpSessionService implements HttpSessionService {

private final String SESSION_SESSIONS = "spring:session:index:org.springframework.session.FindByIndexNameSessionRepository.PRINCIPAL_NAME_INDEX_NAME:";

@Autowired
private RedisTemplate<String, Object> redisTemplate;

@Autowired
private RedisIndexedSessionRepository sessionRepository;

@Autowired
private ObjectMapper objectMapper;

@Override
public Map<String, Date> getAllPrincipals() {
Map<String, Date> map = new HashMap<>();
final Set<String> keys = redisTemplate.keys(SESSION_SESSIONS + "*");
try {
for (String key : keys) {
String principalName = key.replace(SESSION_SESSIONS, "");
final Map byPrincipalName = sessionRepository.findByPrincipalName(principalName);

if (byPrincipalName != null && byPrincipalName.size() > 0) {
final String json = objectMapper.writeValueAsString(byPrincipalName);
final Map<String, RedisSessionPojo> redisSessionMap = objectMapper.readerForMapOf(RedisSessionPojo.class).readValue(json);
for (String mapKey : redisSessionMap.keySet()) {
final RedisSessionPojo redisSession = redisSessionMap.get(mapKey);
if (!redisSession.getExpired()) {
map.put(principalName, redisSession.getLastAccessedTime());
}
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
return map;
}
}

@Data
public class RedisSessionPojo {
private String id;
private Object attributeNames;
private String maxInactiveInterval;
private Boolean expired;
private Date lastAccessedTime;
private String creationTime;
}

Remember-Me

基于内存–InMemoryTokenRepositoryImpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.hanqf.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.InMemoryTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

@Slf4j
@Configuration
public class CommonRememberMeConfig {

@Bean
public PersistentTokenRepository persistentTokenRepository() {
log.info("InMemoryTokenRepositoryImpl");
InMemoryTokenRepositoryImpl tokenRepository = new InMemoryTokenRepositoryImpl();
return tokenRepository;
}
}

基于jdbc

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
package com.hanqf.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import javax.sql.DataSource;

@Slf4j
@Configuration
public class JdbcRememberMeConfig {

@Autowired
private DataSource dataSource;

/**
* RemeberMe
* 配置从数据库中获取token
*
* CREATE TABLE `persistent_logins` (
* `username` varchar(64) NOT NULL,
* `series` varchar(64) NOT NULL,
* `token` varchar(64) NOT NULL,
* `last_used` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
* PRIMARY KEY (`series`)
* ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
log.info("JdbcTokenRepositoryImpl");
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
//自动创建表,第一次运行时可是设置为true,让其自动创建表
tokenRepository.setCreateTableOnStartup(false);
tokenRepository.setDataSource(dataSource);
return tokenRepository;
}
}

基于redis

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
package com.hanqf.config;

import com.hanqf.common.RedisTokenRepositoryImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

@Slf4j
@Configuration
@AutoConfigureAfter(RedisConfig.class)
public class RedisRememberMeConfig {

@Autowired
private RedisTemplate<String ,Object> redisTemplate;

/**
* RemeberMe
* redis,springSecurity没有提供基于redis的PersistentTokenRepository,需要我们自己创建
*/
@Bean
public PersistentTokenRepository persistentTokenRepository() {
log.info("RedisTokenRepositoryImpl");
RedisTokenRepositoryImpl tokenRepository = new RedisTokenRepositoryImpl(redisTemplate);
return tokenRepository;
}
}
  • 自定义基于Redis的PersistentTokenRepository

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package com.hanqf.common;

import lombok.extern.slf4j.Slf4j;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.web.authentication.rememberme.PersistentRememberMeToken;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

@Slf4j
public class RedisTokenRepositoryImpl implements PersistentTokenRepository {

// 默认14天:60 * 60 * 24 * 14
@Value("${rememberMe.expireTime:1209600}")
private Integer rememberMeExpireTime;

private final String SERIES_PREFIX = "spring:security:rememberMe:series:";
private final String USERNAME_PREFIX = "spring:security:rememberMe:username:";

private RedisTemplate<String, Object> redisTemplate;

public RedisTokenRepositoryImpl(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}


private String generateKey(String prefix, String var) {
return prefix + var;
}

@Override
public void createNewToken(PersistentRememberMeToken token) {
String key = generateKey(SERIES_PREFIX, token.getSeries());
if (Boolean.TRUE.equals(redisTemplate.persist(key))) {
throw new DataIntegrityViolationException("Series Id '" + token.getSeries() + "' already exists!");
} else {
//创建一个hashmap
Map<String, String> map = new HashMap<>();
map.put("username", token.getUsername());
map.put("tokenValue", token.getTokenValue());
map.put("date", String.valueOf(token.getDate().getTime()));
map.put("series", token.getSeries());

//这里不能直接将PersistentRememberMeToken对象存入redis,因为这里使用的RedisTemplate是基于json的,要求对象必须有无参构造方法以及属性的setter和getter方法
redisTemplate.opsForValue().set(key, map);
redisTemplate.expire(key, rememberMeExpireTime, TimeUnit.SECONDS);
redisTemplate.opsForValue().set(generateKey(USERNAME_PREFIX, token.getUsername()), token.getSeries());
}
}

@Override
public void updateToken(String series, String tokenValue, Date lastUsed) {
PersistentRememberMeToken token = this.getTokenForSeries(series);
if (token != null) {
PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), series, tokenValue, new Date());
//创建一个hashmap
Map<String, String> map = new HashMap<>();
map.put("username", newToken.getUsername());
map.put("tokenValue", newToken.getTokenValue());
map.put("date", String.valueOf(newToken.getDate().getTime()));
map.put("series", newToken.getSeries());

String key = generateKey(SERIES_PREFIX, series);
redisTemplate.opsForValue().set(key, map);
redisTemplate.expire(key, rememberMeExpireTime, TimeUnit.SECONDS);
}
}

@Override
public PersistentRememberMeToken getTokenForSeries(String seriesId) {
Map<String, String> map = (Map) redisTemplate.opsForValue().get(generateKey(SERIES_PREFIX, seriesId));
if (map == null) {
return null;
}
String username = map.get("username");
String tokenValue = map.get("tokenValue");
String date = map.get("date");

if (null == username || null == tokenValue || null == date) {
return null;
}
Long timestamp = Long.valueOf(date);
Date time = new Date(timestamp);

PersistentRememberMeToken rememberMeToken = new PersistentRememberMeToken(username, seriesId, tokenValue, time);
return rememberMeToken;
}

@Override
public void removeUserTokens(String username) {
try {
//可能redis版本低于6用不了getAndDelete
// String series = (String) redisTemplate.opsForValue().getAndDelete(generateKey(USERNAME_PREFIX, username));
String series = (String) redisTemplate.opsForValue().get(generateKey(USERNAME_PREFIX, username));
redisTemplate.delete(generateKey(USERNAME_PREFIX, username));
redisTemplate.delete(generateKey(SERIES_PREFIX, series));
} catch (Exception e) {
log.error(e.getMessage());
}
}
}