spring boot(学习笔记第十三课)
- Spring Security的logout,传统后端开发模式和前后端分离模式的不同,invalidateHttpSession不好用,bug?
学习内容:
- 传统后端开发模式 vs 前后端分离模式
- Spring Security的logout功能
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;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();}
- 一定要将
/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?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
动作。
传统后端开发模式
如何开发logout
注意,这里传统后端开发模式
需要将successHandler
,failureHandler
和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 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"))
- 设置logout处理的
前后端分离开发模式
如何开发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;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
的访问,期待返回authenciationError
的JSON
应答。
-