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

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

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

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

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

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

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

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

SecurityConfiguration类完整代码:

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如下:

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. 第一个浏览器访问,并登录

    9049b42547dd96026a7a69a258e9963b.png

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

    4abcd31014560429122f6264d8e8b04f.png

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

    外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

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

编写测试代码测试
/*
 * 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类,新增以下代码:

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

    82efc22aaa8a7bd914d58cf08dc5d8c8.png

  2. 第二个浏览器登录

    b61c459e647cb5909c229f07cd27c5aa.png

    第二次被禁止登录

编写测试代码测试
/*
 * 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
	}

}

ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests(); permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll()); httpSecurity // CSRF禁用,因为不使用session .csrf().disable() // 禁用HTTP响应标头 .headers().cacheControl().disable().and() // 认证失败处理类 .exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and() // 基于token,所以不需要session .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() // 过滤请求 .authorizeRequests() // 对于登录login 注册register 验证码captchaImage 允许匿名访问 .antMatchers("/login", "/register", "/captchaImage","/system/workbenchinfo/**").permitAll() // 静态资源,可匿名访问 .antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll() .antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/system/workbenchinfo/**").permitAll() // 除上面外的所有请求全部需要鉴权认证 .anyRequest().authenticated() .and() .headers().frameOptions().disable(); // 添加Logout filter httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler); // 添加JWT filter httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class); // 添加CORS filter httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class); httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);代码解析
06-06
这段代码是一个 Java Spring Security 配置类的代码,主要作用是配置 Spring Security 的一些安全认证策略。以下是代码的解析: 1. `ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = httpSecurity.authorizeRequests();` 这行代码定义了一个 `HttpSecurity` 实例的 `authorizeRequests()` 方法返回的对象,并将其赋值给 `registry` 变量。 2. `permitAllUrl.getUrls().forEach(url -> registry.antMatchers(url).permitAll());` 这行代码遍历了一个 `permitAllUrl` 对象中的所有 URL,并使用 `antMatchers(url)` 方法将这些 URL 添加到 `registry` 对象中,并使用 `permitAll()` 方法允许这些 URL 的匿名访问。 3. `.csrf().disable()` 这行代码禁用了 CSRF 保护,因为这个应用程序不使用 session。 4. `.headers().cacheControl().disable().and()` 这行代码禁用了响应头中的 cacheControl。 5. `.exceptionHandling().authenticationEntryPoint(unauthorizedHandler).and()` 这行代码定义了一个异常处理类 `authenticationEntryPoint`,在认证失败时将会被调用。 6. `.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()` 这行代码定义了一个 `SessionCreationPolicy.STATELESS` 的 `sessionManagement()`,因为这个应用程序是基于 token 的,所以不需要使用 session。 7. `.authorizeRequests()` 这行代码开始定义了所有请求的鉴权认证策略。 8. `.antMatchers("/login", "/register", "/captchaImage","/system/workbenchinfo/**").permitAll()` 这行代码定义了几个 URL 的匿名访问策略。 9. `.antMatchers(HttpMethod.GET, "/", "/*.html", "/**/*.html", "/**/*.css", "/**/*.js", "/profile/**").permitAll()` 这行代码定义了一些静态资源的匿名访问策略,包括 HTML、CSS、JavaScript 文件以及 `/profile/` 目录下的所有资源。 10. `.antMatchers("/swagger-ui.html", "/swagger-resources/**", "/webjars/**", "/*/api-docs", "/druid/**","/system/workbenchinfo/**").permitAll()` 这行代码定义了一些 Swagger 相关的 URL 的匿名访问策略。 11. `.anyRequest().authenticated()` 这行代码定义了除了上面所列出的 URL 之外的所有请求都需要进行鉴权认证。 12. `.headers().frameOptions().disable()` 这行代码禁用了 X-Frame-Options。 13. `httpSecurity.logout().logoutUrl("/logout").logoutSuccessHandler(logoutSuccessHandler);` 这行代码添加了一个 Logout filter,并定义了 `/logout` URL 的登出成功处理器。 14. `httpSecurity.addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);` 这行代码添加了一个 JWT filter,该 filter 会在 `UsernamePasswordAuthenticationFilter` 前执行。 15. `httpSecurity.addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class);` 这行代码添加了一个 CORS filter,该 filter 会在 `JwtAuthenticationTokenFilter` 前执行。 16. `httpSecurity.addFilterBefore(corsFilter, LogoutFilter.class);` 这行代码添加了一个 CORS filter,该 filter 会在 `LogoutFilter` 前执行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值