Spring Cloud微服務安全實戰_5-1_單點登錄基本架構


 

 

 

下面是常見的前后端分離的架構,准確的說,這是一種半分離的架構,這種架構存在一些問題:

1,SEO,搜索引擎只能識別靜態的HTML資源,爬HTML的DOM樹,它是沒法識別js動態渲染出來的內容的,所以如果是這種架構,SEO會有問題

2,頁面渲染是在瀏覽器完成的,如果業務復雜,要數據量大,可能會對瀏覽器造成壓力,可能會卡頓。

 

 鑒於以上前后端分離的架構存在的問題,我們要搭的一個前后端分離的架構是下面的架構:

 

主要區別是把Nginx換成了nodeJS,nodejs可以實現nginx的所有的功能。

而且還提供了其他的功能,比如服務端的頁面渲染,瀏覽器在請求一個html資源的時候,nodejs可以發api請求,在nodejs上把頁面渲染好,然后把渲染好的頁面給瀏覽器,此時做SEO的時候,搜索引擎爬到的是一個完整的頁面。如果某些頁面不需要SEO,仍然可以在頁面直接發api,在瀏覽器渲染頁面,這兩種都支持。

 

 

本實驗用springboot代替nodejs,頁面使用html js。之前的系列文章里,都是使用的postman作為客戶端應用的,從今往后,就用實際的前端應用替換postman了。

整體就是下面這種架構:

 下面開始敲代碼

 1,新建一個應用nb-admin

 

 

效果:

未登錄時候,顯示

 

 

 點擊登錄,輸入用戶名密碼

 

 (認證服務器寫死的,密碼是123456即可)

 

 登錄后:

主要代碼:在  AdminController ,處理登錄請求,調用網關,獲取token ,獲取后放在了 前端服務器的session里

@ResponseBody
    @PostMapping("/login")
    public void login(@RequestParam String username,@RequestParam String password/*@RequestBody Credential credential*/, HttpSession session){

        //認證服務器驗token地址 /oauth/check_token 是  spring .security.oauth2的驗token端點
        String oauthServiceUrl = "http://localhost:9070/token/oauth/token";

        HttpHeaders headers = new HttpHeaders();//org.springframework.http.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("username",username);
        params.add("password",password);
        params.add("grant_type","password");
        params.add("scope","read write");

        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());
        log.info("token info : {}",response.getBody().toString());
    }

在 index.html 里,進頁面就發一個/me 請求,如果能從session獲取到token信息(先這么干),說明登錄成功:

@GetMapping("/me")
    @ResponseBody
    public AccessToken me(HttpSession session){
        AccessToken accessToken = (AccessToken) session.getAttribute("token");
        return accessToken;
    }

index.html

 

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Index</title>
</head>
<body>
    <h1>歡迎來到sso子系統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='/loginPage'>未登錄,去登錄</a>";
                $("#loginTip").append(href);
            }
        });
    });

</script>
</html>

 

 

 

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>login</title>
</head>
<body>

    <center>
        <h3>登錄</h3>
        <table border="1">
            <tr>
                <td>用戶名</td>
                <td><input id="username" name="username" value="lhy"/></td>
            </tr>
            <tr>
                <td>密碼</td>
                <td><input id="password" name="password" value="123456"/></td>
            </tr>
            <tr>
                <td colspan="2" align="right"><button id="submitBtn" onclick="login()">登錄</button></td>
            </tr>
        </table>
    </center>
</body>
<script src="http://libs.baidu.com/jquery/2.0.0/jquery.min.js"></script>
<script>
    function login() {
        var username = $("#username").val();
        var password = $("#password").val();
        //alert(username+", " +password)

        $.ajax({
            type: "POST",
            url: "/login",
            data: {"username":username, "password":password},
            success: function(msg){
                location.href = "/index";
            },
            error:function(msg){
                alert("登錄失敗!!")
            }
        });
    }

</script>
</html>

 

 請求轉發

在nb-admin上,index.html,加上獲取訂單信息,點擊獲取訂單按鈕,發一個請求到前端服務nb-admin,nb-admin配上zuul網關,讓它把這個請求轉發到網關去,由網關再去請求訂單服務,獲取訂單信息。

在nb-admin項目加入zuul依賴:

<dependency>
     <groupId>org.springframework.cloud</groupId>
      <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

啟動類加上注解:

@EnableZuulProxy

配置zuul路由,以 /api 開頭的請求,轉發到網關

zuul:
  routes: #路由的配置是個Map,可以配置多個
    api:  #api開頭的請求,都轉發到網關9070
      url:  http://gateway.nb.com:9070
  sensitive-headers:  null #設置敏感頭設置為空,Authorization等請求頭的請求,都往后轉發

server:
  port: 8080
spring:
  application:
    name: admin

注:幾個項目都配置了host,以方便區分cookie等

127.0.0.1 gateway.nb.com    ----網關

127.0.0.1 admin.nb.com  ----前端服務

127.0.0.1 auth.nb.com    ----認證服務

127.0.0.1 order.nb.com  ----訂單服務

網關上,獲取訂單信息,是需要token的,所以在nb-admin上,需要從session中獲取到token,加在請求頭里,再發往網關。新建一個zuulFilter,統一在請求頭加token。

SessionTokenFilter
package com.nb.security.admin;

import com.netflix.zuul.ZuulFilter;
import com.netflix.zuul.context.RequestContext;
import com.netflix.zuul.exception.ZuulException;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/**
 * 從session獲取token,統一加到請求頭中去
 */
@Component
public class SessionTokenFilter extends ZuulFilter {

    @Override
    public String filterType() {
        return "pre";
    }

    @Override
    public int filterOrder() {
        return 1;
    }

    @Override
    public boolean shouldFilter() {
        return true;
    }

    @Override
    public Object run() throws ZuulException {
        RequestContext requestContext = RequestContext.getCurrentContext();
        HttpServletRequest request = requestContext.getRequest();
        AccessToken accessToken = (AccessToken)request.getSession().getAttribute("token");
        if(accessToken != null){
            requestContext.addZuulRequestHeader("Authorization","Bearer "+accessToken.getAccess_token());
        }
        return null;
    }
}

AccessToken

import lombok.Data;

import java.util.Date;

/**
 * access_token
 * Created by: 李浩洋 on 2020-01-02
 **/
@Data
public class AccessToken {

    private String access_token;

    private String token_type;

    private Date expires_in;

    private String scope;
}

現在效果:

 

 

 登錄:

 

 

 登錄后

 

 

點擊獲取訂單信息,會發一個  api/order/orders/1 請求,由於nb-admin里配置了zuul路由,api開頭的請求都會轉發到網關,所以這里實際訪問了網關,網關又轉發到了order-api,最終返回orderInfo

function getOrderInfo(){
        $.get("api/order/orders/1",function(data){
            $("#orderId").val(data.id);
            $("#productId").val(data.productId);
        });
    }

退出登錄

退出登錄很簡單,直接發一個請求,將nb-admin里的session失效,就行了
@GetMapping("/logout")
    public String logout(HttpSession session){
        session.invalidate();
        return "index";
    }

 

目前已經用 springboot實現了一個前端服務器,實現了oauth協議中 password模式的登錄,請求轉發,退出。目前還存在不少問題,下節講解問題並解決。

(其實我認為將這個就當做某個系統的后端服務也沒啥問題,類似於你去微信申請客戶端應用一樣,這里的nb-admin就是我們自己的公眾號項目)

 

代碼github:https://github.com/lhy1234/springcloud-security/tree/chapt-5-1-ui

如果對你有一點,就給個小星星吧

 

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

 

 


免責聲明!

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



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