网站为什么做黄词骗流量,北住房和城乡建设厅网站,公司网站设计思路,青岛网站优化联系方式spring boot(学习笔记第十三课) 传统后端开发模式和前后端分离模式的不同#xff0c;Spring Security的logout#xff0c;invalidateHttpSession不好用#xff0c;bug#xff1f; 学习内容#xff1a;
传统后端开发模式 vs 前后端分离模式Spring Security的logout功能inv…spring boot(学习笔记第十三课) 传统后端开发模式和前后端分离模式的不同Spring Security的logoutinvalidateHttpSession不好用bug 学习内容
传统后端开发模式 vs 前后端分离模式Spring Security的logout功能invalidateHttpSession不好用bug?原来还是功力不够 1. 传统后端开发模式 vs 前后端分离模式
传统后端开发模式 上面主要练习传统后端开发模式在这种模式下页面的渲染都是请求后端在后端完成页面的渲染。认证的页面都是通过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()))前后端分离开发模式 现在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;charsetutf-8);PrintWriter printWriter httpServletResponse.getWriter();httpServletResponse.setStatus(200);MapString, 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;charsetutf-8);PrintWriter printWriter httpServletResponse.getWriter();httpServletResponse.setStatus(401);MapString, 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();}一定要将/loginProcess的permitAll打开。注意这里的习惯是将认证相关的url都定义成login开头的并且一起进行/login*的permitAll设定 BeanSecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {httpSecurity.authorizeHttpRequests(auth -auth.requestMatchers(/login*).permitAll()使用postman进行认证测试。 pattern-1 正确的密码和用户名 这里使用http://localhost:8080/loginProcess?unamefinlay_userpasswd123456进行访问。注意一定要是用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(ROLEuser)进行登录 访问/db/hello这里需要ROLEDBA)进行登录但是目前的httpSession不满足条件。
2. Spring Security的logout功能
这里httpSession的如果需要logout这里练习如何进行logout动作。
传统后端开发模式如何开发logout 注意这里传统后端开发模式需要将successHandlerfailureHandler和logoutSuccessHandler都注释掉否则这个的对应的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))clearAuthentication 是 Spring 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 langen
headmeta charsetUTF-8titlelogout/title
/head
body
div th:text${login_user}/div
form th:action{/logout} methodpostbutton typesubmit classbtnLogout/button
/form
/body
/html使用logout功能进行logout 在显示logout按钮的同时也显示出了Authentication authentication SecurityContextHolder.getContext().getAuthentication();取出来的login_user名字。点击logout按钮成功后返回 .logoutSuccessUrl(/loginPage)) 前后端分离开发模式如何开发logout 将 .logoutSuccessUrl(/loginPage))替换成 .logoutSuccessHandler(new MyLogoutHandler())) .logout(httpSecurityLogoutConfigurer -httpSecurityLogoutConfigurer.logoutUrl(/logout).clearAuthentication(true).invalidateHttpSession(true)
// .logoutSuccessUrl(/loginPage)).logoutSuccessHandler(new MyLogoutHandler()))定义MyLogoutHandler将logout结果包装成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;charsetutf-8);PrintWriter printWriter response.getWriter();response.setStatus(200);MapString, 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的访问期待返回authenciationError的JSON应答。
3. invalidateHttpSession不好用bug?原来还是功力不够
sessionManagement的设定.sessionManagement(session - session.maximumSessions(1).maxSessionsPreventsLogin(true));在之前的设定中一直设定的是.maximumSessions(-1)这个参数的意思是同一个用户同时登录spring boot security应用的数量-1代表是没有限制任意多个。在真正的系统中一般会设定为1意味着如果这个用户在另一个终端登录另外一个httpSession那么当前的httpSession会被挤掉。 那也意味着某一个用户执行login-logout-login是能够在第二个login能够成功的因为这里中间的logout已经invalidateHttpSession(true)了但是试试果真如此吗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。 如何解决问题 问题在于尽管如下代码在logout的时候进行了处理但是和期待不同 spring boot security不会将httpSession彻底无效化调用了之后spring boot security还是认为有httpSession正在登录并没有过期expired。 .logout(httpSecurityLogoutConfigurer -httpSecurityLogoutConfigurer.logoutUrl(/logout).clearAuthentication(true).invalidateHttpSession(true)在一个csdn旺枝大师文章中给出了解决方法。 spring boot security使用SessionRegistry对httpSession进行管理所以需要这里Autowired出来SessionRegistry的java bean使用这个java bean在LogoutSuccessHandler里面进行session的expireNow的调用。 首先配置SessionRegistryConfiguration
public class SessionRegistryConfig {Beanpublic SessionRegistry getSessionRegistry(){return new SessionRegistryImpl();}}注意这里的SessionRegistryImpl是spring boot security的内部类直接使用不需要定义。在SecurityConfig里面直接AutowiredConfiguration
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();}ListObject o securityConfig.sessionRegistry.getAllPrincipals();//退出成功后删除当前用户sessionfor (Object principal : o) {if (principal instanceof User) {final User loggedUser (User) principal;if (authentication.getName().equals(loggedUser.getUsername())) {ListSessionInformation sessionsInfo securityConfig.sessionRegistry.getAllSessions(principal, false);if (null ! sessionsInfo sessionsInfo.size() 0) {for (SessionInformation sessionInformation : sessionsInfo) {sessionInformation.expireNow();}}}}}response.setContentType(application/json;charsetutf-8);PrintWriter printWriter response.getWriter();response.setStatus(200);MapString, 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 到此为止完美的动作确认结束