07 SpringSecurity-会话管理

文章配套代码:https://gitee.com/lookoutthebush/spring-security-demo

再2个浏览器中用同一个账号登录就会发现,到现在为止,系统还没有任何会话并发限制。一个账号能多处同时登录不是一个好的策略。

一、理解会话

会话(session)就是无状态的 HTTP 实现用户状态可维持的一种解决方案。HTTP 本身的无状态 使得用户在与服务器的交互过程中,每个请求之间都没有关联性。这意味着用户的访问没有身份记 录,站点也无法为用户提供个性化的服务。session的诞生解决了这个难题,服务器通过与用户约定每 个请求都携带一个id类的信息,从而让不同请求之间有了关联,而id又可以很方便地绑定具体用户,所 以我们可以把不同请求归类到同一用户。基于这个方案,为了让用户每个请求都携带同一个id,在不 妨碍体验的情况下,cookie是很好的载体。当用户首次访问系统时,系统会为该用户生成一个 sessionId,并添加到cookie中。在该用户的会话期内,每个请求都自动携带该cookie,因此系统可以很 轻易地识别出这是来自哪个用户的请求。

尽管cookie非常有用,但有时用户会在浏览器中禁用它,可能是出于安全考虑,也可能是为了保 护个人隐私。在这种情况下,基于cookie实现的sessionId自然就无法正常使用了。因此,有些服务还支 持用URL重写的方式来实现类似的体验,例如:

http://xxx.com;jssessionid=xxx

URL重写原本是为了兼容禁用cookie的浏览器而设计的,但也容易被黑客利用。黑客只需访问一 次系统,将系统生成的sessionId提取并拼凑在URL上,然后将该URL发给一些取得信任的用户。只要用户在session有效期内通过此URL进行登录,该sessionId就会绑定到用户的身份,黑客便可以轻松享有同样的会话状态,完全不需要用户名和密码,这就是典型的会话固定攻击。

二、防御会话固定攻击

防御会话固定攻击的方法非常简单,只需在用户登录之后重新生成新的session即可。在继承 WebSecurityConfigurerAdapter时,Spring Security已经启用了该配置。

	protected final HttpSecurity getHttp() throws Exception {
		if (this.http != null) {
			return this.http;
		}
		AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
		this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
		AuthenticationManager authenticationManager = authenticationManager();
		this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
		Map<Class<?>, Object> sharedObjects = createSharedObjects();
		this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
		if (!this.disableDefaults) {
			applyDefaultConfiguration(this.http);
			ClassLoader classLoader = this.context.getClassLoader();
			List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
					.loadFactories(AbstractHttpConfigurer.class, classLoader);
			for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
				this.http.apply(configurer);
			}
		}
		configure(this.http);
		return this.http;
	}

	private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
		http.csrf();
		http.addFilter(new WebAsyncManagerIntegrationFilter());
		http.exceptionHandling();
		http.headers();
        //嗲用sessionManagement
		http.sessionManagement();
		http.securityContext();
		http.requestCache();
		http.anonymous();
		http.servletApi();
		http.apply(new DefaultLoginPageConfigurer<>());
		http.logout();
	}

sessionManagement是一个会话管理的配置器,其中,防御会话固定攻击的策略有四种:

  • none:不做任何变动,登录之后沿用旧的session。
  • newSession:登录之后创建一个新的session。
  • migrateSession:登录之后创建一个新的session,并将旧的session中的数据复制过来。
  • changeSessionId:不创建新的会话,而是使用由Servlet容器提供的会话固定保护。

 在 Spring Security 中,即便没有配置,也大可不必担心会话固定攻击。这是因为Spring Security的 HTTP防火墙会帮助我们拦截不合法的URL,当我们试图访问带session的URL时会被重定向到错误页。

具体细节可以翻看Spring Security源码,该部分内容在StrictHttpFirewall类中实现。

三、会话过期

除防御会话固定攻击外,还可以通过Spring Security配置一些会话过期策略。例如,会话过期时跳 转到某个URL。

或者完全自定义过期策略。通过添加invalidSessionStrategy()

