一、登录配置
对于表单登录,能配置登录成功和失败的跳转和重定向,Spring Security
通过配置可以实现自定义跳转、重定向,以及用户未登录和登录用户无权限的处理。
1.1、URL配置
1.1.1、添加依赖
org.springframework.boot
spring-boot-starter-thymeleaf
1.1.2、自定义登录页面
在resources/templates
下编写简单test-login.html
登录页面(参考官方文档),内容如下:
登录页面
用户名或密码无效
用户名和密码名称默认是
username
和password
创建登录页面映射Controller
@Controller // 这里使用@Controller,跳转动态页面
public class PageController {
@GetMapping("/user-login")
public String myLoginPage(){
return "test-login.html";
}
}
1.1.3、WebSecurityConfig配置
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 自定义页面的路径不用验证
.antMatchers(HttpMethod.GET, "/user-login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 设置自定义登录的页面
.loginPage("/user-login")
// 登录页表单提交的 action(th:action="@{/my-login}") URL
.loginProcessingUrl("/my-login");
// post请求默认需要csrf验证, 这里使用Thymeleaf模板引擎,表单默认发送csrf,可不用关闭
//.and()
//.csrf().disable();
}
}
启动程序后,访问localhost:8080/hello
,会跳转到自定义登录页面登录成功,在F12
可以看到自动发送csrf
:
其他的登录成功和登录失败参考上面,配置如下:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
// 自定义页面的路径不用验证
.antMatchers(HttpMethod.GET, "/user-login").permitAll()
// 失败跳转不用验证
.antMatchers(HttpMethod.GET, "/user-fail").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
// 设置自定义登录的页面
.loginPage("/user-login")
// 登录页表单提交的 action(th:action="@{/my-login}") URL
.loginProcessingUrl("/my-login");
// .usernameParameter("username") // 默认就是 username
// .passwordParameter("password") // 默认就是 password
/**
* 登录成功跳转:
* 登录成功,如果是直接从登录页面登录,会跳转到该URL;
* 如果是从其他页面跳转到登录页面,登录后会跳转到原来页面。
* 可设置true来任何时候到跳转 .defaultSuccessUrl("/hello2", true);
*/
.defaultSuccessUrl("/hello2");
/**
* 登录成功重定向(和上面二选一)
*/
.successForwardUrl("/hello3")
/**
* 登录失败跳转,指定的路径要能匿名访问
*/
.failureUrl("/login-fail")
/**
* 登录失败重定向(和上面二选一)
*/
.failureForwardUrl("/login-fail");
// post请求需要csrf验证, 这里使用Thymeleaf模板引擎,表单默认发送csrf,可不用关闭
//.and()
//.csrf().disable();
}
}
1.2、登录处理器
上面使用URL进行的配置,都是通过Security默认提供的处理器处理的,一般多用于前后端不分离。
Spring Security
的AuthenticationManager
用来处理身份认证的请求,处理的结果分两种:
- 认证成功:结果由
AuthenticationSuccessHandler
处理 - 认证失败:结果由
AuthenticationFailureHandler
处理。
Spring Security
提供了多个实现于AuthenticationSuccessHandler
接口和CustomAuthenticationFailHandler
接口的子类,想自定义处理器,可以实现接口,或继承接口的实现类来重写。
1.2.1、自定义AuthenticationSuccessHandler
AuthenticationSuccessHandler
是身份验证成功处理器的接口,其下有多个子类:
-
SavedRequestAwareAuthenticationSuccessHandler
:默认的成功处理器,默认验证成功后,跳转到原路径。也可通过defaultSuccessUrl()
配置。 -
SimpleUrlAuthenticationSuccessHandler
:SavedRequestAwareAuthenticationSuccessHandler
的父类,只有指定defaultSuccessUrl()
时,才会被调用。作用:清除原路径,使用defaultSuccessUrl()
指定的路径。如果直接使用该处理器,则总跳转到根路径。 -
ForwardAuthenticationSuccessHandler
:请求重定向。只有指定successForwardUrl
时被用到。
要想自定义成功处理器,可以通过实现AuthenticationSuccessHandler
接口或继承其子类SavedRequestAwareAuthenticationSuccessHandler
来实现:
-
实现
AuthenticationSuccessHandler
接口如果直接返回Json数据时,可以实现
AuthenticationSuccessHandler
接口:public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{ @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 200) .put("msg", "登录成功") .toString()); } }
-
继承
SavedRequestAwareAuthenticationSuccessHandler
类如果只是在登录认证后,需要处理数据,再跳转回原路径时,可以继承该类:
public class CustomAuthenticationSuccessHandler2 extends SavedRequestAwareAuthenticationSuccessHandler { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws ServletException, IOException { // 登录成功后,进行数据处理 System.out.println("用户登录成功啦!!!"); String authenticationStr = objectMapper.writeValueAsString(authentication); System.out.println("用户登录信息打印:" + authenticationStr); //处理完成后,跳转回原请求URL super.onAuthenticationSuccess(request, response, authentication); } }
Spring Security
默认是使用SavedRequestAwareAuthenticationSuccessHandler
,在配置中修改为自定义的AuthenticationSuccessHandler
:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 配置使用自定义成功处理器
.successHandler(new AuthenticationSuccessHandler());
}
}
1.2.2、自定义AuthenticationFailureHandler
AuthenticationFailureHandler
是身份认证失败处理器的接口,其下有多个子类实现:
-
SimpleUrlAuthenticationFailureHandler
:默认的失败处理器,默认认证失败后,跳转到登录页路径加error
参数,如:http://localhost:8080/login?error
。可通过failureUrl()
配置。 -
ForwardAuthenticationFailureHandler
:重定向到指定的URL -
DelegatingAuthenticationFailureHandler
:将AuthenticationException
子类委托给不同的AuthenticationFailureHandler
,意味着可以为AuthenticationException
的不同实例创建不同的行为 -
ExceptionMappingAuthenticationFailureHandler
:可以根据不同的AuthenticationException
类型,设置不同的跳转url
自定义失败处理器,可以通过实现AuthenticationFailureHandler
接口或继承其子类SimpleUrlAuthenticationFailureHandler
来实现:
-
实现
AuthenticationFailureHandler
接口:public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 401) .put("msg", "用户名或密码错误") .toString()); } }
-
继承
SimpleUrlAuthenticationFailureHandler
类public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // 登录失败后,进行数据处理 System.out.println("登录失败啦!!!"); String exceptionStr = objectMapper.writeValueAsString(exception.getMessage()); System.out.println(exceptionStr); // 跳转原页面 super.onAuthenticationFailure(request, response, exception); } }
Spring Security
默认验证失败是使用SimpleUrlAuthenticationFailureHandler
,在配置中修改为自定义的AuthenticationFailureHandler
:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
// 配置使用自定义失败处理器
.failureHandler(new AuthenticationFailureHandler());
}
}
这里顺便提及DelegatingAuthenticationFailureHandler
和ExceptionMappingAuthenticationFailureHandler
的使用:
-
DelegatingAuthenticationFailureHandler
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Bean public DelegatingAuthenticationFailureHandler delegatingAuthenticationFailureHandler(){ LinkedHashMap
, AuthenticationFailureHandler> handlers = new LinkedHashMap<>(); // 登录失败时,使用的失败处理器 handlers.put(BadCredentialsException.class, new BadCredentialsAuthenticationFailureHandler()); // 用户过期时,使用的失败处理器 handlers.put(AccountExpiredException.class, new AccountExpiredAuthenticationFailureHandler()); // 用户被锁定时,使用的失败处理 handlers.put(LockedException.class, new LockedAuthenticationFailureHandler()); return new DelegatingAuthenticationFailureHandler(handlers, new AuthenticationFailureHandler()); } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() // 配置使用自定义失败处理器 .failureHandler(delegatingAuthenticationFailureHandler()); } } -
ExceptionMappingAuthenticationFailureHandler
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Bean public ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler(){ ExceptionMappingAuthenticationFailureHandler handler = new ExceptionMappingAuthenticationFailureHandler(); HashMap
map = new HashMap<>(); // 登录失败时,跳转到 /badCredentials map.put(BadCredentialsException.class.getName(), "/badCredentials"); // 用户过期时,跳转到 /accountExpired map.put(AccountExpiredException.class.getName(), "/accountExpired"); // 用户被锁定时,跳转到 /locked map.put(LockedException.class.getName(), "/locked"); handler.setExceptionMappings(map); return handler; } @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin() // 配置使用自定义失败处理器 .failureHandler(exceptionMappingAuthenticationFailureHandler()); } }
1.3、认证入口
AuthenticationEntryPoint
是Spring Security
认证入口点接口,在用户请求处理过程中遇到认证异常时,使用特定认证方式进行认证。
AuthenticationEntryPoint
内置实现类:
LoginUrlAuthenticationEntryPoint
:根据配置的登录页面url
,将用户重定向到该登录页面进行认证。默认的认证方式。-
Http403ForbiddenEntryPoint
:设置响应状态为403
,不触发认证。通常在预身份认证中设置在某些情况下,使用Spring Security进行授权,但是在访问该应用程序之前,某些外部系统已经对该用户进行了可靠的身份验证。这些情况称为“预身份验证(pre-authenticated)”。
HttpStatusEntryPoint
:设置特定的响应状态码,不触发认证。BasicAuthenticationEntryPoint
:设置基本(Http Basic
)认证,在响应状态码401
和Header
为WWW-Authenticate:"Basic realm="xxx"
时使用。DigestAuthenticationEntryPoint
:设置摘要(Http Digest
)认证,在响应状态码401
和Header
为WWW-Authenticate:"Digest realm="xxx"
时使用。DelegatingAuthenticationEntryPoint
:根据匹配URI来委托给不同的AuthenticationEntryPoint
,且必须制定一个默认的认证方式。
1.3.1、自定义AuthenticationEntryPoint
-
自定义处理,需要新建类实现该
AuthenticationEntryPoint
接口:public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.getWriter().append( new ObjectMapper().createObjectNode() .put("status", 401) .put("msg", "未登录,请登录后访问") .toString()); } }
-
WebSecurityConfig
配置:@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { ... @Override protected void configure(HttpSecurity http) throws Exception { // 指定未登录入口点 http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint()); ... } }
其它子类的用法:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Bean
public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
LinkedHashMap map = new LinkedHashMap<>();
// GET方式请求/test时,直接返回 403
map.put(new AntPathRequestMatcher("/test", "GET"), new Http403ForbiddenEntryPoint());
// 访问 /basic时,直接返回 400 bad request
map.put(new AntPathRequestMatcher("/basic"),
new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map);
// 除了上面两个 uri 配置指定的认证入口,其它默认使用 LoginUrlAuthenticationEntryPoint认证入口
entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint("/user-login"));
return entryPoint;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
/**
* Http403ForbiddenEntryPoint 用法
*/
// http.exceptionHandling()
// .authenticationEntryPoint(new Http403ForbiddenEntryPoint());
/**
* HttpStatusEntryPoint 用法
*/
// http.exceptionHandling()
// .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
/**
* DelegatingAuthenticationEntryPoint 用法
*/
http.exceptionHandling()
.authenticationEntryPoint(delegatingAuthenticationEntryPoint());
...
}
}
而对于摘要认证DigestAuthenticationEntryPoint
,因为Http
摘要认证必须基于MD5
或明文,不能使用其它加密方式,且加密方式是MD5(username:realm:password)
,所以我们需要手动加密用户密码:
public int addUser(UserInfo userInfo) throws NoSuchAlgorithmException {
String username = userInfo.getUsername();
String password = userInfo.getPassword();
// 加密密码
MessageDigest md5 = MessageDigest.getInstance("MD5");
String realm = "realm"; // 默认是 readlm
String userData = username + ":" + realm + ":" + password;
password = new String(Hex.encode(md5.digest(userData.getBytes())));
userInfo.setPassword(password);
return userMapper.addUser(userInfo);
}
在WebSecurityConfig
配置中:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Bean
public PasswordEncoder passwordEncoder() {
// 因为已经使用摘要认证MD5加密,不用再加密,所以这里设置为明文
return new PasswordEncoder() {
@Override
public String encode(CharSequence charSequence) {
return charSequence.toString();
}
@Override
public boolean matches(CharSequence charSequence, String s) {
return s.equals(charSequence.toString());
}
};
}
// 摘要认证的过滤器
@Bean
public DigestAuthenticationFilter digestAuthenticationFilter() {
DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());//必须配置
filter.setPasswordAlreadyEncoded(true); // 密码需要加密,设为true
filter.setUserDetailsService(userDetailsService);//必须配置
return filter;
}
@Bean
public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
point.setRealmName("realm");//realm名称,默认为realm,该名称和加密密码的realm一样
return point;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 使用摘要认证的入口
.exceptionHandling().authenticationEntryPoint(digestAuthenticationEntryPoint())
.and()
.authorizeRequests()
.antMatchers(HttpMethod.POST, "/addUser").permitAll()
.antMatchers("/hello2").permitAll()
.anyRequest().authenticated()
.and()
.csrf().disable()
// 摘要认证的过滤器
.addFilter(digestAuthenticationFilter())
}
1.4、无权限处理器
自定义处理,需要新建类实现该AccessDeniedHandler
接口:
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append(
new ObjectMapper().createObjectNode()
.put("status", 401)
.put("msg", "无访问权限")
.toString());
}
}
WebSecurityConfig
配置:
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
...
@Override
protected void configure(HttpSecurity http) throws Exception {
// 先注释,用登录页面登录
//http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
http.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.and()
.csrf().disable();
}
}
启动程序,访问localhost:8080/get-user
,跳转登录页面,输入用户名、密码登录后,访问无权限的资源,会返回无权限Json
信息:
1.5、记住登录
Spring Security记住登录功能有两种方式:基于浏览器的Cookie
存储和基于数据库的存储。
登录页添加记住登录按钮
登录页面
用户名或密码无效
1.5.1、Cookie存储
WebSecurityConfig配置:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
//...
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/user-login").permitAll()
.loginProcessingUrl("/my-login")
.successHandler(new CustomAuthenticationSuccessHandler())
.failureHandler(new CustomAuthenticationFailureHandler())
.and()
.rememberMe()
// 即登录页面的记住登录按钮的参数名
.rememberMeParameter("remember-me")
// 过期时间
.tokenValiditySeconds(1800)
.and()
.csrf().disable();
}
}
启动程序,在勾选记住登录下进行登录,cookie信息如下,remember-me的过期时间内,重启浏览器访问不用登录。
1.5.2、数据库存储
使用 Cookie
存储虽然很方便,但是Cookie
毕竟是保存在客户端的,而且 Cookie
的值还与用户名、密码这些敏感数据相关,虽然加密,但是将敏感信息存在客户端,毕竟不太安全。
Spring security
还提供了另一种更安全的实现机制:在客户端的 Cookie
中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用 Cookie
中的加密串,到数据库中验证,如果通过,自动登录才算通过。
在 WebSecurityConfig
中注入 dataSource
,创建一个 PersistentTokenRepository
的Bean
,并配置数据库存储自动登录:
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private DataSource dataSource;
@Bean
public PersistentTokenRepository persistentTokenRepository(){
JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
tokenRepository.setDataSource(dataSource);
// 启动时创建表,注意,创建好表后,注释掉
// tokenRepository.setCreateTableOnStartup(true);
return tokenRepository;
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login").permitAll()
.and()
// 记住登录
.rememberMe()
// 记住我的数据存储,调用上面写的方法
.tokenRepository(persistentTokenRepository())
// 过期时间
.tokenValiditySeconds(1800)
.and()
.csrf().disable();
}
}
二、session管理
在执行认证过程之前,Spring Security
将运行SecurityContextPersistenceFilter
过滤器负责存储安请求之间的全上下文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository
,其使用http session
作为存储器。
对于session
管理,有三种:
-
session
超时处理:session
有效的时间,超时后删除 -
session
并发控制:同个用户登录,是否强制退出前一个登录,还是禁止后一个登录。 - 集群
session
管理:默认session
是放在单个服务器的单个应用里,在集群中,会出现在一个节点应用登录后,session
只能在该节点使用。另一个节点不能使用其他节点的session
,还会需要登录,所以需要集群共用一个session
2.1、session超时
设置Session
的超时,很简单,只需要在配置文件application.yml
配置即可,如下为设置50
秒:
-
Springboot2.0
前的版本:
spring:
session:
timeout: 50
-
Springboot2.0
后的版本:
server:
servlet:
session:
timeout: 50
上面设置Session
失效时间为50s
,实际源码TomcatEmbeddedServletContainerFactory
类内部会取1分钟。源码内部转成分钟,然后设置给tomcat
原生的StandardContext
,所以一般设置为60秒的整数倍。
其实通过上面配置的点击进去源码发现:
public void setTimeout(Duration timeout) {
this.timeout = timeout;
}
参数传入的是Duration
的实例,Duration
是Java8
新增的,用来计算日期差值,并且是被final
声明,是线程安全的
Duration
转换字符串方式,默认为正;负以-
开头,紧接着P
。
以下字母不区分大小写:
-
D
:天 -
T
:天和小时之间的分隔符 -
H
:小时 -
M
:分钟 -
S
:秒
每个单位都必须是数字,且时分秒顺序不能乱
比如:
-
P2DT3M5S
:2
天3
分5
秒 -
P3D:
3`天 -
PT3H
:3
小时
所以上面配置文件中可以写:
server:
servlet:
session:
timeout: PT50S
2.2、session超时处理
2.2.1、超时跳转URL
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
@Override
protected void configure(HttpSecurity http) throws Exception {
// session无效时跳转的url
http.sessionManagement().invalidSessionUrl("/session/invalid");
http
.authorizeRequests()
// 需要放行条跳转的url
.antMatchers("/session/invalid").permitAll()
.anyRequest().authenticated()
}
}
}
2.2.2、超时处理器
session无效时的处理策略,优先级比上面的高
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomInvalidSessionStrategy invalidSessionStrategy;
@Override
protected void configure(HttpSecurity http) throws Exception {
// 设置session无效处理策略
http.sessionManagement().invalidSessionStrategy(invalidSessionStrategy);
http
.authorizeRequests()
.antMatchers("/session/invalid").permitAll()
.anyRequest().authenticated()
}
}
处理策略:
@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
@Override
public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
// 自定义session无效处理
response.setContentType("application/json;charset=UTF-8");
response.getWriter().append("session无效,请重新登录");
}
}
2.3、session并发控制
默认下,我们可以在不同浏览器同时登录同一个用户,这样就会保存了多个Session
,而有时,我们需要只能在一处地方登录,其他地方的登录就让前一个失效或不能登录。
2.3.1、后登录致前登录失效
在一个浏览器登录后,再到另一个浏览器登录,再回到前一个登录刷新页面,登录失效。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
// 设置session无效处理策略
.invalidSessionStrategy(invalidSessionStrategy)
// 设置同一个用户只能有一个登陆session
.maximumSessions(1);
http
.authorizeRequests()
.anyRequest().authenticated();
}
}
上面设置maximumSessions
设置为1
后,只能有一个登录Session
,多个登录,后一个会把前一个登录的Sesson
失效。
而对于前一个登录Sesson
失效后,刷新页面会显示:
This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).
我们也可以自定义失效返回信息,有两种
-
设置失效session处理URL:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .invalidSessionStrategy(invalidSessionStrategy) .maximumSessions(1) // 其他地方登录session失效处理URL .expiredUrl("/session/expired"); http .authorizeRequests() // URL不需验证 .antMatchers("/session/expired").permitAll() .anyRequest().authenticated() } }
-
设置失效session处理策略:
@EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy; @Override protected void configure(HttpSecurity http) throws Exception { http.sessionManagement() .invalidSessionStrategy(invalidSessionStrategy) .maximumSessions(1) // 其他地方登录session失效处理策略 .expiredSessionStrategy(sessionInformationExpiredStrategy); http .authorizeRequests() .anyRequest().authenticated() } }
过期策略:
@Component public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy { @Override public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException { HttpServletResponse response = event.getResponse(); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("当前用户已在其他地方登录..."); } }
2.3.2、前登录禁后登录
有时,我们在一个地方登录正在操作,不能被打断,这时就要禁止在其他地方登录导致当前的登录Session
失效。
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.sessionManagement()
.invalidSessionStrategy(invalidSessionStrategy)
.maximumSessions(1)
// 设置为true,即禁止后面其它人的登录
.maxSessionsPreventsLogin(true)
.expiredSessionStrategy(sessionInformationExpiredStrategy);
http
.authorizeRequests()
.anyRequest().authenticated()
}
}
禁止后登录后,可以通过如下方式判断异常进行用户通知:
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request,
HttpServletResponse response,
AuthenticationException exception) {
response.setContentType("application/json;charset=utf-8");
if (exception instanceof SessionAuthenticationException){
response.getWriter().write("用户已在其它地方登录,禁止当前登录...");
}
}
}
2.4、集群session管理
在部署应用时,搭建至少两台机器的集群环境,防止一台服务器出现问题而服务中断,这样在一台机器在停止服务时,另一台机器还能继续提供服务。
而使用集群,在基于Session
的身份认证就会导致问题:一个用户登录成功后,其Session
存放在A
机器上,而如果Session
不做其他处理,在用户操作时,在负载均衡下,可能会请求发到B
机器上,而B
机器无Session
导致无权限访问而需要再次登录。
而解决集群中Session
的管理,可以把Session
抽取出来为一个独立存储,用户请求需要Session
时都会读取该存储Session
Spring
提供有Spring Session
来处理集群Session
管理,需要引入如下依赖:
org.springframework.session
spring-session-data-redis
org.springframework.boot
spring-boot-starter-data-redis
使用redis
作为Session
存储管理,而Spring Session
支持以下方式存储Session
,这里只使用Redis。
public enum StoreType {
REDIS,
MONGODB,
JDBC,
HAZELCAST,
NONE;
private StoreType() {
}
}
在配置文件application.yml中配置Redis:
spring:
session:
store-type: redis # session存储类型为 redis
redis:
database: 1
host: localhost
port: 6379
# 更新策略,ON_SAVE在调用#SessionRepository#save(Session)时,在response commit前刷新缓存,
# IMMEDIATE只要有任何更新就会刷新缓存
flush-mode: on_save # 默认
# 存储session的密钥的命名空间
namespace: spring:session #默认
以不同的端口启动程序,如分别以端口8080
和8081
启动两个服务。访问8080
端口登录后,在访问8081
就不需要登录了,说明Session
被共用了。
二、退出登录
默认的退出登录URL
为/logout
,如前面登录的程序,访问localhost:8080/logout
便退出登录,退出登录后,默认跳转到登录页面。
2.1、自定义退出URL
也可通过在WebSecurityConfig
进行自定义配置:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
// 退出登录的url, 默认为/logout
.logoutUrl("/logout2")
}
}
2.2、退出成功处理
-
退出成功处理URL:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() // 退出登录的url, 默认为/logout .logoutUrl("/logout2") // 退出成功跳转URL,注意该URL不需要权限验证 .logoutSuccessUrl("/logout/success").permitAll() } }
-
退出成功处理器
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .logout() // 退出登录的url, 默认为/logout .logoutUrl("/logout2") // 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll //.logoutSuccessUrl("/logout/success").permitAll() //退出登录成功处理器 .logoutSuccessHandler(logoutSuccessHandler) } }
处理器:
@Component public class CustomLogoutSuccessHandler implements LogoutSuccessHandler { @Override public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.getWriter().write("退出登录成功"); } }
2.3、退出成功删除Cookie
默认退出后不会删除Cookie。可配置退出后删除:
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.logout()
// 退出登录的url, 默认为/logout
.logoutUrl("/logout2")
// 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll
//.logoutSuccessUrl("/logout/success").permitAll()
//退出登录成功处理器
.logoutSuccessHandler(logoutSuccessHandler)
// 退出登录删除指定的cookie
.deleteCookies("JSESSIONID")
}
}