SpringSecurity:session管理

x33g5p2x  于2021-11-30 转载在 Spring  
字(6.3k)|赞(0)|评价(0)|浏览(321)

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 在退出时候做了这样几件事:

  1. 使当前的 session 失效
  2. 清除与当前用户有关的 remember-me 记录
  3. 清空当前的 SecurityContext
  4. 重定向到登录页

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

docker安装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 设置是否还有效,使用其他浏览器登陆,登陆成功后发现原浏览器用户的确被踢出。

相关文章

微信公众号

最新文章

更多