Spring Cloud微服務安全實戰_5-3_基於session的SSO


上一篇將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 如果幫到了你,給個小星星吧

 

 

歡迎關注個人公眾號一起交流學習:

 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM