spring boot(学习笔记第十三课)

spring boot(学习笔记第十三课)

  • 传统后端开发模式和前后端分离模式的不同,Spring Security的logout,invalidateHttpSession不好用,bug?

学习内容:

  1. 传统后端开发模式 vs 前后端分离模式
  2. Spring Security的logout功能
  3. invalidateHttpSession不好用,bug?原来还是功力不够!

1. 传统后端开发模式 vs 前后端分离模式

  1. 传统后端开发模式
    上面主要练习传统后端开发模式,在这种模式下,页面的渲染都是请求后端,在后端完成页面的渲染。认证的页面都是通过https://localhost:8080/loginPage进行用户名和密码的form填写,之后重定向到需要认证的资源的页面。
    在这里插入图片描述
    正如spring boot(学习笔记第十二课)的练习的那样,在传统后端开发模式,需要配置各种页面.
    .formLogin(form -> form.loginPage("/loginPage")
              .loginProcessingUrl("/doLogin")//这里的url不用使用controller进行相应,spring security自动处理
              .usernameParameter("uname")//页面上form的用户名
              .passwordParameter("passwd")
              .defaultSuccessUrl("/index")//默认的认证之后的页面
              .failureForwardUrl("/loginPasswordError"))//默认的密码失败之后的页面
    .exceptionHandling(exceptionHandling ->
                            exceptionHandling.accessDeniedHandler(new CustomizeAccessDeniedHandler()))
    
  2. 前后端分离开发模式
    现在web application的已经过渡到了前后端分离开发模式,而spring boot security也兼容这种模式。
    在这里插入图片描述
    接下来通过使用postman,模拟下前后端分离模式的spring security开发和使用场景。
    • 指定认证成功和失败的handler
      注意,这里一定要去掉 .loginPage("/loginPage")
      .formLogin(form -> form.loginProcessingUrl("/loginProcess")//这里对于前后端分离,提供的非页面访问url
                 .usernameParameter("uname")
                 .passwordParameter("passwd")
                 .successHandler(new SuccessHandler())
                 .failureHandler(new FailureHandler()))
      
    • 定义认证成功和失败的handler
      //success handler
          private static class SuccessHandler implements AuthenticationSuccessHandler {
              @Override
              public void onAuthenticationSuccess(
                      HttpServletRequest httpServletRequest,
                      HttpServletResponse httpServletResponse,
                      Authentication authentication
              ) throws IOException {
                  Object principal = authentication.getPrincipal();
                  httpServletResponse.setContentType("application/json;charset=utf-8");
                  PrintWriter printWriter = httpServletResponse.getWriter();
                  httpServletResponse.setStatus(200);
                  Map<String, Object> map = new HashMap<>();
                  map.put("status", 200);
                  map.put("msg", principal);
                  ObjectMapper om = new ObjectMapper();
                  printWriter.write(om.writeValueAsString(map));
                  printWriter.flush();
                  printWriter.close();
              }
          }
      
          //failure handler
          private static class FailureHandler implements AuthenticationFailureHandler {
              @Override
              public void onAuthenticationFailure(
                      HttpServletRequest httpServletRequest,
                      HttpServletResponse httpServletResponse,
                      AuthenticationException authenticationException
              ) throws IOException {
                  httpServletResponse.setContentType("application/json;charset=utf-8");
                  PrintWriter printWriter = httpServletResponse.getWriter();
                  httpServletResponse.setStatus(401);
                  Map<String, Object> map = new HashMap<>();
                  map.put("status", 401);
                  if (authenticationException instanceof LockedException) {
                      map.put("msg", "账户被锁定,登陆失败");
                  } else if (authenticationException instanceof BadCredentialsException) {
                      map.put("msg", "账户输入错误,登陆失败");
                  } else {
                      map.put("msg", authenticationException.toString());
                  }
                  ObjectMapper om = new ObjectMapper();
                  printWriter.write(om.writeValueAsString(map));
                  printWriter.flush();
                  printWriter.close();
              }
      
    • 一定要将/loginProcesspermitAll打开。注意,这里的习惯是将认证相关的url都定义成login开头的,并且一起进行/login*permitAll设定
          @Bean
          SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
              httpSecurity.authorizeHttpRequests(auth ->
                              auth.requestMatchers("/login*")
                                      .permitAll()
      
    • 使用postman进行认证测试。
      • pattern-1 正确的密码和用户名
        这里使用http://localhost:8080/loginProcess?uname=finlay_user&passwd=123456进行访问。注意,一定要是用post,不能使用get
        这里看到SuccessHandler
        在这里插入图片描述
    • pattern-2 错误的密码和用户名
      在这里插入图片描述
    • 认证成功,但是访问资源权限不够,需要设置exceptionHandling
      • 设置 exceptionHandling.accessDeniedHandler
       .exceptionHandling(exceptionHandling ->
                exceptionHandling.accessDeniedHandler(new CustomizeAccessDeniedHandler()))
      
      • 定义 exceptionHandler
        注意,在上一课传统后端开发模式的时候,定义的是redirect到画面,但是前后端分离模式,定义JSON返回值
        • 传统后端开发模式
        // 传统后端开发模式
        private static class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
        		@Override
        		public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        			   response.sendRedirect("/loginNoPermissionError");
        		}
        }
        
        • 传统前后端分离开发模式(JSON返回)
        // 传统前后端开发模式
        private static class CustomizeAccessDeniedHandler implements AccessDeniedHandler {
        		@Override
        		public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
        			   response.sendRedirect("/loginNoPermissionError");
        		}
        }
        
        • 访问/loginProcess,使用finlay_user(ROLE==user)进行登录
          在这里插入图片描述
        • 访问/db/hello,这里需要ROLE==DBA)进行登录,但是目前的httpSession不满足条件。在这里插入图片描述