public class MyInvalidSessionStrategy implements InvalidSessionStrategy {
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write(JSON.toJSONString(ResultVO.success("session timeout")));
    }
}

 invalidSessionStrategy与invalidSessionUrl配置其中一个就可以。

四、会话并发控制

1.控制最大会话数

maximumSessions会限制同时在线的会话数,如果没有额外的配置,重新登录的会话会踢掉旧的会话。

具体的实现细节在ConcurrentSessionControlAuthenticationStrategy类中可以看到。

2.重写hashCode和equals方法

SpringSecurity为了实现会话并发控制,采用会话信息表来管理用户的会话状态,具体实现见 SessionRegistryImpl类。

public class SessionRegistryImpl implements SessionRegistry, ApplicationListener<AbstractSessionEvent> {

	protected final Log logger = LogFactory.getLog(SessionRegistryImpl.class);

	// <principal:Object,SessionIdSet>
	private final ConcurrentMap<Object, Set<String>> principals;

	// <sessionId:Object,SessionInformation>
	private final Map<String, SessionInformation> sessionIds;

	public SessionRegistryImpl() {
		this.principals = new ConcurrentHashMap<>();
		this.sessionIds = new ConcurrentHashMap<>();
	}

该类中处理了移除旧会话的操作,是通过对象来对比的,所以需要在自定义用户类中重写hashCode和equals方法

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        UsersDO usersDO = (UsersDO) o;
        return Objects.equals(username, usersDO.username);
    }

    @Override
    public int hashCode() {
        return Objects.hash(super.hashCode(), username);
    }

五、集群会话的缺陷

session(会话)保存在服务器上,单个服务的时候没问题。但是通常会有多个服务器来一起工作。通过一用户的登录请求可能在服务器A上,那么session也会被保存到服务器A上,但是后续请求可能被分配到其他服务器上,这个时候就会有问题了,会让用户重新登录。

六、解决方案

通常由三种方案:

  1. session保持
  2. session复制
  3. session共享

session保持也叫粘滞会话(Sticky Sessions),通常采用IP哈希负载策略将来自相同客户端的请求 转发至相同的服务器上进行处理。session保持虽然避开了集群会话,但也存在一些缺陷。例如,某个 营业部的网络使用同个IP出口,那么使用该营业部网络的所有员工实际的源IP其实是同一个,在IP哈 希负载策略下,这些员工的请求都将被转发到相同的服务器上,存在一定程度的负载失衡。

session复制是指在集群服务器之间同步session数据,以达到各个实例之间会话状态一致的做法。 但毫无疑问,在集群服务器之间进行数据同步的做法非常不可取,尤其是在服务器实例很多的情况 下,任何变动都需要其他所有实例同步,不仅消耗数据带宽,还会占用大量的资源。

相较于前两种方案,session 共享则要实用得多。session 共享是指将 session 从服务器内存抽离出 来,集中存储到独立的数据容器,并由各个服务器共享。

由于所有的服务器实例单点存取session,所以集群不同步的问题自然也就不存在了,而且独立的 数据容器容量相较于服务器内存要大得多。另外,与服务本身分离、可持久化等特性使得会话状态不会因为服务停止而丢失。当然,session共享并非没有缺点,独立的数据容器增加了网络交互,数据容 器的读/写性能、稳定性以及网络I/O速度都成为性能的瓶颈。基于这些问题,尽管在理论上使用任何 存储介质都可以实现session共享,但在内网环境下,高可用部署的Redis服务器无疑为最优选择。Redis 基于内存的特性让它拥有极高的读/写性能,高可用部署不仅降低了网络I/O损耗,还提高了稳定性。

七、整合SpringSession解决集群会话问题

SpringSession就是专门用来解决集群会话问题的,它不仅为集群 会话提供了非常完善的支持,与Spring Security的整合也有专门的实现。

SpringSession支持多种类型的存储容器,包括Redis、MongoDB等。由于接下来的整合都是基于 Redis的。

1.准备redis

我们这里使用windows版的redis。https://download.csdn.net/download/LookOutThe/80098914

解压就能使用

2.在pom.xml中引入三个依赖。

        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-core</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

3.修改security的配置:

4. 启动项目,登录:

 另一个浏览器登录后,当前浏览器刷新会提示session过期。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

七号公园的忧伤

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值