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
	}

}
相关推荐
元Y亨H2 小时前
微服务架构核心组件、职责与交互全解析
spring cloud
李慕婉学姐3 小时前
【开题答辩过程】以《基于JAVA的校园即时配送系统的设计与实现》为例,不知道这个选题怎么做的,不知道这个选题怎么开题答辩的可以进来看看
java·开发语言·数据库
奋进的芋圆4 小时前
Java 延时任务实现方案详解(适用于 Spring Boot 3)
java·spring boot·redis·rabbitmq
sxlishaobin5 小时前
设计模式之桥接模式
java·设计模式·桥接模式
model20055 小时前
alibaba linux3 系统盘网站迁移数据盘
java·服务器·前端
荒诞硬汉5 小时前
JavaBean相关补充
java·开发语言
提笔忘字的帝国5 小时前
【教程】macOS 如何完全卸载 Java 开发环境
java·开发语言·macos
2501_941882485 小时前
从灰度发布到流量切分的互联网工程语法控制与多语言实现实践思路随笔分享
java·开发语言
華勳全栈6 小时前
两天开发完成智能体平台
java·spring·go
alonewolf_996 小时前
Spring MVC重点功能底层源码深度解析
java·spring·mvc