2. Spring Security的logout功能

这里httpSession的如果需要logout,这里练习如何进行logout动作。

  1. 传统后端开发模式如何开发logout
    注意,这里传统后端开发模式需要将successHandlerfailureHandlerlogoutSuccessHandler都注释掉,否则,这个的对应的url设置都会无效
    .formLogin(form ->
             form.loginProcessingUrl("/loginProcess")//这里对于前后端分离,提供的非页面访问url
                     .usernameParameter("uname")
                     .passwordParameter("passwd")
                     .loginPage("/loginPage")
                     .failureForwardUrl("/loginPasswordError")
                     .successForwardUrl("/index"))
    //                                .successHandler(new SuccessHandler())
    //                                .failureHandler(new FailureHandler()))
     .logout(httpSecurityLogoutConfigurer ->
             httpSecurityLogoutConfigurer.logoutUrl("/logout")
                     .clearAuthentication(true)
                     .invalidateHttpSession(true)
                     .logoutSuccessUrl("/loginPage"))
    //                                .logoutSuccessHandler(new MyLogoutHandler()))
     .exceptionHandling(exceptionHandling ->
             exceptionHandling.accessDeniedHandler(new CustomizeAccessDeniedHandler()))
     .csrf(csrf -> csrf.disable())//csrf跨域访问无效
     .sessionManagement(session -> session
             .maximumSessions(-1)
             .maxSessionsPreventsLogin(true));
    
    • 设置logout处理的url
      .logoutUrl(“/logout”),这里的/logouot不需要进行对应,spring boot security会进行响应处理。
    • 对logout进行处理
       .logout(httpSecurityLogoutConfigurer ->
               httpSecurityLogoutConfigurer.logoutUrl("/logout")
                   .clearAuthentication(true)
                   .invalidateHttpSession(true)
                   .logoutSuccessUrl("/loginPage"))
      
      • clearAuthenticationSpring Security 中的一个方法,用于清除当前用户的认证信息,即使当前用户注销登录。在 SecurityContextHolder 中保存的 SecurityContext 对象将被清除,这意味着在下一次调用 SecurityContextHolder.getContext() 时,将不再有认证信息。
      • .invalidateHttpSession(true)是将httpSession删除,彻底进行logout
      • .logoutSuccessUrl("/loginPage"))调用将重定向到行的页面/logoutPage,这里是使用登录的页面。注意,这里如果调用.logoutSuccessHandler(new MyLogoutHandler())进行设定的话,就是使用前后端分离开发模式logoutSuccessUrl("/loginPage")即便设置也会无效
    • 设置logout处理页面(controller在页面上表示登录用户的用户名
       @GetMapping("/logoutPage")
          public String logoutPage(Model model) {
              String userName = "anonymous";
              Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
              if (authentication != null && authentication.isAuthenticated()) {
                  if (authentication.getName() != null) {
                      userName = authentication.getName();
                  }
              }
              model.addAttribute("login_user",userName);
              return "logout";
          }
      
    • 设置logout处理页面(html
      <!DOCTYPE html>
      <html lang="en">
      <head>
          <meta charset="UTF-8">
          <title>logout</title>
      </head>
      <body>
      <div th:text="${login_user}"></div>
      <form th:action="@{/logout}" method="post">
          <button type="submit" class="btn">Logout</button>
      </form>
      </body>
      </html>
      
    • 使用logout功能进行logout
      在这里插入图片描述
      在显示logout按钮的同时,也显示出了Authentication authentication = SecurityContextHolder.getContext().getAuthentication();取出来的login_user名字。
    • 点击logout按钮,成功后返回 .logoutSuccessUrl("/loginPage"))在这里插入图片描述
  2. 前后端分离开发模式如何开发logout
    • .logoutSuccessUrl("/loginPage"))替换成 .logoutSuccessHandler(new MyLogoutHandler()))

       .logout(httpSecurityLogoutConfigurer ->
               httpSecurityLogoutConfigurer.logoutUrl("/logout")
                       .clearAuthentication(true)
                       .invalidateHttpSession(true)
      //                                .logoutSuccessUrl("/loginPage"))
                       .logoutSuccessHandler(new MyLogoutHandler()))
      
    • 定义MyLogoutHandlerlogout结果包装成JSON格式,传给前端。

          private static class MyLogoutHandler implements LogoutSuccessHandler {
              @Override
              public void onLogoutSuccess(HttpServletRequest request
                      , HttpServletResponse response
                      , Authentication authentication) throws IOException {
                  HttpSession session = request.getSession(false);
                  if (session != null) {
                      // 使会话失效
                      session.invalidate();
                  }
                  response.setContentType("application/json;charset=utf-8");
                  PrintWriter printWriter = response.getWriter();
                  response.setStatus(200);
                  Map<String, Object> map = new HashMap<>();
                  map.put("status", 200);
                  map.put("msg", "logout OK");
                  ObjectMapper om = new ObjectMapper();
                  printWriter.write(om.writeValueAsString(map));
                  printWriter.flush();
                  printWriter.close();
              }
          }
      
    • 如果logout完毕了,没有有效httpSession,那么访问/db/hello资源的话,怎么让spring security返回JSON,让前端框架接收到呢。这里需要AuthenticationEntryPoint

      • 设定AuthenticationEntryPoint
        .logout(httpSecurityLogoutConfigurer ->
                 httpSecurityLogoutConfigurer.logoutUrl("/logout")
                         .clearAuthentication(true)
                         .invalidateHttpSession(true)
        //                                .logoutSuccessUrl("/loginPage"))
                         .logoutSuccessHandler(new MyLogoutHandler()))
        .exceptionHandling(exceptionHandling ->
                 exceptionHandling
                         .accessDeniedHandler(new CustomizeAccessDeniedHandler())
                         .authenticationEntryPoint(new RestAuthenticationEntryPoint()))
        
      • 定义AuthenticationEntryPoint
            private static class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
                    response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
                    response.setContentType("application/json");
        
                    String body = "{\"error\":\"Not Authenticated\"}";
                    OutputStream out = response.getOutputStream();
                    out.write(body.getBytes());
                    out.flush();
                }
            }
        
    • 使用postman模拟前端进行login在这里插入图片描述

    • 模拟前端调用/logout进行logout在这里插入图片描述

    • 模拟前端调用/db/hello进行没有httpSession的访问,期待返回authenciationErrorJSON应答。
      在这里插入图片描述

