当前位置: 首页 > news >正文

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(学习笔记第十二课)](https://blog.csdn.net/s在这里插入图片描述
    ealaugh1980/article/details/140224760)的练习的那样,在传统后端开发模式,需要配置各种页面.
    .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 handlerprivate static class SuccessHandler implements AuthenticationSuccessHandler {@Overridepublic 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 handlerprivate static class FailureHandler implements AuthenticationFailureHandler {@Overridepublic 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设定
          @BeanSecurityFilterChain 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 {@Overridepublic void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {response.sendRedirect("/loginNoPermissionError");}
        }
        
        • 传统前后端分离开发模式(JSON返回)
        // 传统前后端开发模式
        private static class CustomizeAccessDeniedHandler implements AccessDeniedHandler {@Overridepublic 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 {@Overridepublic 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 {@Overridepublic 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 {@Beanpublic SessionRegistry getSessionRegistry(){return new SessionRegistryImpl();}}
        
        注意,这里的SessionRegistryImplspring boot security的内部类,直接使用,不需要定义。
      • SecurityConfig里面直接Autowired
        @Configuration
        public class SecurityConfig {@BeanPasswordEncoder passwordEncoder() {return NoOpPasswordEncoder.getInstance();}@Autowiredprivate SessionRegistry sessionRegistry;
        
      • SecurityConfig里面的MyLogoutHandler增加处理,调用expireNow()
         private static class MyLogoutHandler implements LogoutSuccessHandler {private SecurityConfig securityConfig = null;public MyLogoutHandler(SecurityConfig securityConfig) {this.securityConfig = securityConfig;}@Overridepublic 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();//退出成功后删除当前用户sessionfor (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
        到此为止,完美的动作确认结束!在这里插入图片描述

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Qt支持LG高级汽车内容平台
  • Springboot+Aop用注解实现阿里云短信验证码校验,校验通过自动删除验证码缓存
  • TF和TF-IDF区别和联系
  • qt 点击高亮的矩形,且可拖拽 开发过程笔记
  • WEB前端06-BOM对象
  • 【博士每天一篇文献-算法】连续学习算法之HNet:Continual learning with hypernetworks
  • 【ensp】防火墙------NET相关配置实验
  • leetcode_189. 轮转数组
  • LLMs之RAG:GraphRAG(本质是名词Knowledge Graph/Microsoft微软发布)的简介、安装和使用方法、案例应用之详细攻略
  • PAT甲级真题1020树的遍历
  • Spring Boot中@Async注解的使用及原理 + 常见问题及解决方案
  • MFC CRectTracker 类用法详解
  • Linux环境下安装Nodejs
  • Flutter热更新技术探索
  • 【ffmpeg命令入门】重新编码媒体流、设置码率、设置帧速率
  • 2018一半小结一波
  • Android 初级面试者拾遗(前台界面篇)之 Activity 和 Fragment
  • css选择器
  • golang 发送GET和POST示例
  • HashMap ConcurrentHashMap
  • java第三方包学习之lombok
  • JS正则表达式精简教程(JavaScript RegExp 对象)
  • Quartz实现数据同步 | 从0开始构建SpringCloud微服务(3)
  • React中的“虫洞”——Context
  • SAP云平台运行环境Cloud Foundry和Neo的区别
  • uni-app项目数字滚动
  • 从零到一:用Phaser.js写意地开发小游戏(Chapter 3 - 加载游戏资源)
  • 简单实现一个textarea自适应高度
  • 前端相关框架总和
  • 什么软件可以提取视频中的音频制作成手机铃声
  • 文本多行溢出显示...之最后一行不到行尾的解决
  • Redis4.x新特性 -- 萌萌的MEMORY DOCTOR
  • 好程序员大数据教程Hadoop全分布安装(非HA)
  • ​LeetCode解法汇总2670. 找出不同元素数目差数组
  • ​无人机石油管道巡检方案新亮点:灵活准确又高效
  • !! 2.对十份论文和报告中的关于OpenCV和Android NDK开发的总结
  • #ifdef 的技巧用法
  • #mysql 8.0 踩坑日记
  • #微信小程序:微信小程序常见的配置传旨
  • (1) caustics\
  • (JSP)EL——优化登录界面,获取对象,获取数据
  • (vue)el-tabs选中最后一项后更新数据后无法展开
  • (ZT)出版业改革:该死的死,该生的生
  • (二)Pytorch快速搭建神经网络模型实现气温预测回归(代码+详细注解)
  • (最新)华为 2024 届秋招-硬件技术工程师-单板硬件开发—机试题—(共12套)(每套四十题)
  • .h头文件 .lib动态链接库文件 .dll 动态链接库
  • .NET Core 和 .NET Framework 中的 MEF2
  • .net项目IIS、VS 附加进程调试
  • .ui文件相关
  • @JSONField或@JsonProperty注解使用
  • @staticmethod和@classmethod的作用与区别
  • [Algorithm][动态规划][01背包问题][目标和][最后一块石头的重量Ⅱ]详细讲解
  • [C++] vector list 等容器的迭代器失效问题
  • [C++11 多线程同步] --- 条件变量的那些坑【条件变量信号丢失和条件变量虚假唤醒(spurious wakeup)】
  • [CSAWQual 2019]Web_Unagi ---不会编程的崽