1、前后端分離架構
1.1、前后端半分離


工作流程大致是,訪問html頁面,加載css、js等靜態資源,加載到瀏覽器時,js會發送一些ajax請求由nginx轉發到后端服務器,后端服務器給出相應的json數據,頁面解析Json數據,通過Dom操作渲染頁面。
在這樣的架構下,存在一些明顯的弊端:
SEO( 搜索引擎優化)非常不方便,由於搜索引擎的爬蟲無法爬下JS異步渲染的數據,導致這樣的頁面,SEO會存在一定的問題;
在Json返回的數據量比較大的情況下,渲染的十分緩慢,會出現頁面卡頓的情況;
1.2、前后端分離


將nginx替換成了NodeJS,瀏覽器請求NodeJS,NodeJS在發http請求去服務器獲取json數據,NodeJS收到json后再渲染出HTML頁面,NodeJS直接將HTML頁面flush到瀏覽器,這樣,瀏覽器得到的就是普通的HTML頁面,而不用再發Ajax去請求服務器了。
在這里,前端不僅包括瀏覽器,還包括一個NodeJS服務器,后端只負責數據的處理。
2、前后端分離的OAuth2認證架構

在之前我們實現的OAuth2認證架構中,客戶端應用,其實是使用的一些http請求工具,如Restlet Client、Postman等。我們將架構改造為如下,客戶端應用,其實應該由瀏覽器和前端服務器組成。(在這里前端服務器,我們使用SpringBoot模擬實現,在實際工作中,應該是由前端人員使用NodeJS來實現)
3、項目改造實現認證流程
由上圖可知,我們需要改造的有以下兩點:第一步請求令牌時,將http請求工具更換為前端服務器來請求令牌;第五步由前端服務器攜帶令牌請求服務。
3.1、搭建前端服務器(由SpringBoot模擬實現)
3.1.1、項目結構

3.1.2、頁面index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>cfq security</title> </head> <body> <h1>Study Security</h1> <div id="login-success"> <h1>登陸成功!</h1> <p> <button onclick="getOrder()">獲取訂單信息</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> </hr> <p> <button onclick="logout()">退出</button> </p> </div> <div id="goto-login"> <table> <tr> <td>用戶名</td> <td><input id="username" name="username" value=""/></td> </tr> <tr> <td>密碼</td> <td><input id="password" name="password" value=""/></td> </tr> <tr> <td colspan="2" align="right"> <button id="submit" onclick="login()">登錄</button> </td> </tr> </table> </div> <script src="https://apps.bdimg.com/libs/jquery/2.1.4/jquery.min.js"></script> <script> //判斷用戶是否登陸 $(function () { $.get("/me", function (data) { if (data) { //已登錄 $("#login-success").show(); $("#goto-login").hide(); } else { //未登錄 $("#login-success").hide(); $("#goto-login").show(); } }); }); //登陸方法 function login() { var username = $("#username").val(); var password = $("#password").val(); $.ajax({ type: "POST", url: "/login", contentType: "application/json;charset=utf-8", data: JSON.stringify({"username": username, "password": password}), success: function (msg) { location.href = "/"; }, error: function (msg) { alert("登錄失敗!!") } }); } //獲取訂單信息,通過/api轉發到網關,通過/order轉發到order微服務 function getOrder() { $.get("/api/order/orders/1", function (data) { $("#orderId").val(data.id); $("#productId").val(data.productId); }); } //退出 function logout() { $.get("/logout",function(){}); location.href = "/"; } </script> </body> </html>
3.1.3、提供登陸方法,使用password模式像認證服務器獲取令牌,將原來由http客戶端的動作,放到代碼中
/** * 模擬前端服務器 * * @author caofanqi * @date 2020/2/5 16:34 */ @Slf4j @RestController @EnableZuulProxy @SpringBootApplication public class WebAppApplication { private RestTemplate restTemplate = new RestTemplate(); public static void main(String[] args) { SpringApplication.run(WebAppApplication.class, args); } /** * 獲取當前認證的token信息 */ @GetMapping("/me") public TokenInfoDTO me(HttpServletRequest request){ return (TokenInfoDTO)request.getSession().getAttribute("token"); } /** * 登陸方法 * 向認證服務器獲取令牌,將token信息放到session中 */ @PostMapping("/login") public void login(@RequestBody UserDTO userDTO, HttpServletRequest request) { String oauthTokenUrl = "http://gateway.caofanqi.cn:9010/token/oauth/token"; HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth("webApp", "123456"); MultiValueMap<String, String> params = new LinkedMultiValueMap<>(); params.set("username", userDTO.getUsername()); params.set("password", userDTO.getPassword()); params.set("grant_type", "password"); params.set("scope", "read write"); HttpEntity<MultiValueMap<String, String>> httpEntity = new HttpEntity<>(params, headers); ResponseEntity<TokenInfoDTO> response = restTemplate.exchange(oauthTokenUrl, HttpMethod.POST, httpEntity, TokenInfoDTO.class); request.getSession().setAttribute("token", response.getBody()); log.info("tokenInfo : {}",response.getBody()); } /** * 退出 */ @RequestMapping("/logout") public void logout(HttpServletRequest request){ request.getSession().invalidate(); } }
3.1.4、通過zuul的轉發功能,我們再每個請求頭中添加token令牌
/** * 將session中的token取出放到請求頭中 * * @author caofanqi * @date 2020/2/6 0:34 */ @Component public class SessionTokenFilter extends ZuulFilter { @Override public String filterType() { return "pre"; } @Override public int filterOrder() { return 0; } @Override public boolean shouldFilter() { return true; } @Override public Object run() throws ZuulException { RequestContext requestContext = RequestContext.getCurrentContext(); HttpServletRequest request = requestContext.getRequest(); TokenInfoDTO token = (TokenInfoDTO) request.getSession().getAttribute("token"); if (token != null) { requestContext.addZuulRequestHeader("Authorization","bearer " + token.getAccess_token()); } return null; } }
3.1.5、修改hosts文件,便於區別各服務

3.1.6、application.yml配置文件,將/api/**的請求都轉發到gateway網關服務
spring: application: name: webApp server: port: 9000 zuul: routes: api: path: /api/** url: http://gateway.caofanqi.cn:9010 sensitive-headers:
3.2、啟動各服務,測試
3.2.1、訪問http://web.caofanqi.cn:9000/,因為沒有登陸,所以顯示登陸界面

3.2.2、輸入zhangsan,123456,登陸成功

認證服務器,返回給webApp的token信息

3.2.3、點擊獲取訂單信息,可以獲得訂單,SessionTokenFilter會在請求頭中添加token,並且請求是以/api開頭的會轉發到網關,再由網關轉發到order服務。

3.2.4、點擊退出,又到了登陸頁面
項目源碼:https://github.com/caofanqi/study-security/tree/dev-web-password
