SpirngSecurity-会话管理(sessionManagement)(三)

SpringSecurity-SpirngBoot-会话管理(sessionManagement)(三)

SpringSecurity默认是通过session对用户的登录进行管理的,如果想控制同一时间,只允许用户在一个地方登录,就需要使用SpringSecurity的sessionManagement功能。

基于上一节的分支,我们新建一个spring-security-session-management分支。

第二次登录使第一次登录无效

修改SecurityConfiguration类,新增以下代码:

java 复制代码
//session管理 同一个账号只能在一处登录 在其他地方登录会使第一次登录登出
.sessionManagement((sessionManagement) -> sessionManagement.maximumSessions(1))

配置表示同一用户最多允许一个session存在,用户第一次登录获取到的session在第二次登录时会失效。

SecurityConfiguration类完整代码:

java 复制代码
package com.jackmouse.security.config;

import com.jackmouse.security.entity.CustomUser;
import com.jackmouse.security.repository.MapCustomUserRepository;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.HttpSecurityBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
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 java.util.HashMap;
import java.util.Map;

@Configurable
@EnableWebSecurity
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                //所有的请求都需要用户进行认证。
                .authorizeHttpRequests(
                        (authorize) -> authorize.anyRequest().authenticated()
                )
                //开启表单认证
                .formLogin(Customizer.withDefaults())
                //session管理 同一个账号只能在一处登录 在其他地方登录会使第一次登录登出
                .sessionManagement((sessionManagement) -> sessionManagement.maximumSessions(1));

        return http.build();
    }
    @Bean
    PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }


    @Bean
    MapCustomUserRepository userRepository() {
        String password = new BCryptPasswordEncoder().encode("password");

        CustomUser customUser = new CustomUser(1L, "user", password);
        Map<String, CustomUser> emailToCustomUser = new HashMap<>();
        emailToCustomUser.put(customUser.getEmail(), customUser);
        return new MapCustomUserRepository(emailToCustomUser);
    }
}

这里注意:由于我们自定义了CustomUser类,因为SpringSecurity是通过管理UserDetails对象来实现用户管理的,类的比较是不能用==比较的,类之间的比较是通过类的equals方法进行比较的,所以我们要重写CustomUser类的equals方法。更新的CustomUser如下:

java 复制代码
package com.jackmouse.security.entity;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class CustomUser {
    private final long id;

    private final String email;

    @JsonIgnore
    private final String password;

    @JsonCreator
    public CustomUser(long id, String email, String password) {
        this.id = id;
        this.email = email;
        this.password = password;
    }
    public long getId() {
        return this.id;
    }

    public String getEmail() {
        return this.email;
    }

    public String getPassword() {
        return this.password;
    }
	// 重写 toString hashCode equals方法
    @Override
    public String toString() {
        return email;
    }

    @Override
    public int hashCode() {
        return email.hashCode();
    }

    @Override
    public boolean equals(Object obj) {
        return this.toString().equals(obj.toString());
    }
}
浏览器访问验证
  1. 第一个浏览器访问,并登录

  2. 第二个浏览器访问,并登录

  3. 回到第一个浏览器,刷新页面

    第一个浏览器提示session已经过期。

编写测试代码测试
java 复制代码
/*
 * Copyright 2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jackmouse.security;

import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureMockMvc
public class MaximumSessionsTests {

	@Autowired
	private MockMvc mvc;

	@Test
	void loginOnSecondLoginThenFirstSessionTerminated() throws Exception {
		// @formatter:off
		MvcResult mvcResult = this.mvc.perform(formLogin())
				.andExpect(authenticated())
				.andReturn();
		// 获取第一次登录的session
		MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();

		// 使用第一次登录获取的session访问后端资源,应该是已经认证过的
		this.mvc.perform(get("/").session(firstLoginSession))
				.andExpect(authenticated());
		// 登录第二次
		this.mvc.perform(formLogin()).andExpect(authenticated());

		// 再次使用第一次登录获取的session访问后端资源,应该是未认证的
		this.mvc.perform(get("/").session(firstLoginSession))
				.andExpect(unauthenticated());
		// @formatter:on
	}

}

第二次无法登录

如果想要实现登录了以后,防止在其他地方被挤掉,需要在SecurityConfiguration类,新增以下代码:

java 复制代码
//session管理 同一个账号只能在一处登录 在其他地方登录会会被禁止登录
.sessionManagement((sessionManagement) -> sessionManagement.sessionConcurrency((concurrency) -> concurrency
                        .maximumSessions(1)
                        .maxSessionsPreventsLogin(true)
                ));
浏览器访问验证
  1. 第一个浏览器登录

  2. 第二个浏览器登录

    第二次被禁止登录

编写测试代码测试
java 复制代码
/*
 * Copyright 2023 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.jackmouse.security;

import com.jackmouse.security.config.SecurityConfiguration;
import org.junit.jupiter.api.Test;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.mock.web.MockHttpSession;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestBuilders.formLogin;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.authenticated;
import static org.springframework.security.test.web.servlet.response.SecurityMockMvcResultMatchers.unauthenticated;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@Import(SecurityConfiguration.class)
@AutoConfigureMockMvc
public class MaximumSessionsPreventLoginTests {

	@Autowired
	private MockMvc mvc;

	@Test
	void loginOnSecondLoginThenPreventLogin() throws Exception {
		// @formatter:off
		MvcResult mvcResult = this.mvc.perform(formLogin())
				.andExpect(authenticated())
				.andReturn();
		// 获取第一次登录的session
		MockHttpSession firstLoginSession = (MockHttpSession) mvcResult.getRequest().getSession();
		// 使用第一次登录获取的session访问后端资源,应该是已经认证过的
		this.mvc.perform(get("/").session(firstLoginSession))
				.andExpect(authenticated());

		// 第二次登录被拒绝
		this.mvc.perform(formLogin()).andExpect(unauthenticated());

		// 使用第一次登录获取的session仍然可以访问后端资源
		this.mvc.perform(get("/").session(firstLoginSession))
				.andExpect(authenticated());
		// @formatter:on
	}

}
相关推荐
Wang's Blog2 分钟前
Java 项目实战: 交友项目 - 用户体系集成:根据环信id查询用户详情
java·lua·交友
摇滚侠6 分钟前
《On Java 中文版 基础卷》阅读笔记 复用 13
java·笔记·python
2601_968900779 分钟前
玻璃深加工的工艺窗口:4 道工序的温度与时间参数边界
android·java·javascript
薛之谦_15 分钟前
用 Java 实现阿里云式 IAM 子用户管理:架构设计与表结构详解
java·python·阿里云
甜到心里的蛋糕43 分钟前
抖音评论自动回复机器人部署实践:无头 Chrome + DeepSeek + NapCat(2C2G 云主机)
java·jvm·chrome·python·抖音·deepseek
Java爱好狂.1 小时前
阿里1658页Java面试题总结(含答案)
java·后端·高并发·架构师·java面试·java面试题·java八股文
Thanks_ks1 小时前
从 0.1+0.2 不等于 0.3 说起:聊聊浮点数精度问题
java·python·数值计算·精度丢失·bigdecimal·浮点数·ieee754
海鸥-w1 小时前
区域名称同步存储实现笔记
java·笔记
Rain的Java大神之路1 小时前
LeetCode 21 合并两个有序链表:一道 Easy 题,凭什么刷掉一半候选人?
java·后端·算法
仙逆GPT1 小时前
ChatGPT、Codex工程机制:Agent把@Transactional方法改成异步执行,为什么数据会出现部分提交?
spring boot·chatgpt·codex·chatgptplus·chatgptpro