SpringSecurity:session管理
1 Session超时
当用户登录后,我们可以设置 session 的超时时间,当达到超时时间后,自动将用户退出登录。
Session 超时的配置是 SpringBoot 原生支持的,我们只需要在 application.properties
配置文件中配置:
server:
servlet:
session:
timeout: 60 # 过期时间,单位s
Spring Security 提供了两种处理配置,一个是 invalidSessionStrategy()
,另外一个是 invalidSessionUrl()
。
这两个的区别就是一个是前者是在一个类中进行处理,后者是直接跳转到一个 Url。简单起见,我就直接用 invalidSessionUrl()
了,跳转到 /login/invalid
,我们需要把该 Url 设置为免授权访问, 配置如下:
在 controller 中写一个接口进行处理:
@RequestMapping("/login/invalid")
@ResponseStatus(HttpStatus.UNAUTHORIZED)
@ResponseBody
public String invalid() {
return "Session 已过期,请重新登录";
}
运行程序,登陆成功后等待一分钟(或者重启服务器),刷新页面:
2 限制最大登录数
原理:限制单个用户能够存在的最大session数
在http.sessionManagement()
下添加三行代码:
maximumSessions(int)
:指定最大登录数maxSessionsPreventsLogin(boolean)
:是否保留已经登录的用户;为true,新用户无法登录;为 false,旧用户被踢出expiredSessionStrategy(SessionInformationExpiredStrategy)
:旧用户被踢出后处理方法
修改结果如下:
http.sessionManagement()
// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.invalidSessionUrl("/login/invalid")
.maximumSessions(1)
//当达到最大值时,是否保留已经登录的用户
.maxSessionsPreventsLogin(false)
//当达到最大值时,旧用户被提出后的操作
.expiredSessionStrategy(new CustomExpiredSessionStrategy());
编写 CustomExpiredSessionStrategy 类,来处理旧用户登陆失败的逻辑:
public class CustomExpiredSessionStrategy implements SessionInformationExpiredStrategy {
private final ObjectMapper objectMapper = new ObjectMapper();
// private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
Map<String, Object> map = new HashMap<>(16);
map.put("code", 0);
map.put("msg", "已经另一台机器登录,您被迫下线。" + event.getSessionInformation().getLastRequest());
// Map -> Json
String json = objectMapper.writeValueAsString(map);
event.getResponse().setContentType("application/json;charset=UTF-8");
event.getResponse().getWriter().write(json);
// 如果是跳转html页面,url代表跳转的地址
// redirectStrategy.sendRedirect(event.getRequest(), event.getResponse(), "url");
}
}
执行程序,打开两个浏览器,登录同一个账户。因为我设置了 maximumSessions(1)
,也就是单个用户只能存在一个 session,因此当你刷新先登录的那个浏览器时,被提示踢出了。
下面我们来测试下 maxSessionsPreventsLogin(true)
时的情况,我们发现第一个浏览器登录后,第二个浏览器无法登录:
3 踢出用户
首先需要在容器中注入名为 SessionRegistry
的 Bean,这里我就简单的写在 WebSecurityConfig 中:
@Bean
public SessionRegistry sessionRegistry() {
return new SessionRegistryImpl();
}
其次在sessionManagement中添加一行.sessionRegistry()
http.sessionManagement()
// .sessionCreationPolicy(SessionCreationPolicy.ALWAYS)
.invalidSessionUrl("/login/invalid")
.maximumSessions(1)
//当达到最大值时,是否保留已经登录的用户
.maxSessionsPreventsLogin(true)
//当达到最大值时,旧用户被提出后的操作
.expiredSessionStrategy(new CustomExpiredSessionStrategy())
.sessionRegistry(sessionRegistry());
最后编写一个接口用于测试踢出用户:
@Controller
public class LoginController {
@Autowired
private SessionRegistry sessionRegistry;
...
@GetMapping("/kick")
@ResponseBody
public String removeUserSessionByUsername(@RequestParam String username) {
int count = 0;
// 获取session中所有的用户信息
List<Object> users = sessionRegistry.getAllPrincipals();
for (Object principal : users) {
if (principal instanceof User) {
String principalName = ((User)principal).getUsername();
if (principalName.equals(username)) {
// 参数二:是否包含过期的Session
List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false);
if (null != sessionsInfo && sessionsInfo.size() > 0) {
for (SessionInformation sessionInformation : sessionsInfo) {
sessionInformation.expireNow();
count++;
}
}
}
}
}
return "操作成功,清理session共" + count + "个";
}
}
sessionRegistry.getAllPrincipals();
获取所有 principal 信息- 通过 principal.getUsername 是否等于输入值,获取到指定用户的 principal
sessionRegistry.getAllSessions(principal, false)
获取该 principal 上的所有 session- 通过
sessionInformation.expireNow()
使得 session 过期
运行程序,分别使用 admin 和 hl 账户登录,admin 访问 /kick?username=jitwxs
来踢出用户 hl,hl刷新页面,发现被踢出。
4 退出登录
http.logout();
是 Spring Security 的默认退出配置,Spring Security 在退出时候做了这样几件事:
- 使当前的 session 失效
- 清除与当前用户有关的 remember-me 记录
- 清空当前的 SecurityContext
- 重定向到登录页
Spring Security 默认的退出 Url 是 /logout
,我们可以修改默认的退出 Url,例如修改为 /signout
:
http.logout()
.logoutUrl("/signout");
我们也可以配置当退出时清除浏览器的 Cookie,例如清除 名为 JSESSIONID 的 cookie:
http.logout()
.logoutUrl("/signout")
.deleteCookies("JSESSIONID");
我们也可以配置退出后处理的逻辑,方便做一些别的操作:
http.logout()
.logoutUrl("/signout")
.deleteCookies("JSESSIONID")
.logoutSuccessHandler(logoutSuccessHandler);
创建类 DefaultLogoutSuccessHandler
:
package com.hl.hl01springsecurity.security.logout;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
private final static Logger log = LoggerFactory.getLogger(CustomLogoutSuccessHandler.class);
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
String username = ((User) authentication.getPrincipal()).getUsername();
log.info("退出成功,用户名:{}", username);
// 重定向到登录页
response.sendRedirect("/login");
}
}
最后把它注入到 WebSecurityConfig 即可:
@Autowired
private CustomLogoutSuccessHandler logoutSuccessHandler;
5 Session 共享
在最后补充下关于 Session 共享的知识点,一般情况下,一个程序为了保证稳定至少要部署两个,构成集群。那么就牵扯到了 Session 共享的问题,不然用户在 8080 登录成功后,后续访问了 8060 服务器,结果又提示没有登录。
(1)首先安装redis
(2)配置session共享
导入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
在 application.xml
中新增配置指定 redis 地址以及 session 的存储方式:
spring.redis.host=127.0.0.1
spring.redis.port=6379
spring.session.store-type=redis
然后为主类添加 @EnableRedisHttpSession
注解。
@SpringBootApplication
@EnableRedisHttpSession
public class Hl01SpringSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(Hl01SpringSecurityApplication.class, args);
}
}
测试:
修改 IDEA 配置来允许项目在多端口运行,勾选 Allow running in parallel
:
运行程序,然后修改配置文件,将 server.port
更改为 8060,再次运行。这样项目就会分别在默认的 8080 端口和 8060 端口运行。
先访问 localhost:8080
,登录成功后,再访问 localhost:8060
,发现无需登录
然后我们进入 Redis 查看下 key:
最后再测试下之前配置的 session 设置是否还有效,使用其他浏览器登陆,登陆成功后发现原浏览器用户的确被踢出。