Security之会话篇

1、功能实现

1.session 超时失效
2.session 并发失效
3.手动清除孤立 session
4.整合redis实现session共享(集群)

2、security06 子工程

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>com.yzm</groupId>
        <artifactId>security</artifactId>
        <version>0.0.1-SNAPSHOT</version>
        <relativePath>../pom.xml</relativePath> <!-- lookup parent from repository -->
    </parent>

    <artifactId>security06</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>
    <name>security06</name>
    <description>Demo project for Spring Boot</description>

    <dependencies>
        <dependency>
            <groupId>com.yzm</groupId>
            <artifactId>common</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>

		<!-- session-redis -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <!-- redis支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

applicatiion.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://192.168.192.128:3306/testdb?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: 1234

  main:
    allow-bean-definition-overriding: true
  
mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.yzm.security06.entity
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

# session 过期时间,单位:秒
server:
  servlet:
    session:
      timeout: 60

3、自定义会话无效策略、会话过期策略

package com.yzm.security06.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.session.InvalidSessionStrategy;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 默认session超时失效
 */
@Slf4j
public class SecSessionInvalidStrategy implements InvalidSessionStrategy {

    private final boolean createNewSession;

    public SecSessionInvalidStrategy() {
        this.createNewSession = true;
    }

    public SecSessionInvalidStrategy(boolean createNewSession) {
        this.createNewSession = createNewSession;
    }

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        log.info("Session 超时失效");

        if (this.createNewSession) {
            request.getSession();
        }

        response.sendRedirect(request.getContextPath() + "/auth/login?invalid");
    }

}

package com.yzm.security06.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.security.web.session.SessionInformationExpiredEvent;
import org.springframework.security.web.session.SessionInformationExpiredStrategy;

import java.io.IOException;

/**
 * 并发登录导致session失效
 */
@Slf4j
public class SecSessionExpiredStrategy implements SessionInformationExpiredStrategy {

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException {
        log.info("session 并发失效");
        // 跳转html页面
        event.getResponse().sendRedirect(event.getRequest().getContextPath() + "/auth/login?expired");
    }

}

4、SecurityConfig 配置类

package com.yzm.security06.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
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.web.access.expression.DefaultWebSecurityExpressionHandler;

import javax.sql.DataSource;

@Slf4j
@Configuration
@EnableWebSecurity 
@EnableGlobalMethodSecurity(prePostEnabled = true) 
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private final UserDetailsService userDetailsService;
    private final SecPermissionEvaluator permissionEvaluator;

    public SecurityConfig(@Qualifier("secUserDetailsServiceImpl") UserDetailsService userDetailsService, SecPermissionEvaluator permissionEvaluator) {
        this.userDetailsService = userDetailsService;
        this.permissionEvaluator = permissionEvaluator;
    }

    /**
     * 密码编码器
     * passwordEncoder.encode是用来加密的,passwordEncoder.matches是用来解密的
     */
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 注入自定义PermissionEvaluator
     */
    @Bean
    public DefaultWebSecurityExpressionHandler webSecurityExpressionHandler() {
        DefaultWebSecurityExpressionHandler handler = new DefaultWebSecurityExpressionHandler();
        handler.setPermissionEvaluator(permissionEvaluator);
        return handler;
    }

    /**
     * session 管理
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    /**
     * 配置用户
     * 指定默认从哪里获取认证用户的信息,即指定一个UserDetailsService接口的实现类
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 从数据库读取用户、并使用密码编码器解密
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }

    //配置资源权限规则
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 关闭CSRF跨域
                .csrf().disable()

                // 登录
                .formLogin()
                .loginPage("/auth/login") //指定登录页的路径,默认/login
                .loginProcessingUrl("/login") //指定自定义form表单请求的路径(必须跟login.html中的form action=“url”一致)
                .defaultSuccessUrl("/home", true) // 登录成功后的跳转url地址
                .failureUrl("/auth/login?error") // 登录失败后的跳转url地址
                .permitAll()
                .and()

                // 记住我
                .rememberMe()
                .tokenValiditySeconds(120) // 有效时间,单位秒,默认30分钟
                .and()

                .exceptionHandling()
                .accessDeniedPage("/401") // 拒接访问跳转页面
                .and()

                // 退出登录
                .logout()
                .permitAll()
                .and()

                // 访问路径URL的授权策略,如注册、登录免登录认证等
                .authorizeRequests()
                .antMatchers("/", "/home", "/register", "/auth/login").permitAll() //指定url放行
                .anyRequest().authenticated() //其他任何请求都需要身份认证
                .and()

                // session 管理
                .sessionManagement()
                // 1.session 超时,默认60秒,即60秒内无操作就会过期
                // 通过在yml里面设置 server.servlet.session.timeout=60
                //.invalidSessionUrl("/invalid")
                .invalidSessionStrategy(new SecSessionInvalidStrategy())
                // 2.session 并发控制:控制一个账号同一时刻最多能登录多少个
                .maximumSessions(1) // 限制最大登陆数
                // 当达到最大值时,是否阻止用户登录,false表示不阻止,那么新的会覆盖旧的,旧的被迫下载
                .maxSessionsPreventsLogin(false)
                //.expiredUrl("/invalid")
                .expiredSessionStrategy(new SecSessionExpiredStrategy()) // 当达到最大值时,旧用户被踢出后的操作
                // 3.手动使Session立即失效
                .sessionRegistry(sessionRegistry())
        ;
    }
}

login.thml

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>登录页</title>
</head>
<body>
<h1 th:if="${param.logout}">You have been logged out.</h1>
<h1 th:if="${param.error}">You username or password is wrong.</h1>
<h1 th:if="${param.invalid}">Session invalid.</h1>
<h1 th:if="${param.expired}">Session expired.</h1>

<h2>用户名密码登录</h2>
<form action="/login" method="post">
    <p>
        <label for="username">Username</label>
        <input type="text" id="username" name="username" placeholder="Username">
    </p>
    <p>
        <label for="password">Password</label>
        <input type="password" id="password" name="password" placeholder="Password">
    </p>
    <p>
        <label>
            <input type="checkbox" name="remember-me">
        </label> Remember me on this computer.
    </p>
    <button type="submit">Sign in</button>
</form>
<script type="text/javascript">
    function kickOut() {
        const href = location.href;
        if (href.indexOf("expired") > 0) {
            alert("您的账号在另一台设备上登录,如非本人操作,请立即修改密码!");
        }
    }
    window.onload = kickOut();
</script>
</body>
</html>

5、超时失效测试

默认超时时间60秒

server:
  servlet:
    session:
      timeout: 60

登录yzm,停留时间超过一分钟,刷新一下
在这里插入图片描述

6、并发失效

需要2个浏览器,先用谷歌登录yzm,再用其他浏览器登录yzm后,刷新谷歌浏览器
在这里插入图片描述

7、测试手动清除session

在SecurityConfig注入

    /**
     * 会话注册表
     */
    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

	// session 管理
    .sessionManagement()
    ...
    // 3.手动使Session立即失效
    .sessionRegistry(sessionRegistry())