3. invalidateHttpSession不好用,bug?原来还是功力不够!

  1. sessionManagement的设定
    .sessionManagement(session -> session
                            .maximumSessions(1)
                            .maxSessionsPreventsLogin(true));
    
    在之前的设定中,一直设定的是.maximumSessions(-1),这个参数的意思是同一个用户同时登录spring boot security应用的数量,-1代表是没有限制,任意多个。在真正的系统中,一般会设定为1,意味着如果这个用户在另一个终端登录另外一个httpSession,那么当前的httpSession会被挤掉。
    那也意味着某一个用户执行,login->logout->login是能够在第二个login能够成功的,因为这里中间的logout已经invalidateHttpSession(true)了,但是试试果真如此吗?
  2. sessionManagement的设定maximumSessions(1),之后进行postman测试
    • 使用finlay_dba用户进行认证
      这里没有问题,认证OK。
      在这里插入图片描述
    • 访问http://localhost:8080:logout用户进行logout
      这里的logout也没有问题,成功。在这里插入图片描述
    • 访问http://localhost:8080/loginProcess用户进行再次login
      期待能够正常再次login,但是很遗憾,这里返回exceptionMaximum sessions of 1 for this principal exceeded
      在这里插入图片描述
  3. 如何解决问题
    • 问题在于尽管如下代码,在logout的时候进行了处理,但是和期待不同
      spring boot security不会将httpSession彻底无效化,调用了之后,spring boot security还是认为有httpSession正在登录,并没有过期expired
       .logout(httpSecurityLogoutConfigurer ->
                              httpSecurityLogoutConfigurer.logoutUrl("/logout")
                                      .clearAuthentication(true)
                                      .invalidateHttpSession(true)
      
    • 在一个csdn旺枝大师文章中,给出了解决方法。
      spring boot security使用SessionRegistryhttpSession进行管理,所以需要这里Autowired出来SessionRegistryjava bean,使用这个java beanLogoutSuccessHandler里面进行sessionexpireNow的调用。
      • 首先配置SessionRegistry
        @Configuration
        public class SessionRegistryConfig {
        
           @Bean
           public SessionRegistry getSessionRegistry(){
               return new SessionRegistryImpl();
           }
        
        }
        
        注意,这里的SessionRegistryImplspring boot security的内部类,直接使用,不需要定义。
      • SecurityConfig里面直接Autowired
        @Configuration
        public class SecurityConfig {
            @Bean
            PasswordEncoder passwordEncoder() {
                return NoOpPasswordEncoder.getInstance();
            }
            @Autowired
            private SessionRegistry sessionRegistry;
        
      • SecurityConfig里面的MyLogoutHandler增加处理,调用expireNow()
         private static class MyLogoutHandler implements LogoutSuccessHandler {
                private SecurityConfig securityConfig = null;
        
                public MyLogoutHandler(SecurityConfig securityConfig) {
                    this.securityConfig = securityConfig;
                }
        
                @Override
                public void onLogoutSuccess(HttpServletRequest request
                        , HttpServletResponse response
                        , Authentication authentication) throws IOException {
                    HttpSession session = request.getSession(false);
                    if (session != null) {
                        // 使会话失效
                        session.invalidate();
                    }
                    List<Object> o = securityConfig.sessionRegistry.getAllPrincipals();
                    //退出成功后删除当前用户session
                    for (Object principal : o) {
                        if (principal instanceof User) {
                            final User loggedUser = (User) principal;
                            if (authentication.getName().equals(loggedUser.getUsername())) {
                                List<SessionInformation> sessionsInfo = securityConfig.sessionRegistry.getAllSessions(principal, false);
                                if (null != sessionsInfo && sessionsInfo.size() > 0) {
                                    for (SessionInformation sessionInformation : sessionsInfo) {
                                        sessionInformation.expireNow();
                                    }
                                }
                            }
                        }
                    }
                    response.setContentType("application/json;charset=utf-8");
                    PrintWriter printWriter = response.getWriter();
                    response.setStatus(200);
                    Map<String, Object> map = new HashMap<>();
                    map.put("status", 200);
                    map.put("msg", "logout OK");
                    ObjectMapper om = new ObjectMapper();
                    printWriter.write(om.writeValueAsString(map));
                    printWriter.flush();
                    printWriter.close();
                }
            }
        
    • 进行login->logout->login的动作验证
      • 首先login
        在这里插入图片描述
      • 其次访问http://localhost:8080/logout在这里插入图片描述
      • 最后再次访问http://localhost:8080/loginProcess
        到此为止,完美的动作确认结束!在这里插入图片描述
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值