美文网首页安全Spring
三、Spring Security登录、Session及退出配置

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

作者: zenghi | 来源:发表于2019-07-18 15:13 被阅读0次

    一、登录配置

    对于表单登录,能配置登录成功和失败的跳转和重定向,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")
        }
    }
    

    相关文章

      网友评论

        本文标题:三、Spring Security登录、Session及退出配置

        本文链接:https://www.haomeiwen.com/subject/tlellctx.html