Spring Boot整合Security系列步骤及问题排查(十四)—— Session管理及退出

过期管理

1.直接在配置文件中配置:

server:
  servlet:
    session:
      # 单位(s),默认30分钟,最少1分钟
      timeout: 600
      # session集群存储配置,如REDIS
#      store-type: REDIS

多端登录管理和退出

退出处理:
使当前session失效
清除与当前用户相关的remember-me记录
清空当前的SecurityContext
重定向到登录页

2.更新WebSecurityConfig:

// session管理
@Autowired
private InvalidSessionStrategy invalidSessionStrategy;

@Autowired
private SessionInformationExpiredStrategy sessionInformationExpiredStrategy;

@Autowired
private LogoutSuccessHandler logoutSuccessHandler;

...

// session管理
.sessionManagement()
// 过期处理
.invalidSessionStrategy(invalidSessionStrategy)
// 同时在线数量
.maximumSessions(securityProperties.getBrowser().getSession().getMaximumSessions())
// 是否阻止登录
.maxSessionsPreventsLogin(securityProperties.getBrowser().getSession().isMaxSessionsPreventsLogin())
// 多端登录提示
.expiredSessionStrategy(sessionInformationExpiredStrategy)
.and()
.and()
// 退出
.logout()
// 触发退出操作的url
.logoutUrl("/signOut")
// .logoutSuccessUrl("demoLogout.html")
// 与logoutSuccessUrl只能调用其中一个
.logoutSuccessHandler(logoutSuccessHandler)
// 删除cookie
.deleteCookies("JSESSIONID")
.and()
// 对任何请求授权
.authorizeRequests()
// 匹配页面授权所有权限
.antMatchers(
        // API
        "/swagger-ui.html",
        // 默认登录页
        SecurityConstants.DEFAULT_UNAUTHENTICATION_URL,
        // 自定义登录页(demoLogin)
        securityProperties.getBrowser().getLoginPage(),
        // 验证码
        SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX + "/*",
        securityProperties.getBrowser().getSignUpUrl(),
        securityProperties.getBrowser().getSession().getSessionInvalidUrl(),
        securityProperties.getBrowser().getSignOutUrl(),
        "/user/regist")
.permitAll()

实现LogoutSuccessHandler:

/**
 * 默认的退出成功处理器,如果设置了demo.security.browser.signOutUrl,则跳到配置的地址上,
 * 如果没配置,则返回json格式的响应。
 *
 * @author zhaohaibin
 */
@Slf4j
public class DemoLogoutSuccessHandler implements LogoutSuccessHandler {

    public DemoLogoutSuccessHandler(String signOutSuccessUrl) {
        this.signOutSuccessUrl = signOutSuccessUrl;
    }

    private String signOutSuccessUrl;

    private ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {

        log.info("退出成功");

        if (StringUtils.isBlank(signOutSuccessUrl)) {
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse("退出成功")));
        } else {
            response.sendRedirect(signOutSuccessUrl);
        }

    }

}

实现InvalidSessionStrategy:

/**
 * 默认的session失效处理策略
 *
 * @author zhaohaibin
 */
public class DemoInvalidSessionStrategy extends AbstractSessionStrategy implements InvalidSessionStrategy {

    public DemoInvalidSessionStrategy(SecurityProperties securityProperties) {
        super(securityProperties);
    }

    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response)
            throws IOException, ServletException {
        onSessionInvalid(request, response);
    }

}

实现SessionInformationExpiredStrategy:

/**
 * 并发登录导致session失效时,默认的处理策略
 *
 * @author zhaohaibin
 */
public class DemoExpiredSessionStrategy extends AbstractSessionStrategy implements SessionInformationExpiredStrategy {

    public DemoExpiredSessionStrategy(SecurityProperties securityPropertie) {
        super(securityPropertie);
    }

    @Override
    public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
        onSessionInvalid(event.getRequest(), event.getResponse());
    }

    @Override
    protected boolean isConcurrency() {
        return true;
    }

}

抽象继承:

/**
 * 抽象的session失效处理器
 *
 * @author zhaohaibin
 */
@Slf4j
public class AbstractSessionStrategy {

    /**
     * 跳转的url
     */
    private String destinationUrl;
    /**
     * 系统配置信息
     */
    private SecurityProperties securityPropertie;
    /**
     * 重定向策略
     */
    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();
    /**
     * 跳转前是否创建新的session
     */
    private boolean createNewSession = true;

    private ObjectMapper objectMapper = new ObjectMapper();

    /**
     * @param securityPropertie
     */
    public AbstractSessionStrategy(SecurityProperties securityPropertie) {
        String invalidSessionUrl = securityPropertie.getBrowser().getSession().getSessionInvalidUrl();
        Assert.isTrue(UrlUtils.isValidRedirectUrl(invalidSessionUrl), "url must start with '/' or with 'http(s)'");
        Assert.isTrue(StringUtils.endsWithIgnoreCase(invalidSessionUrl, ".html"), "url must end with '.html'");
        this.destinationUrl = invalidSessionUrl;
        this.securityPropertie = securityPropertie;
    }

