目前的架構
到目前為止,已經實現了在前后端分離的架構,微服務的環境下,一個完整的業務邏輯,包括用戶的登錄,獲取令牌,拿着令牌調服務,退出。(流程如下)
目前的架構是基於oauth2的password模式的,是存在一些問題的:
1,用戶輸入用戶名密碼,是提交給了前端服務器的,前端服務器的開發人員,都會接觸到用戶的用戶名密碼,存在安全問題
2,每個客戶端應用都要處理登錄邏輯,一旦登錄邏輯有變化,所有的客戶端都要改,重新部署,有耦合。
希望的場景
用戶需要登錄的時候,前端服務器直接將用戶引導到 認證服務器 上,認證的動作是在認證服務器上完成的。
這樣做的好處是:
1,客戶端應用完全接觸不到用戶的用戶名密碼,保證了安全性。
2,客戶端應用沒有登錄邏輯,登錄邏輯都在認證服務器上,如果登錄邏輯有變化,直接修改認證服務器一處,減少了耦合。
將password授權模式改為授權碼模式
+++++++++++++++++++++++ 番外, 講一些 OAuth協議的4種授權模型++++++++++++++++++++++++
1,密碼模式
也是之前一直在用的授權模式,適用場景:手機app ,這個客戶端應用是你完全可以信任的,你的app就是自己公司開發的。但是這個模式並不適合在web場景下用,在web下,用戶名密碼並不是直接填給自己寫的應用的,而是填在瀏覽器呈現的一個頁面上的,這個瀏覽器是客戶端應用的一個代理,瀏覽器是沒法保證安全性的。(還是不是很理解哪里不安全)
2,授權碼模式
1,用戶訪問客戶端應用
2,引導用戶到認證服務器進行登錄(此步驟需要攜帶客戶端應用的clientId,可以是html直接轉發認證服務器),用戶輸入用戶名、密碼
3,認證成功后,認證服務器向客戶端應用發一個授權碼code
4,客戶端應用拿着授權碼code,和clientId,clientSecret,去換取access_token
5,返回access_token給客戶端應用
這種場景下,用戶名、密碼、客戶端應用信息,都沒有直接暴露在瀏覽器,是web下是最安全的。
3,隱式授權/簡化授權
授權碼模式的簡化,用戶認證成功后,直接將token返回給瀏覽器。因為某些應用沒有前端服務器,只有一堆靜態的html(很少見),這種模式,一般不用。
4,客戶端證書
客戶端應用直接發 clientId、clientSecret給認證服務器,發的令牌是針對客戶端應用的,不是針對用戶的。跟沒授權一樣,令牌不能識別用戶身份。
+++++++++++++++++++++++++++++++++我是分割線結束+++++++++++++++++++++++++++++++++++++++++
改造nb-admin代碼為授權碼模式
1,刪掉登錄頁面,登錄是在認證服務器上完成的,所以這里刪除nb-admin的登錄頁面
2,進入 index.html,點擊登錄,將用戶引導到認證服務器提供的登錄頁去。
index.html:
<!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <head> <meta charset="UTF-8"> <title>Index</title> </head> <body> <h1>welcome to 系統1</h1> <div id="loginTip"></div> <p><button onclick="getOrderInfo()">獲取訂單信息</button></p> <table> <tr><td>order id</td><td><input id="orderId" /></td></tr> <tr><td>order product id</td><td><input id="productId" /></td></tr> </table> </body> <script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script> <script> function getOrderInfo(){ $.get("api/order/orders/1",function(data){ $("#orderId").val(data.id); $("#productId").val(data.productId); }); } $(document).ready(function(){ $.get("/me",function(data,status){ if(data){ //已登錄 var htm = "已登錄,<a href='/logout'>退出</a>"; $("#loginTip").html(htm); }else{ //未登錄 var href = "<a href='/toAuthLogin'>未登錄,去登錄</a>"; $("#loginTip").append(href); } }); }); </script> </html>
點擊登錄按鈕,轉發到認證服務器,這個可以放在前端做,因為只有clientId信息,沒有安全問題。
/** * 重定向到認證服務器的登錄頁 * @param response * @throws IOException */ @GetMapping("/toAuthLogin") public void toAuthLogin(HttpServletResponse response) throws IOException{ String redirectUrl = "http://auth.nb.com:9090/oauth/authorize?" +"client_id=admin&" +"redirect_uri=http://admin.nb.com:8080/oauth/callback&" +"response_type=code&" +"state=/index"; //state參數傳過去啥傳回來啥,一般記錄跳轉之前的路徑 response.sendRedirect(redirectUrl); }
授權回調:
/** * 授權回調 * @param code 授權碼 * @param state 自定義參數 * @param session */ @GetMapping("/oauth/callback") public String callback(@RequestParam String code,String state ,HttpSession session){ log.info("code is {}, state is {}",code,state); //認證服務器驗token地址 /oauth/check_token 是 spring .security.oauth2的驗token端點 String oauthServiceUrl = "http://gateway.nb.com:9070/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);//不是json請求 //網關的appId,appSecret,需要在數據庫oauth_client_details注冊 headers.setBasicAuth("admin","123456"); MultiValueMap<String,String> params = new LinkedMultiValueMap<>(); params.add("code",code);//授權碼 params.add("grant_type","authorization_code");//授權類型-授權碼模式 //認證服務器會對比數據庫客戶端信息的的redirect_uri和這里的是不是一致,不一致就報錯 params.add("redirect_uri","http://admin.nb.com:8080/oauth/callback"); HttpEntity<MultiValueMap<String,String>> entity = new HttpEntity<>(params,headers); ResponseEntity<AccessToken> response = restTemplate.exchange(oauthServiceUrl, HttpMethod.POST, entity, AccessToken.class); session.setAttribute("token",response.getBody()); return "redirect:/index"; }
給admin應用添加授權碼模式:
啟動admin服務,認證服務,訂單服務,網關,訪問admin
點擊去登錄,跳轉到了認證服務器,這是自帶的頁面
可以重寫認證服務器的安全配置方法,自定義登錄頁
到目前為止已將OAuth2的授權模式由 【密碼模式】改造成了 【 授權碼】 授權流程,完成改造的同時,也實現了SSO,微服務環境前后端分離模式下的單點登錄。
目前的單點登錄實際上是基於session的,目前有兩個session,一個是認證服務器的session,一個是客戶端應用的session,用戶在客戶端應用上點擊登錄按鈕,跳轉到認證服務器上做登錄,登錄成功后認證服務器上存了該用戶的session信息,客戶端應用拿到認證服務器返回的access_token后,將token存到自己的session,這樣就認為該用戶已登錄:
但是還存在很多問題,比如,現在的退出登錄:
@GetMapping("/logout") public String logout(HttpSession session){ session.invalidate(); return "index"; }
只是將nb-admin客戶端應用的session失效掉了,里面沒有token了,但是認證服務器上的session並沒有失效,重啟nb-admin,客戶端應用的session是失效掉了,重新訪問 http://admin.nb.com:8080/index ,出現未登錄:
點擊登錄,跳轉到認證服務器,由於認證服務器session上還有客戶端該用戶的登錄信息,所以不會出現登錄表單,用戶的感覺是沒有再做登錄就已經登錄,給人的感覺是,我已經點了退出登錄,但是點擊登錄按鈕后,沒讓我再輸用戶名密碼,感覺是“沒有退出去”。其實F12可以看到,已經請求了認證服務器,只不過是認證服務器的session沒有失效,直接就登錄成功了而已 :
1,請求認證服務器登錄頁 http://auth.nb.com:9090/oauth/authorize?client_id=admin&redirect_uri=http://admin.nb.com:8080/oauth/callback&response_type=code&state=/index
2,登錄成功后的回調 http://admin.nb.com:8080/oauth/callback?code=IMbu7Y&state=/index
還有目前認證服務器的session是存在內存的,生產環境下認證服務器要保證高可用,是一個集群,要保證session共享,所以直接用session是不行的,接下來來處理這些問題。
本節代碼github:https://github.com/lhy1234/springcloud-security/tree/chapt-5-2-authcode,如果幫助到了你,給個小星星吧
歡迎關注個人公眾號一起交流學習: