二、Spring Security登录、Session及退出配置

一、登录配置

对于表单登录,能配置登录成功和失败的跳转和重定向,Spring Security通过配置可以实现自定义跳转、重定向,以及用户未登录和登录用户无权限的处理。

1.1、URL配置

1.1.1、添加依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

1.1.2、自定义登录页面

resources/templates下编写简单test-login.html登录页面(参考官方文档),内容如下:

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
    <div th:if="${param.error}">
        <p>用户名或密码无效</p>
    </div>
    <form th:action="@{/my-login}" method="post">
        <div><label> 用户名 : <input type="text" name="username"/> </label></div>
        <div><label> 密码: <input type="password" name="password"/> </label></div>
        <button type="submit" class="btn">登录</button>
    </form>
</body>
</html>

用户名和密码名称默认是usernamepassword

创建登录页面映射Controller

@Controller  // 这里使用@Controller,跳转动态页面
public class PageController {
    @GetMapping("/user-login")
    public String myLoginPage(){
        return "test-login.html";
    }
}

1.1.3、WebSecurityConfig配置

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    // 自定义页面的路径不用验证
                    .antMatchers(HttpMethod.GET, "/user-login").permitAll()
                    .anyRequest().authenticated() 
                .and()
                    .formLogin()
                    // 设置自定义登录的页面
                    .loginPage("/user-login")
                    // 登录页表单提交的 action(th:action="@{/my-login}") URL
                    .loginProcessingUrl("/my-login");   
                    // post请求默认需要csrf验证, 这里使用Thymeleaf模板引擎,表单默认发送csrf,可不用关闭
                    //.and()
                    //.csrf().disable(); 
    }
}

启动程序后,访问localhost:8080/hello,会跳转到自定义登录页面登录成功,在F12可以看到自动发送csrf

图片

其他的登录成功和登录失败参考上面,配置如下:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                    // 自定义页面的路径不用验证
                    .antMatchers(HttpMethod.GET, "/user-login").permitAll()
                    // 失败跳转不用验证
                    .antMatchers(HttpMethod.GET, "/user-fail").permitAll()
                    .anyRequest().authenticated() 
                .and()
                    .formLogin()
                    // 设置自定义登录的页面
                    .loginPage("/user-login")
                    // 登录页表单提交的 action(th:action="@{/my-login}") URL
                    .loginProcessingUrl("/my-login");   
                   // .usernameParameter("username") // 默认就是 username
                  // .passwordParameter("password") // 默认就是 password
                    /** 
                     *  登录成功跳转:
                     *  登录成功,如果是直接从登录页面登录,会跳转到该URL;
                     *  如果是从其他页面跳转到登录页面,登录后会跳转到原来页面。
                     *  可设置true来任何时候到跳转 .defaultSuccessUrl("/hello2", true);
                     */
                    .defaultSuccessUrl("/hello2");
                    /**
                     *  登录成功重定向(和上面二选一)
                     */
                    .successForwardUrl("/hello3")
                    /**
                     *  登录失败跳转,指定的路径要能匿名访问
                     */
                    .failureUrl("/login-fail")
                      /**
                       *  登录失败重定向(和上面二选一)
                       */
                    .failureForwardUrl("/login-fail");
                    // post请求需要csrf验证, 这里使用Thymeleaf模板引擎,表单默认发送csrf,可不用关闭
                    //.and()
                    //.csrf().disable(); 
    }
}

1.2、登录处理器

上面使用URL进行的配置,都是通过Security默认提供的处理器处理的,一般多用于前后端不分离。

Spring SecurityAuthenticationManager用来处理身份认证的请求,处理的结果分两种:

  • 认证成功:结果由AuthenticationSuccessHandler处理
  • 认证失败:结果由AuthenticationFailureHandler处理。

Spring Security提供了多个实现于AuthenticationSuccessHandler接口和CustomAuthenticationFailHandler接口的子类,想自定义处理器,可以实现接口,或继承接口的实现类来重写。

1.2.1、自定义AuthenticationSuccessHandler

AuthenticationSuccessHandler是身份验证成功处理器的接口,其下有多个子类:

  • SavedRequestAwareAuthenticationSuccessHandler:默认的成功处理器,默认验证成功后,跳转到原路径。也可通过defaultSuccessUrl()配置。
  • SimpleUrlAuthenticationSuccessHandlerSavedRequestAwareAuthenticationSuccessHandler的父类,只有指定defaultSuccessUrl()时,才会被调用。作用:清除原路径,使用defaultSuccessUrl()指定的路径。如果直接使用该处理器,则总跳转到根路径。
  • ForwardAuthenticationSuccessHandler:请求重定向。只有指定successForwardUrl时被用到。

要想自定义成功处理器,可以通过实现AuthenticationSuccessHandler接口或继承其子类SavedRequestAwareAuthenticationSuccessHandler来实现:

  • 实现AuthenticationSuccessHandler接口

    如果直接返回Json数据时,可以实现AuthenticationSuccessHandler接口:

    public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler{
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            Authentication authentication) 
            throws ServletException, IOException {
          response.setContentType("application/json;charset=UTF-8");
          response.getWriter().append(
                  new ObjectMapper().createObjectNode()
                          .put("status", 200)
                          .put("msg", "登录成功")
                          .toString());
        }
    }
    
  • 继承SavedRequestAwareAuthenticationSuccessHandler

    如果只是在登录认证后,需要处理数据,再跳转回原路径时,可以继承该类:

    public class CustomAuthenticationSuccessHandler2 extends SavedRequestAwareAuthenticationSuccessHandler {
        @Override
        public void onAuthenticationSuccess(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            Authentication authentication) 
            throws ServletException, IOException {
            // 登录成功后,进行数据处理
            System.out.println("用户登录成功啦!!!");
            String authenticationStr = objectMapper.writeValueAsString(authentication);
            System.out.println("用户登录信息打印:" + authenticationStr);
    
            //处理完成后,跳转回原请求URL
            super.onAuthenticationSuccess(request, response, authentication);
        }
    }
    

Spring Security默认是使用SavedRequestAwareAuthenticationSuccessHandler,在配置中修改为自定义的AuthenticationSuccessHandler

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // 配置使用自定义成功处理器
                .successHandler(new AuthenticationSuccessHandler());
    }
}

1.2.2、自定义AuthenticationFailureHandler

AuthenticationFailureHandler是身份认证失败处理器的接口,其下有多个子类实现:

  • SimpleUrlAuthenticationFailureHandler:默认的失败处理器,默认认证失败后,跳转到登录页路径加error参数,如:http://localhost:8080/login?error。可通过failureUrl()配置。
  • ForwardAuthenticationFailureHandler:重定向到指定的URL
  • DelegatingAuthenticationFailureHandler:将AuthenticationException子类委托给不同的AuthenticationFailureHandler,意味着可以为AuthenticationException的不同实例创建不同的行为
  • ExceptionMappingAuthenticationFailureHandler:可以根据不同的AuthenticationException 类型,设置不同的跳转 url

自定义失败处理器,可以通过实现AuthenticationFailureHandler接口或继承其子类SimpleUrlAuthenticationFailureHandler来实现:

  • 实现AuthenticationFailureHandler接口:

    public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            AuthenticationException exception) 
            throws IOException, ServletException {
          response.setContentType("application/json;charset=UTF-8");
          response.getWriter().append(
                  new ObjectMapper().createObjectNode()
                          .put("status", 401)
                          .put("msg", "用户名或密码错误")
                          .toString());
        }
    }
    
  • 继承SimpleUrlAuthenticationFailureHandler

    public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {  
        @Override
        public void onAuthenticationFailure(HttpServletRequest request, 
                                            HttpServletResponse response, 
                                            AuthenticationException exception) 
            throws IOException, ServletException {
            // 登录失败后,进行数据处理
            System.out.println("登录失败啦!!!");
            String exceptionStr = objectMapper.writeValueAsString(exception.getMessage());
            System.out.println(exceptionStr);
    
            // 跳转原页面
            super.onAuthenticationFailure(request, response, exception);
        }
    }
    

Spring Security默认验证失败是使用SimpleUrlAuthenticationFailureHandler,在配置中修改为自定义的AuthenticationFailureHandler

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                // 配置使用自定义失败处理器
                .failureHandler(new AuthenticationFailureHandler());
    }
}

这里顺便提及DelegatingAuthenticationFailureHandlerExceptionMappingAuthenticationFailureHandler的使用:

  • DelegatingAuthenticationFailureHandler

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...
        @Bean
        public DelegatingAuthenticationFailureHandler delegatingAuthenticationFailureHandler(){
            LinkedHashMap<Class<? extends AuthenticationException>, AuthenticationFailureHandler> handlers = new LinkedHashMap<>();
            // 登录失败时,使用的失败处理器
            handlers.put(BadCredentialsException.class, new BadCredentialsAuthenticationFailureHandler());
            // 用户过期时,使用的失败处理器
            handlers.put(AccountExpiredException.class, new AccountExpiredAuthenticationFailureHandler());
            // 用户被锁定时,使用的失败处理
            handlers.put(LockedException.class, new LockedAuthenticationFailureHandler());
            return new DelegatingAuthenticationFailureHandler(handlers, new AuthenticationFailureHandler());
        }
        
      @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                  // 配置使用自定义失败处理器
                    .failureHandler(delegatingAuthenticationFailureHandler());
        }
    }
    
  • ExceptionMappingAuthenticationFailureHandler

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...
        @Bean
        public ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler(){
            ExceptionMappingAuthenticationFailureHandler handler = new ExceptionMappingAuthenticationFailureHandler();
            HashMap<String, String> map = new HashMap<>();
            // 登录失败时,跳转到 /badCredentials
            map.put(BadCredentialsException.class.getName(), "/badCredentials");
            // 用户过期时,跳转到 /accountExpired
            map.put(AccountExpiredException.class.getName(), "/accountExpired");
            // 用户被锁定时,跳转到 /locked
            map.put(LockedException.class.getName(), "/locked");
            handler.setExceptionMappings(map);
            return handler;
        }
        
      @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                  // 配置使用自定义失败处理器
                    .failureHandler(exceptionMappingAuthenticationFailureHandler());
        }
    }
    

1.3、认证入口

AuthenticationEntryPointSpring Security认证入口点接口,在用户请求处理过程中遇到认证异常时,使用特定认证方式进行认证。

AuthenticationEntryPoint内置实现类:

  • LoginUrlAuthenticationEntryPoint:根据配置的登录页面url,将用户重定向到该登录页面进行认证。默认的认证方式。

  • Http403ForbiddenEntryPoint:设置响应状态为403,不触发认证。通常在预身份认证中设置

    在某些情况下,使用Spring Security进行授权,但是在访问该应用程序之前,某些外部系统已经对该用户进行了可靠的身份验证。这些情况称为“预身份验证(pre-authenticated)”。

  • HttpStatusEntryPoint:设置特定的响应状态码,不触发认证。

  • BasicAuthenticationEntryPoint:设置基本(Http Basic)认证,在响应状态码401HeaderWWW-Authenticate:"Basic realm="xxx"时使用。

  • DigestAuthenticationEntryPoint:设置摘要(Http Digest)认证,在响应状态码401HeaderWWW-Authenticate:"Digest realm="xxx"时使用。

  • DelegatingAuthenticationEntryPoint:根据匹配URI来委托给不同的AuthenticationEntryPoint,且必须制定一个默认的认证方式。

1.3.1、自定义AuthenticationEntryPoint

  1. 自定义处理,需要新建类实现该AuthenticationEntryPoint接口:

    public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
        @Override
        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {
         response.setContentType("application/json;charset=UTF-8");
         response.getWriter().append(
                 new ObjectMapper().createObjectNode()
                         .put("status", 401)
                         .put("msg", "未登录,请登录后访问")
                         .toString());
        }
    }
    
  2. WebSecurityConfig配置:

    @EnableWebSecurity
    @EnableGlobalMethodSecurity(prePostEnabled = true)
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        ...       
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 指定未登录入口点
            http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
            ...
        }
    }
    

其它子类的用法:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    ...
    @Bean
    public DelegatingAuthenticationEntryPoint delegatingAuthenticationEntryPoint() {
        LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> map = new LinkedHashMap<>();
        // GET方式请求/test时,直接返回 403
        map.put(new AntPathRequestMatcher("/test", "GET"), new Http403ForbiddenEntryPoint());
        // 访问 /basic时,直接返回 400 bad request
        map.put(new AntPathRequestMatcher("/basic"), 
                new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
        DelegatingAuthenticationEntryPoint entryPoint = new DelegatingAuthenticationEntryPoint(map);
        // 除了上面两个 uri 配置指定的认证入口,其它默认使用 LoginUrlAuthenticationEntryPoint认证入口
        entryPoint.setDefaultEntryPoint(new LoginUrlAuthenticationEntryPoint("/user-login"));
        return entryPoint;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        /**
         * Http403ForbiddenEntryPoint 用法
         */
        // http.exceptionHandling()
        //     .authenticationEntryPoint(new Http403ForbiddenEntryPoint());
        /** 
         * HttpStatusEntryPoint 用法
         */
        // http.exceptionHandling()
        //     .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.BAD_REQUEST));
        /**
         * DelegatingAuthenticationEntryPoint 用法
         */
        http.exceptionHandling()
            .authenticationEntryPoint(delegatingAuthenticationEntryPoint());
        ...
    }
}

而对于摘要认证DigestAuthenticationEntryPoint,因为Http摘要认证必须基于MD5或明文,不能使用其它加密方式,且加密方式是MD5(username:realm:password),所以我们需要手动加密用户密码:

public int addUser(UserInfo userInfo) throws NoSuchAlgorithmException {
        String username = userInfo.getUsername();
        String password = userInfo.getPassword();

        // 加密密码
        MessageDigest md5 = MessageDigest.getInstance("MD5");
        String realm = "realm";  // 默认是 readlm
        String userData = username + ":" + realm + ":" + password;
        password = new String(Hex.encode(md5.digest(userData.getBytes())));

        userInfo.setPassword(password);
        return userMapper.addUser(userInfo);
}

WebSecurityConfig配置中:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        // 因为已经使用摘要认证MD5加密,不用再加密,所以这里设置为明文
        return new PasswordEncoder() {
            @Override
            public String encode(CharSequence charSequence) {
                return charSequence.toString();
            }

            @Override
            public boolean matches(CharSequence charSequence, String s) {
                return s.equals(charSequence.toString());
            }
        };
    }
    
    // 摘要认证的过滤器
    @Bean
    public DigestAuthenticationFilter digestAuthenticationFilter() {
        DigestAuthenticationFilter filter = new DigestAuthenticationFilter();
        filter.setAuthenticationEntryPoint(digestAuthenticationEntryPoint());//必须配置
        filter.setPasswordAlreadyEncoded(true); // 密码需要加密,设为true
        filter.setUserDetailsService(userDetailsService);//必须配置
        return filter;
    }

    @Bean
    public DigestAuthenticationEntryPoint digestAuthenticationEntryPoint() {
        DigestAuthenticationEntryPoint point = new DigestAuthenticationEntryPoint();
        point.setRealmName("realm");//realm名称,默认为realm,该名称和加密密码的realm一样
        return point;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                // 使用摘要认证的入口
                .exceptionHandling().authenticationEntryPoint(digestAuthenticationEntryPoint())
                .and()
                .authorizeRequests()
                .antMatchers(HttpMethod.POST, "/addUser").permitAll()
                .antMatchers("/hello2").permitAll()
                .anyRequest().authenticated()
                .and()
                .csrf().disable()
                // 摘要认证的过滤器
                .addFilter(digestAuthenticationFilter())
}

1.4、无权限处理器

自定义处理,需要新建类实现该AccessDeniedHandler接口:

public class CustomAccessDeniedHandler implements AccessDeniedHandler {
    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException e) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().append(
                new ObjectMapper().createObjectNode()
                        .put("status", 401)
                        .put("msg", "无访问权限")
                        .toString());
    }
}

WebSecurityConfig配置:

@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    ...    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 先注释,用登录页面登录
        //http.exceptionHandling().authenticationEntryPoint(new CustomAuthenticationEntryPoint());
        http.exceptionHandling().accessDeniedHandler(new CustomAccessDeniedHandler());
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .csrf().disable();
    }
}

启动程序,访问localhost:8080/get-user,跳转登录页面,输入用户名、密码登录后,访问无权限的资源,会返回无权限Json信息:

图片

1.5、记住登录

Spring Security记住登录功能有两种方式:基于浏览器的Cookie存储和基于数据库的存储。

登录页添加记住登录按钮

<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
<head>
    <meta charset="UTF-8">
    <title>登录页面</title>
</head>
<body>
<div th:if="${param.error}">
    <p>用户名或密码无效</p>
</div>
<form th:action="@{/my-login}" method="post">
    <div><label> 用户名 : <input type="text" name="username"/> </label></div>
    <div><label> 密码: <input type="password" name="password"/> </label></div>
    <div>
        <label><input type="checkbox" name="remember-me"/>记住登录</label>
        <button type="submit">登录</button>
    </div>
</form>
</body>
</html>

1.5.1、Cookie存储

WebSecurityConfig配置:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    //...
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/user-login").permitAll()
                .loginProcessingUrl("/my-login")
                .successHandler(new CustomAuthenticationSuccessHandler())
                .failureHandler(new CustomAuthenticationFailureHandler())
                .and()
                .rememberMe()
                // 即登录页面的记住登录按钮的参数名
                .rememberMeParameter("remember-me")
                // 过期时间
                .tokenValiditySeconds(1800)
                .and()
                .csrf().disable();
    }
}

启动程序,在勾选记住登录下进行登录,cookie信息如下,remember-me的过期时间内,重启浏览器访问不用登录。


图片

1.5.2、数据库存储

使用 Cookie 存储虽然很方便,但是Cookie毕竟是保存在客户端的,而且 Cookie 的值还与用户名、密码这些敏感数据相关,虽然加密,但是将敏感信息存在客户端,毕竟不太安全。

Spring security 还提供了另一种更安全的实现机制:在客户端的 Cookie 中,仅保存一个无意义的加密串(与用户名、密码等敏感数据无关),然后在数据库中保存该加密串-用户信息的对应关系,自动登录时,用 Cookie 中的加密串,到数据库中验证,如果通过,自动登录才算通过。

WebSecurityConfig 中注入 dataSource ,创建一个 PersistentTokenRepositoryBean,并配置数据库存储自动登录:

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Autowired
    private DataSource dataSource;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 启动时创建表,注意,创建好表后,注释掉
        // tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/login").permitAll()
                .and()
                // 记住登录
                .rememberMe()
                // 记住我的数据存储,调用上面写的方法
                .tokenRepository(persistentTokenRepository())
                // 过期时间
                .tokenValiditySeconds(1800)
                .and()
                .csrf().disable();
    }
}

二、session管理

在执行认证过程之前,Spring Security将运行SecurityContextPersistenceFilter过滤器负责存储安请求之间的全上下文,上下文根据策略进行存储,默认为HttpSessionSecurityContextRepository ,其使用http session作为存储器。

对于session管理,有三种:

  1. session超时处理:session有效的时间,超时后删除
  2. session并发控制:同个用户登录,是否强制退出前一个登录,还是禁止后一个登录。
  3. 集群session管理:默认session是放在单个服务器的单个应用里,在集群中,会出现在一个节点应用登录后,session只能在该节点使用。另一个节点不能使用其他节点的session,还会需要登录,所以需要集群共用一个session

2.1、session超时

设置Session的超时,很简单,只需要在配置文件application.yml配置即可,如下为设置50秒:

  • Springboot2.0前的版本:
spring:
  session:
    timeout: 50
  • Springboot2.0后的版本:
server:
  servlet:
    session:
      timeout: 50

上面设置Session失效时间为50s,实际源码TomcatEmbeddedServletContainerFactory类内部会取1分钟。源码内部转成分钟,然后设置给tomcat原生的StandardContext,所以一般设置为60秒的整数倍。

其实通过上面配置的点击进去源码发现:

public void setTimeout(Duration timeout) {
    this.timeout = timeout;
}

参数传入的是Duration的实例,DurationJava8新增的,用来计算日期差值,并且是被final声明,是线程安全的

Duration转换字符串方式,默认为正;负以-开头,紧接着P

以下字母不区分大小写:

  • D:天
  • T:天和小时之间的分隔符
  • H:小时
  • M:分钟
  • S:秒