在HomeController中添加接口

...

@Controller
public class HomeController {

    private final UserService userService;
    private final PasswordEncoder passwordEncoder;
    private final SessionRegistry sessionRegistry;

    public HomeController(UserService userService, PasswordEncoder passwordEncoder,SessionRegistry sessionRegistry) {
        this.userService = userService;
        this.passwordEncoder = passwordEncoder;
        this.sessionRegistry = sessionRegistry;
    }

    ...

    @GetMapping("/kick")
    @ResponseBody
    public String removeUserSessionByUsername(@RequestParam String username) {
        int count = 0;

        // 获取session中所有的用户信息
        List<Object> users = sessionRegistry.getAllPrincipals(); // 获取所有 principal 信息
        for (Object principal : users) {
            if (principal instanceof UserDetails) {
                String principalName = ((UserDetails) principal).getUsername();
                if (principalName.equals(username)) {
                    // 参数二:是否包含过期的Session
                    List<SessionInformation> sessionsInfo = sessionRegistry.getAllSessions(principal, false);
                    if (null != sessionsInfo && sessionsInfo.size() > 0) {
                        for (SessionInformation sessionInformation : sessionsInfo) {
                            // 使 session 过期
                            sessionInformation.expireNow();
                            count++;
                        }
                    }
                }
            }
        }
        return "操作成功,清理session共" + count + "个";
    }
}

谷歌登录yzm 再用其他浏览器登录admin管理员
访问/kick?username=yzm,yzm被踢下线
在这里插入图片描述

8、session共享(集群)

引入依赖

        <!-- session-redis -->
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>

        <!-- redis支持 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

配置redis

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://127.0.0.1:3306/test04?useUnicode=true&characterEncoding=utf8&useSSL=false&allowMultiQueries=true&zeroDateTimeBehavior=convertToNull&serverTimezone=Asia/Shanghai
    username: root
    password: root

  main:
    allow-bean-definition-overriding: true

  redis:
    host: 127.0.0.1
    port: 6379
    password: 1234
    database: 0

在SecurityConfig 启用 redis 管理 session
配置了maxInactiveIntervalInSeconds = 200之后,这个server.servlet.session.timeout=60就无效了

@Slf4j
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableRedisHttpSession(redisNamespace = "spring:session:yzm", maxInactiveIntervalInSeconds = 200, flushMode = FlushMode.ON_SAVE)
public class SecurityConfig extends WebSecurityConfigurerAdapter {...}

注释session注册表,手动清除session功能不可用

    /**
     * 会话注册表
     */
//    @Bean
//    public SessionRegistry sessionRegistry() {
//        return new SessionRegistryImpl();
//    }

    /**
     * 是spring session为Spring Security提供的,
     * 用于在集群环境下控制会话并发的会话注册表实现
     */
    @Autowired
    @Lazy
    private FindByIndexNameSessionRepository<? extends Session> sessionRepository;

    @Bean
    public SpringSessionBackedSessionRegistry sessionRegistry(){
        return new SpringSessionBackedSessionRegistry<>(sessionRepository);
    }

启动端口8080、8090;火狐8080登录yzm,谷歌8090再次登录yzm,刷新火狐yzm
在这里插入图片描述

查看缓存
在这里插入图片描述

相关链接

首页
上一篇:自定义处理程序篇
下一篇:认证流程篇

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值