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());
}
}
浏览器访问验证
-
第一个浏览器访问,并登录
-
第二个浏览器访问,并登录
-
回到第一个浏览器,刷新页面
第一个浏览器提示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)
));
浏览器访问验证
-
第一个浏览器登录
-
第二个浏览器登录
第二次被禁止登录
编写测试代码测试
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
}
}