    protected void onSessionInvalid(HttpServletRequest request, HttpServletResponse response) throws IOException {

        log.info("session失效");

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

        String sourceUrl = request.getRequestURI();
        String targetUrl;

        // 如果是页面则跳转
        if (StringUtils.endsWithIgnoreCase(sourceUrl, ".html")) {
            // 跳到登录登出页面
            if (StringUtils.equals(sourceUrl, securityPropertie.getBrowser().getSignInPage())
                    || StringUtils.equals(sourceUrl, securityPropertie.getBrowser().getSignOutUrl())) {
                targetUrl = sourceUrl;
                // 跳到session过期页面
            } else {
                targetUrl = destinationUrl;
            }
            log.info("跳转到:" + targetUrl);

            if (StringUtils.isNotBlank(SecurityConstants.DEFAULT_PROJECT_NAME_URL)) {
                targetUrl = targetUrl.replace(SecurityConstants.DEFAULT_PROJECT_NAME_URL, "");
            }

            redirectStrategy.sendRedirect(request, response, targetUrl);
            // 否则返回JSON提示
        } else {
            Object result = buildResponseContent(request);
            response.setStatus(HttpStatus.UNAUTHORIZED.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(result));
        }

    }

    /**
     * @param request
     * @return
     */
    protected Object buildResponseContent(HttpServletRequest request) {
        String message = "session已失效";
        if (isConcurrency()) {
            message = message + ",有可能是并发登录导致的";
        }
        return new SimpleResponse(message);
    }

    /**
     * session失效是否是并发导致的
     *
     * @return
     */
    protected boolean isConcurrency() {
        return false;
    }

    /**
     * Determines whether a new session should be created before redirecting (to
     * avoid possible looping issues where the same session ID is sent with the
     * redirected request). Alternatively, ensure that the configured URL does
     * not pass through the {@code SessionManagementFilter}.
     *
     * @param createNewSession defaults to {@code true}.
     */
    public void setCreateNewSession(boolean createNewSession) {
        this.createNewSession = createNewSession;
    }

}

Bean定义:

/**
 * 浏览器环境下扩展点配置,配置在这里的bean,业务系统都可以通过声明同类型或同名的bean来覆盖安全
 * 模块默认的配置。
 *
 * @author zhaohaibin
 */
@Configuration
public class BrowserSecurityBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;

    /**
     * session失效时的处理策略配置
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(InvalidSessionStrategy.class)
    public InvalidSessionStrategy invalidSessionStrategy() {
        return new DemoInvalidSessionStrategy(securityProperties);
    }

    /**
     * 并发登录导致前一个session失效时的处理策略配置
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(SessionInformationExpiredStrategy.class)
    public SessionInformationExpiredStrategy sessionInformationExpiredStrategy() {
        return new DemoExpiredSessionStrategy(securityProperties);
    }

    /**
     * 退出时的处理策略配置
     *
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(LogoutSuccessHandler.class)
    public LogoutSuccessHandler logoutSuccessHandler() {
        return new DemoLogoutSuccessHandler(securityProperties.getBrowser().getSignOutUrl());
    }

}

更新SecurityConstants:

/**
 * session失效默认的跳转地址
 */
String DEFAULT_SESSION_INVALID_URL = DEFAULT_PROJECT_NAME_URL + "/demoSessionInvalid.html";

配置:

新建Session属性配置类SessionProperties:

/**
 * session管理相关配置项
 *
 * @author zhaohaibin
 */
@Data
public class SessionProperties {

    /**
     * 同一个用户在系统中的最大session数,默认1
     */
    private int maximumSessions = 1;
    /**
     * 达到最大session时是否阻止新的登录请求,默认为false,不阻止,新的登录会将老的登录失效掉
     */
    private boolean maxSessionsPreventsLogin;
    /**
     * session失效时跳转的地址
     */
    private String sessionInvalidUrl = SecurityConstants.DEFAULT_SESSION_INVALID_URL;

}

更新BrowserProperties增加session属性配置:

/**
 * 默认注册页
 */
private String signUpUrl= DEFAULT_PROJECT_NAME_URL + "signUp.html";

/**
 * 默认登录页
 */
private String loginPage = SecurityConstants.DEFAULT_SIGN_IN_PAGE_URL;

/**
 * 登录页面,当引发登录行为的url以html结尾时,会跳到这里配置的url上
 */
private String signInPage = SecurityConstants.DEFAULT_SIGN_IN_PAGE_URL;

/**
 * session管理配置项
 */
private SessionProperties session = new SessionProperties();

/**
 * 退出成功时跳转的url,如果配置了,则跳到指定的url,如果没配置,则返回json数据。
 */
private String signOutUrl;

对应失效跳转页面:

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Session失效</title>
</head>
<body>
	<h2>安全模块默认的session失效提示页面</h2>
	<h3>请通过demo.security.browser.session.sessionInvalidUrl配置自己的页面URL</h3>
</body>
</html>

登录成功跳转页增加退出链接:

<!--默认退出url /logout-->
<a href="signOut">退出登录</a>

新建退出成功页面:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>退出成功</title>
</head>
<body>
<h2>退出成功</h2>
</body>
</html>

问题排查:
暂无

发布了81 篇原创文章 · 获赞 12 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/u012382791/article/details/105285147