每个单位都必须是数字,且时分秒顺序不能乱
比如:

  • P2DT3M5S235
  • P3D:3`天
  • PT3H3小时

所以上面配置文件中可以写:

server:
  servlet:
    session:
      timeout: PT50S

2.2、session超时处理

2.2.1、超时跳转URL

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // session无效时跳转的url
            http.sessionManagement().invalidSessionUrl("/session/invalid");
            http
                .authorizeRequests()
                // 需要放行条跳转的url
                .antMatchers("/session/invalid").permitAll()
                .anyRequest().authenticated()
        }
    }
}

2.2.2、超时处理器

session无效时的处理策略,优先级比上面的高

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private CustomInvalidSessionStrategy invalidSessionStrategy;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置session无效处理策略
        http.sessionManagement().invalidSessionStrategy(invalidSessionStrategy);
        http
                .authorizeRequests()
                .antMatchers("/session/invalid").permitAll()
                .anyRequest().authenticated()
    }
}

处理策略:

@Component
public class CustomInvalidSessionStrategy implements InvalidSessionStrategy {
    @Override
    public void onInvalidSessionDetected(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
        // 自定义session无效处理
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().append("session无效,请重新登录");
    }
}

2.3、session并发控制

默认下,我们可以在不同浏览器同时登录同一个用户,这样就会保存了多个Session,而有时,我们需要只能在一处地方登录,其他地方的登录就让前一个失效或不能登录。

2.3.1、后登录致前登录失效

在一个浏览器登录后,再到另一个浏览器登录,再回到前一个登录刷新页面,登录失效。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
             // 设置session无效处理策略
            .invalidSessionStrategy(invalidSessionStrategy)
            // 设置同一个用户只能有一个登陆session
            .maximumSessions(1);
        http
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}

上面设置maximumSessions设置为1后,只能有一个登录Session,多个登录,后一个会把前一个登录的Sesson失效。

而对于前一个登录Sesson失效后,刷新页面会显示:

This session has been expired (possibly due to multiple concurrent logins being attempted as the same user).

我们也可以自定义失效返回信息,有两种

  1. 设置失效session处理URL:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy)
                    .maximumSessions(1)
                    // 其他地方登录session失效处理URL
                    .expiredUrl("/session/expired");
            http
                    .authorizeRequests()
                 // URL不需验证
                    .antMatchers("/session/expired").permitAll()
                    .anyRequest().authenticated()
        }
    }
    
  2. 设置失效session处理策略:

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {  
        @Autowired
        private CustomSessionInformationExpiredStrategy sessionInformationExpiredStrategy;
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http.sessionManagement()
                    .invalidSessionStrategy(invalidSessionStrategy)
                    .maximumSessions(1)
                    // 其他地方登录session失效处理策略
                    .expiredSessionStrategy(sessionInformationExpiredStrategy);
            http
                    .authorizeRequests()
                    .anyRequest().authenticated()
        }
    }
    

    过期策略:

    @Component
    public class CustomSessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
        @Override
        public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
            HttpServletResponse response = event.getResponse();
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write("当前用户已在其他地方登录...");
        }
    }
    

2.3.2、前登录禁后登录

有时,我们在一个地方登录正在操作,不能被打断,这时就要禁止在其他地方登录导致当前的登录Session失效。

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter { 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.sessionManagement()
            .invalidSessionStrategy(invalidSessionStrategy)
            .maximumSessions(1)
            // 设置为true,即禁止后面其它人的登录 
            .maxSessionsPreventsLogin(true)
            .expiredSessionStrategy(sessionInformationExpiredStrategy);
        http
            .authorizeRequests()
            .anyRequest().authenticated()
    }
}

禁止后登录后,可以通过如下方式判断异常进行用户通知:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Autowired
    private ObjectMapper objectMapper;
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, 
                                        HttpServletResponse response, 
                                        AuthenticationException exception) {
        response.setContentType("application/json;charset=utf-8");
        if (exception instanceof SessionAuthenticationException){
            response.getWriter().write("用户已在其它地方登录,禁止当前登录...");
        }
    }
}

2.4、集群session管理

在部署应用时,搭建至少两台机器的集群环境,防止一台服务器出现问题而服务中断,这样在一台机器在停止服务时,另一台机器还能继续提供服务。

而使用集群,在基于Session的身份认证就会导致问题:一个用户登录成功后,其Session存放在A机器上,而如果Session不做其他处理,在用户操作时,在负载均衡下,可能会请求发到B机器上,而B机器无Session导致无权限访问而需要再次登录。

而解决集群中Session的管理,可以把Session抽取出来为一个独立存储,用户请求需要Session时都会读取该存储Session

1585560853325

Spring提供有Spring Session来处理集群Session管理,需要引入如下依赖:

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

使用redis作为Session存储管理,而Spring Session支持以下方式存储Session,这里只使用Redis。

public enum StoreType {
    REDIS,
    MONGODB,
    JDBC,
    HAZELCAST,
    NONE;
    private StoreType() {
    }
}

在配置文件application.yml中配置Redis:

spring:
  session:
    store-type: redis   # session存储类型为 redis
  redis:
    database: 1
    host: localhost
    port: 6379
    # 更新策略,ON_SAVE在调用#SessionRepository#save(Session)时,在response commit前刷新缓存,
    # IMMEDIATE只要有任何更新就会刷新缓存
    flush-mode: on_save  # 默认
    # 存储session的密钥的命名空间
    namespace: spring:session #默认

以不同的端口启动程序,如分别以端口80808081启动两个服务。访问8080端口登录后,在访问8081就不需要登录了,说明Session被共用了。

二、退出登录

默认的退出登录URL/logout,如前面登录的程序,访问localhost:8080/logout便退出登录,退出登录后,默认跳转到登录页面。

2.1、自定义退出URL

也可通过在WebSecurityConfig进行自定义配置:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登录的url, 默认为/logout
                .logoutUrl("/logout2")
    }
}

2.2、退出成功处理

  1. 退出成功处理URL:

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .logout()
                    // 退出登录的url, 默认为/logout
                    .logoutUrl("/logout2")
                 // 退出成功跳转URL,注意该URL不需要权限验证
                    .logoutSuccessUrl("/logout/success").permitAll()
        }
    }
    
  2. 退出成功处理器

    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .logout()
                    // 退出登录的url, 默认为/logout
                    .logoutUrl("/logout2")
                 // 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll
                    //.logoutSuccessUrl("/logout/success").permitAll()
                 //退出登录成功处理器
                    .logoutSuccessHandler(logoutSuccessHandler)
        }
    }
    

    处理器:

    @Component
    public class CustomLogoutSuccessHandler implements LogoutSuccessHandler {
        @Override
        public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
            response.setContentType("application/json;charset=utf-8");
            response.getWriter().write("退出登录成功");
        }
    }
    

2.3、退出成功删除Cookie

默认退出后不会删除Cookie。可配置退出后删除:

public class WebSecurityConfig extends WebSecurityConfigurerAdapter {    
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .logout()
                // 退出登录的url, 默认为/logout
                .logoutUrl("/logout2")
                // 退出成功跳转URL,注意该URL不需要权限验证,所有加.permitAll
                //.logoutSuccessUrl("/logout/success").permitAll()
                //退出登录成功处理器
                .logoutSuccessHandler(logoutSuccessHandler)
                // 退出登录删除指定的cookie
                .deleteCookies("JSESSIONID")
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345

推荐阅读更多精彩内容

  • 成功是有方法的,不信看下面三条。 一.学会靠别人。每个都有自己的特点,你有你的,别人有别人的,你...
    鸿运当头168阅读 291评论 0 1
  • 1)查找被占用的端口: abloume@ubuntu:~$ netstat -tln | grep 8000tcp...
    23d7c1910238阅读 1,192评论 0 0
  • 作者:詹姆斯,艾伦 001 人的内心就好像一个庭院,它可以长出美丽的花朵,也可以长满杂草。 Sp :除个草,撒点苗...
    破土的芬芳阅读 351评论 0 0
  • 傍晚,几人散步。 操场很大,绿茵茵的人工草坪颇为宽大。虽是人工的绿色,也给校园带来了几丝生气。晚风...
    豆小小豆阅读 281评论 0 0
  • 韩晴是个小餐厅的职员, 一天早上, 她在一堆树枝下发现了小狗皮皮, 蜷缩着的身子, 大大的泛着泪光的眼睛, 好像在...
    94暖暖阳光阅读 302评论 0 2