上一篇將OAuth2授權模式的password模式改造成了授權碼模式,並初步實現了一個前后端分離架構下基於session的微服務的SSO。用戶在客戶端點擊登錄,會跳轉到認證服務器的登錄頁面進行登錄,登錄成功后,認證服務器回調到客戶端應用的callback方法,並攜帶了授權碼,客戶端拿着授權碼去認證服務器換取access_token ,客戶端拿到access_token后存到自己的session,就認為該用戶已登錄成功。
上邊這個流程是一個基於session的SSO,其中有三個效期:
1,客戶端應用的session的有效期,控制着多長時間跳轉一次認證服務器
2,認證服務器的session的有效期 , 控制多長時間需要用戶輸入一次用戶名密碼
3,access_token的有效期,控制着登錄一次能訪問多久的微服務
如上篇所說,目前還存在着一系列的問題,比如點擊退出,只是將客戶端應用的session失效掉了,並沒有將認證服務器的session失效,用戶退出后,點擊登錄按鈕,重定向到認證服務器,由於認證服務器的session並沒有失效,所以認證服務器會自動回調到客戶端,客戶端表現就是直接就又登錄了,給用戶的感覺就是點了退出按鈕,但是並沒有退出去。下邊就來解決這個問題,思路也很簡單,點擊退出按鈕的時候,同時將客戶端和認證服務器的session都失效。下面開始寫代碼。
處理退出登錄邏輯
退出按鈕的處理:
1,將自己客戶端應用的session失效
2,將認證服務器的session失效,
這樣,再次點擊退出按鈕,客戶端session失效后,又跳轉到了認證服務器,這是認證服務器默認給的一個提示
點擊確定,頁面停留在了認證服務器的默認的登錄頁面:
輸入用戶名(隨便),密碼(123456 認證服務器寫死的),點擊sign in,
會跳轉到了認證服務器默認的首頁,沒有,所以出現了404。
為什么直接在客戶端應用點擊登錄按鈕,登錄成功后就可以跳回到客戶端應用?看一下在客戶端應用點擊登錄按鈕的處理:
里面有一個 redirect_uri 參數,這樣的請求,認證服務器在登錄成功后,就知道要跳轉到redirect_uri 。但是點擊退出后出現的登錄頁面 ,是由【退出】觸發的,認證服務器是不知道登錄成功后要跳轉到admin應用的。所以,要做退出的處理,讓認證服務器知道,退出后要跳轉到指定的uri,思路就是在退出的請求上,加一個 redirect_uri的參數,重寫認證服務器的退出邏輯,退出后跳轉到redirect_uri 即可。
請求認證服務器的退出邏輯的請求上,加上 redirect_uri=http://admin.nb.com:8080/index
在認證服務器上找到Spring處理退出邏輯的過濾器 org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter :
/** * Generates a default log out page. * * @author Rob Winch * @since 5.1 */ public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = request -> Collections .emptyMap(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (this.matcher.matches(request)) { renderLogout(request, response); } else { filterChain.doFilter(request, response); } } private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException { String page = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + " <head>\n" + " <meta charset=\"utf-8\">\n" + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n" + " <meta name=\"description\" content=\"\">\n" + " <meta name=\"author\" content=\"\">\n" + " <title>Confirm Log Out?</title>\n" + " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n" + " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n" + " </head>\n" + " <body>\n" + " <div class=\"container\">\n" + " <form class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n" + " <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + renderHiddenInputs(request) + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n" + " </form>\n" + " </div>\n" + " </body>\n" + "</html>"; response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(page); } /** * Sets a Function used to resolve a Map of the hidden inputs where the key is the * name of the input and the value is the value of the input. Typically this is used * to resolve the CSRF token. * @param resolveHiddenInputs the function to resolve the inputs */ public void setResolveHiddenInputs( Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) { Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); this.resolveHiddenInputs = resolveHiddenInputs; } private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) { sb.append("<input name=\"").append(input.getKey()).append("\" type=\"hidden\" value=\"").append(input.getValue()).append("\" />\n"); } return sb.toString(); } }
1,重寫退出表單源碼
這就是處理退出邏輯的過濾器,其中的html就是之前看到的讓用戶確認退出的頁面,在認證服務器項目新建一個一模一樣的包,將上邊的類copy進去,由於java的類加載機制,自己寫的類會優先於spring的類加載,java會加載我們自己寫的類,而不加載spring包里的類:
重寫后的類源碼:
package org.springframework.security.web.authentication.ui; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.RequestMatcher; import org.springframework.util.Assert; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.Collections; import java.util.Map; import java.util.function.Function; /** * 重寫退出邏輯,由於java的類加載機制,會優先執行自己的類,就不加載spring的了 * 這里有一個默認的 確認退出頁面,可以定制 * 這里注釋掉確認退出的提示語,直接寫一段js腳本,提交退出表單 * 從request里獲取到退出邏輯攜帶的 redirect_uri 參數,放入退出表單的隱藏input, * 這樣在重寫退出成功handler時,可以拿出這個參數,做跳轉 * Generates a default log out page. * * @author Rob Winch * @since 5.1 */ public class DefaultLogoutPageGeneratingFilter extends OncePerRequestFilter { private RequestMatcher matcher = new AntPathRequestMatcher("/logout", "GET"); private Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs = request -> Collections .emptyMap(); @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { if (this.matcher.matches(request)) { renderLogout(request, response); } else { filterChain.doFilter(request, response); } } private void renderLogout(HttpServletRequest request, HttpServletResponse response) throws IOException { String page = "<!DOCTYPE html>\n" + "<html lang=\"en\">\n" + " <head>\n" + " <meta charset=\"utf-8\">\n" + " <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n" + " <meta name=\"description\" content=\"\">\n" + " <meta name=\"author\" content=\"\">\n" + " <title>Confirm Log Out?</title>\n" + " <link href=\"https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css\" rel=\"stylesheet\" integrity=\"sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M\" crossorigin=\"anonymous\">\n" + " <link href=\"https://getbootstrap.com/docs/4.0/examples/signin/signin.css\" rel=\"stylesheet\" crossorigin=\"anonymous\"/>\n" + " </head>\n" + " <body>\n" + " <div class=\"container\">\n" + " <form id=\"logoutForm\" class=\"form-signin\" method=\"post\" action=\"" + request.getContextPath() + "/logout\">\n" // + " <h2 class=\"form-signin-heading\">Are you sure you want to log out?</h2>\n" + renderHiddenInputs(request) // + " <button class=\"btn btn-lg btn-primary btn-block\" type=\"submit\">Log Out</button>\n" + "<input type='hidden' name='redirect_uri' value="+request.getParameter("redirect_uri")+"/>" + "<script>document.getElementById('logoutForm').submit()</script>" + " </form>\n" + " </div>\n" + " </body>\n" + "</html>"; response.setContentType("text/html;charset=UTF-8"); response.getWriter().write(page); } /** * Sets a Function used to resolve a Map of the hidden inputs where the key is the * name of the input and the value is the value of the input. Typically this is used * to resolve the CSRF token. * @param resolveHiddenInputs the function to resolve the inputs */ public void setResolveHiddenInputs( Function<HttpServletRequest, Map<String, String>> resolveHiddenInputs) { Assert.notNull(resolveHiddenInputs, "resolveHiddenInputs cannot be null"); this.resolveHiddenInputs = resolveHiddenInputs; } private String renderHiddenInputs(HttpServletRequest request) { StringBuilder sb = new StringBuilder(); for (Map.Entry<String, String> input : this.resolveHiddenInputs.apply(request).entrySet()) { sb.append("<input name=\"").append(input.getKey()).append("\" type=\"hidden\" value=\"").append(input.getValue()).append("\" />\n"); } return sb.toString(); } }
上述代碼中的熒光綠色的是注釋掉的兩行代碼,這兩行代碼是說顯示的讓用戶確認退出登錄的提示,這里給注釋掉,讓用戶看不到退出提醒
上述代碼中的熒光黃色的是新添加的代碼,給退出登錄的表單加了個id,然后在表單里寫了個隱藏域,name= redirect_uri,值從request里獲取,用於自定義退出成功Handler里,可以重定向到該路徑。最后新增一個JavaScript腳本,自動提交表單。
如果你就想給用戶一個退出提示,可以重寫這個表單的樣式。
2,下面自定義退出登錄成功處理器
3,配置退出成功處理器
實驗
啟動四個微服務
訪問客戶端應用 http://admin.nb.com:8080/index/
點擊去登錄,跳轉到了認證服務器的登錄頁面
登錄成功,回調到客戶端應用admin,點擊獲取訂單信息,獲取到了訂單信息
點擊退出登錄,先是在客戶端應用將session失效,然后再去認證服務器上做退出登錄操作()
然后又跳轉到了客戶端應用的index頁
總結
本篇解決了上篇遺留的問題(點擊退出登錄只是在客戶端應用做session失效操作,當再次點擊登錄后,由於認證服務器的session還有效,用戶不用輸入用戶名密碼直接就登錄了,給人的感覺是沒有徹底退出去)。
本節在客戶端應用做退出操作的同時,也在認證服務器上將session失效掉,讓用戶徹底退出登錄。思路是在點擊【退出登錄】按鈕的同時做兩件事,一是讓客戶端應用的session失效,然后再發一個請求到認證服務器的 /logout 端點,這是spring OAuth自帶的退出登錄過濾器,同時並攜帶一個redirect_uri參數,讓認證服務器退出登錄之后,知道跳轉到客戶端應用去。否則認證服務器默認的退出邏輯是,退出后跳轉到了認證服務器的首頁,由於沒有做首頁,所以返回了一個404,我們重寫了退出登錄類org.springframework.security.web.authentication.ui.DefaultLogoutPageGeneratingFilter ,讓退出登錄表單自動提交,實現了退出成功handler,重定向到了客戶端應用退出時攜帶過來的redirect_uri。
本篇代碼github : https://github.com/lhy1234/springcloud-security/tree/chapt-5-3-sso-session 如果幫到了你,給個小星星吧
歡迎關注個人公眾號一起交流學習: