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
	}

}
相关推荐
m0_748238633 分钟前
Spring Boot项目接收前端参数的11种方式
前端·spring boot·后端
Forget the Dream25 分钟前
设计模式之责任链模式
java·c++·设计模式·责任链模式
jonyleek28 分钟前
「JVS更新日志」低代码、企业会议、智能BI、智能排产2.26更新说明
java·大数据·低代码·数据分析·软件需求
计算机小白一个1 小时前
蓝桥杯 Java B 组之最短路径算法(Dijkstra、Floyd-Warshall)
java·数据结构·算法·蓝桥杯
曼岛_1 小时前
[密码学实战]Java实现SM4加解密(ecb,cbc)及工具验证
java·密码学
菜鸟阿达1 小时前
spring boot 2.7 + seata +微服务 降级失败问题修复
spring boot·后端·微服务
Forget the Dream1 小时前
设计模式之代理模式
java·c++·设计模式·代理模式
帅的飞起来1 小时前
设计模式--spring中用到的设计模式
java·spring·设计模式
張葒兵3 小时前
记一次命令行启动springboot项目的问题 java -jar的问题
java·spring boot·jar
zr想努力3 小时前
Lua的table类型的增删改查操作
java·开发语言·lua