1. 程式人生 > >Vue.js——使用$.ajax和vue-resource實現OAuth的註冊、登入、登出和API呼叫【6】

Vue.js——使用$.ajax和vue-resource實現OAuth的註冊、登入、登出和API呼叫【6】

概述

上一篇我們介紹瞭如何使用vue resource處理HTTP請求,結合服務端的REST API,就能夠很容易地構建一個增刪查改應用。
這個應用始終遺留了一個問題,Web App在訪問REST API時,沒有經過任何認證,這使得服務端的REST API是不安全的,只要有人知道api地址,就可以呼叫API對服務端的資源進行修改和刪除。
今天我們就來探討一下如何結合Web API來限制資源的訪問。

本文的主要內容如下:

  • 介紹傳統的Web應用和基於REST服務的Web應用
  • 介紹OAuth認證流程和密碼模式
  • 建立一個基於ASP.NET Identity的Web API應用程式
  • 基於$.ajax實現OAuth的註冊、登入、登出和API呼叫
  • 基於vue-resource實現OAuth的註冊、登入、登出和API呼叫

本文的最終示例是結合上一篇的CURD,本文的登入、註冊、登出和API呼叫功能實現的。

35

本文9個示例的原始碼已放到GitHub,如果您覺得本篇內容不錯,請點個贊,或在GitHub上加個星星!

基於$.ajax的示例如下:

基於vue-resource的示例如下:

OAuth介紹

傳統的Web應用

在傳統的Web應用程式中,前後端是放在一個站點下的,我們可以通過會話(Session)來儲存使用者的資訊。
例如:一個簡單的ASP.NET MVC應用程式,使用者登入成功後,我們將使用者的ID記錄在Session中,假設為Session["UserID"]。
前端傳送ajax請求時,如果這個請求要求已登入的使用者才能訪問,我們只需在後臺Controller中驗證Session["UserID"]是否為空,就可以判斷使用者是否已經登入了。
這也是傳統的Web應用能夠逃避HTTP面向無連線的方法。

基於REST服務的Web應用

當今很多應用,客戶端和服務端是分離的,服務端是基於REST風格構建的一套Service,客戶端是第三方的Web應用,客戶端通過跨域的ajax請求獲取REST服務的資源。
然而REST Service通常是被設計為無狀態的(Stateless),這意味著我們不能依賴於Session來儲存使用者資訊,也不能使用Session["UserID"]這種方式確定使用者身份。

解決這個問題的方法是什麼呢?常規的方法是使用OAuth 2.0。
對於使用者相關的OpenAPI,為了保護使用者資料的安全和隱私,第三方Web應用訪問使用者資料前都需要顯式的向用戶徵求授權。
相比於OAuth 1.0,OAuth 2.0的認證流程更加簡單。

專用名詞介紹

在瞭解OAuth 2.0之前,我們先了解幾個名詞:

  1. Resource:資源,和REST中的資源概念一致,有些資源是訪問受保護的
  2. Resource Server:存放資源的伺服器
  3. Resource Owner:資源所有者,本文中又稱為使用者(user)
  4. User Agent:使用者代理,即瀏覽器
  5. Client: 訪問資源的客戶端,也就是應用程式
  6. Authorization Server:認證伺服器,用於給Client提供訪問令牌的伺服器
  7. Access Token:訪問資源的令牌,由Authorization Server器授予,Client訪問Resource時,需提供Access Token
  8. Bearer Token:Bearer Token是Access Token的一種,另一種是Mac Token。
    Bearer Token的使用格式為:Bearer XXXXXXXX

有時候認證伺服器和資源伺服器可以是一臺伺服器,本文中的Web API示例正是這種運用場景。

OAuth認證流程

在知道這幾個詞以後,我們用這幾個名詞來編個故事。

簡化版本

這個故事的簡化版本是:使用者(Resource Owner)訪問資源(Resource)。

image

具體版本

簡化版的故事只有一個結果,下面是這個故事的具體版本:

  1. 使用者通過瀏覽器開啟客戶端後,客戶端要求使用者給予授權。
    客戶端可以直接將授權請求發給使用者(如圖所示),或者傳送給一箇中間媒介,比如認證伺服器。
  2. 使用者同意給予客戶端授權,客戶端收到使用者的授權。
    授權模式(Grant Type)取決於客戶端使用的模式,以及認證伺服器所支援的模式。
  3. 客戶端提供身份資訊,然後向認證伺服器傳送請求,申請訪問令牌
  4. 認證伺服器驗證客戶端提供的身份資訊,如果驗證通過,則向客戶端發放令牌
  5. 客戶端使用訪問令牌,向資源伺服器請求受保護的資源
  6. 資源伺服器驗證訪問令牌,如果有效,則向客戶端開放資源

image

客戶端的授權模式

客戶端必須得到使用者的授權(authorization grant),才能獲得令牌(access token)。

OAuth 2.0定義了四種授權方式:

  • 授權碼模式(authorization code)
  • 簡化模式(implicit)
  • 密碼模式(resource owner password credentials)
  • 客戶端模式(client credentials)

本文的示例是基於密碼模式的,我就只簡單介紹這種模式,其他3我就不介紹了,大家有興趣可以看阮大的文章:

密碼模式

密碼模式(Resource Owner Password Credentials Grant)中,使用者向客戶端提供自己的使用者名稱和密碼。客戶端使用這些資訊,向服務端申請授權。

在這種模式中,使用者必須把自己的密碼給客戶端,但是客戶端不得儲存密碼。這通常用在使用者對客戶端高度信任的情況下,比如客戶端是作業系統的一部分,或者由一個著名公司出品。

image

密碼嘛事的執行步驟如下:

(A)使用者向客戶端提供使用者名稱和密碼。

(B)客戶端將使用者名稱和密碼發給認證伺服器,向後者請求令牌。

(C)認證伺服器確認無誤後,向客戶端提供訪問令牌。

(B)步驟中,客戶端發出的HTTP請求,包含以下引數:

  • grant_type:表示授權型別,此處的值固定為"password",必選項。
  • username:表示使用者名稱,必選項。
  • password:表示使用者的密碼,必選項。
  • scope:表示許可權範圍,可選項。

注意:在後面的客戶端示例中,除了提供username和password,grant_type也是必須指定為"password",否則無法獲取服務端的授權。

服務端環境準備

如果您是前端開發人員,並且未接觸過ASP.NET Web API,可以跳過此段落。

image

Authentication選擇Individual User Accounts

image

建立這個Web API工程時,VS會自動引入Owin和AspNet.Identity相關的庫。

image

修改ValuesController,除了IEnumerable<string> Get()操作外,其他操作都刪除,併為該操作應用[Authorize]特性,這表示客戶端必須通過身份驗證後才能呼叫該操作。

public class ValuesController : ApiController
{
    // GET: api/Values
    [Authorize]
    public IEnumerable<string> Get()
    {
        return new string[] { "value1", "value2" };
    }
}

新增Model, Controller

image

image

image

初始化資料庫

image

執行以下3個命令

image

image

執行以下SQL語句:

顯示程式碼

CustomersController類有5個Action,除了2個GET請求外,其他3個請求分別是POST, PUT和DELETE。
為這3個請求新增[Authorize]特性,這3個請求必須通過身份驗證才能訪問。

隱藏程式碼
public class CustomersController : ApiController
{
    private ApplicationDbContext db = new ApplicationDbContext();

    // GET: api/Customers
    public IQueryable<Customer> GetCustomers()
    {
        return db.Customers;
    }

    // GET: api/Customers/5
    [ResponseType(typeof(Customer))]
    public async Task<IHttpActionResult> GetCustomer(int id)
    {
        Customer customer = await db.Customers.FindAsync(id);
        if (customer == null)
        {
            return NotFound();
        }

        return Ok(customer);
    }

    // PUT: api/Customers/5
    [Authorize]
    [ResponseType(typeof(void))]
    public async Task<IHttpActionResult> PutCustomer(int id, Customer customer)
    {
      // ...
    }

    // POST: api/Customers
    [Authorize]
    [ResponseType(typeof(Customer))]
    public async Task<IHttpActionResult> PostCustomer(Customer customer)
    {
        // ...
    }

    // DELETE: api/Customers/5
    [ResponseType(typeof(Customer))]
    [Authorize]
    public async Task<IHttpActionResult> DeleteCustomer(int id)
    {
    	// ...
    }
}

讓Web API以CamelCase輸出JSON

在Global.asax檔案中新增以下幾行程式碼:

var formatters = GlobalConfiguration.Configuration.Formatters;
var jsonFormatter = formatters.JsonFormatter;
var settings = jsonFormatter.SerializerSettings;
settings.Formatting = Formatting.Indented;
settings.ContractResolver = new CamelCasePropertyNamesContractResolver();

啟用CORS

在Nuget Package Manager Console輸入以下命令:

Install-Package Microsoft.AspNet.WebApi.Cors

在WebApiConfig中啟用CORS:

public static class WebApiConfig
{
    public static void Register(HttpConfiguration config)
    {
        var cors = new EnableCorsAttribute("*", "*", "*");
        config.EnableCors(cors);

        // ...

    }
}

類說明

在執行上述步驟時,VS已經幫我們生成好了一些類

image

IdentityModels.cs:包含ApplicationDbContext類和ApplicationUser類,無需再建立DbContext類

public class ApplicationUser : IdentityUser
{
    // ...
}

public class ApplicationDbContext : IdentityDbContext<ApplicationUser>
{
    // ...
}

Startup.Auth.cs:用於配置OAuth的一些屬性。

public partial class Startup
{
    public static OAuthAuthorizationServerOptions OAuthOptions { get; private set; }

    public static string PublicClientId { get; private set; }

    // For more information on configuring authentication, please visit http://go.microsoft.com/fwlink/?LinkId=301864
    public void ConfigureAuth(IAppBuilder app)
    {
        // ..

        // Configure the application for OAuth based flow
        PublicClientId = "self";
        OAuthOptions = new OAuthAuthorizationServerOptions
        {
            TokenEndpointPath = new PathString("/Token"),
            Provider = new ApplicationOAuthProvider(PublicClientId),
            AuthorizeEndpointPath = new PathString("/api/Account/ExternalLogin"),
            AccessTokenExpireTimeSpan = TimeSpan.FromDays(14),
            // In production mode set AllowInsecureHttp = false
            AllowInsecureHttp = true
        };

        // Enable the application to use bearer tokens to authenticate users
        app.UseOAuthBearerTokens(OAuthOptions);

        // ..
    }
}

這些OAuth配置項,我們只用關注其中的兩項:

  • TokenEndpointPath:表示客戶端傳送驗證請求的地址,例如:Web API的站點為www.example.com,驗證請求的地址則為www.example.com/token
  • UseOAuthBearerTokens:使用Bearer型別的token_type(令牌型別)。

ApplicationOAuthProvider.cs:預設的OAuthProvider實現,GrantResourceOwnerCredentials方法用於驗證使用者身份資訊,並返回access_token(訪問令牌)。

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
   // ...     
}

通俗地講,客戶端輸入使用者名稱、密碼,點選登入後,會發起請求到www.example.com/token
token這個請求在服務端執行的驗證方法是什麼呢?正是GrantResourceOwnerCredentials方法。

客戶端發起驗證請求時,必然是跨域的,token這個請求不屬於任何ApiController的Action,而在WebApiConfig.cs中啟用全域性的CORS,只對ApiController有效,對token請求是不起作用的。
所以還需要在GrantResourceOwnerCredentials方法中新增一行程式碼:

public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
    context.Response.Headers.Add("Access-Control-Allow-Origin", new []{"*"});
    // ...
}

IdentityConfig.cs:配置使用者名稱和密碼的複雜度,主要用於使用者註冊時。例如:不允許使用者名稱為純字母和數字的組合,密碼長度至少為6位…。

隱藏程式碼
public static ApplicationUserManager Create(IdentityFactoryOptions<ApplicationUserManager> options, IOwinContext context)
{
    var manager = new ApplicationUserManager(new UserStore<ApplicationUser>(context.Get<ApplicationDbContext>()));
    // Configure validation logic for usernames
    manager.UserValidator = new UserValidator<ApplicationUser>(manager)
    {
        AllowOnlyAlphanumericUserNames = false,
        RequireUniqueEmail = true
    };
    // Configure validation logic for passwords
    manager.PasswordValidator = new PasswordValidator
    {
        RequiredLength = 6,
        RequireNonLetterOrDigit = true,
        RequireDigit = true,
        RequireLowercase = true,
        RequireUppercase = true,
    };
   	// ...
    return manager;
}

使用Postman測試GET和POST請求

測試GET請求

image

GET請求測試成功,可以獲取到JSON資料。

測試POST請求

image

POST請求測試不通過,提示:驗證不通過,請求被拒絕。

基於$.ajax實現註冊、登入、登出和API呼叫

服務端的環境已經準備好了,現在我們就逐個實現使用者註冊、登入,以及API呼叫功能吧。

註冊

頁面的HTML程式碼如下:

<div id="app">
    <div class="container">
        <span id="message">{{ msg }}</span>
    </div>
    <div class="container">
            <div class="form-group">
                <label>電子郵箱</label>
                <input type="text" v-model="registerModel.email" />
            </div>
            <div class="form-group">
                <label>密碼</label>
                <input type="text" v-model="registerModel.password" />
            </div>

            <div class="form-group">
                <label>確認密碼</label>
                <input type="text" v-model="registerModel.confirmPassword" />
            </div>

            <div class="form-group">
                <label></label>
                <button @click="register">註冊</button>
            </div>
    </div>
</div>

建立Vue例項,然後基於$.ajax傳送使用者註冊請求:

var demo = new Vue({
    el: '#app',
    data: {
        registerUrl: 'http://localhost:10648/api/Account/Register',
        registerModel: {
            email: '',
            password: '',
            confirmPassword: ''
        },
        msg: ''
    },
    methods: {
        register: function() {
            var vm = this
            vm.msg = ''
            
            $.ajax({
                url: vm.registerUrl,
                type: 'POST',
                dataType: 'json',
                data: vm.registerModel,
                success: function() {
                    vm.msg = '註冊成功!'
                },
                error: vm.requestError
            })
        },
        requestError: function(xhr, errorType, error) {
            this.msg = xhr.responseText
        }
    }
})

32

登入和登出

登入的HTML程式碼:

<div id="app">
    <div class="container text-center">
        <span id="message">{{ msg }}</span>
    </div>
    <div class="container">
        <div class="account-info">
            <span v-if="userName">{{ userName }} | <a href="#" @click="logout">登出</a></span>
        </div>
    </div>
    <div class="container">
            <div class="form-group">
                <label>電子郵箱</label>
                <input type="text" v-model="loginModel.username" />
            </div>
            <div class="form-group">
                <label>密碼</label>
                <input type="text" v-model="loginModel.password" />
            </div>
            <div class="form-group">
                <label></label>
                <button @click="login">登入</button>
            </div>
    </div>
</div>

建立Vue例項,然後基於$.ajax傳送使用者登入請求:

var demo = new Vue({
    el: '#app',
    data: {
        loginUrl: 'http://localhost:10648/token',
        logoutUrl: 'http://localhost:10648/api/Account/Logout',
        loginModel: {
            username: '',
            password: '',
            grant_type: 'password'
        },
        msg: '',
        userName: ''
    },

    ready: function() {
        this.userName = sessionStorage.getItem('userName')
    },
    methods: {
        login: function() {
            var vm = this
                vm.msg = ''
                vm.result = ''
            
            $.ajax({
                url: vm.loginUrl,
                type: 'POST',
                dataType: 'json',
                data: vm.loginModel,
                success: function(data) {
                    vm.msg = '登入成功!'
                    vm.userName = data.userName
                    sessionStorage.setItem('accessToken', data.access_token)
                    sessionStorage.setItem('userName', vm.userName)
                },
                error: vm.requestError
            })
        },
        logout: function() {
            var vm = this
                vm.msg = ''

            $.ajax({
                url: vm.logoutUrl,
                type: 'POST',
                dataType: 'json',
                success: function(data) {
                    
                    vm.msg = '登出成功!'
                    vm.userName = ''
                    vm.loginModel.userName = ''
                    vm.loginModel.password = ''
                    
                    sessionStorage.removeItem('userName')
                    sessionStorage.removeItem('accessToken')
                },
                error: vm.requestError
            })
        },
        requestError: function(xhr, errorType, error) {
            this.msg = xhr.responseText
        }
    }
})

33

在試驗這個示例時,把Fiddler也開啟,我們一共進行了3次操作:

  1. 第1次操作:輸入了錯誤的密碼,服務端響應400的狀態碼,並提示了錯誤資訊。
  2. 第2次操作:輸入了正確的使用者名稱和密碼,服務端響應200的狀態碼
  3. 第3次操作:點選右上角的登出連結

image

注意第2次操作,在Fiddler中檢視服務端返回的內容:

image

服務端返回了access_token, expires_in, token_type,userName等資訊,在客戶端可以用sessionStoragelocalStorage儲存access_token

呼叫API

取到了access_token後,我們就可以基於access_token去訪問服務端受保護的資源了。
這裡我們要訪問的資源是/api/Values,它來源於ValuesController的Get操作。

基於註冊畫面,新增一段HTML程式碼:

<div class="container text-center">
    <div>
        <button @click="callApi">呼叫API</button>
    </div>
    <div class="result">
        API呼叫結果:{{ result | json }}
    </div>
</div>

在Vue例項中新增一個callApi方法:

callApi: function() {
    var vm = this
        vm.msg = ''
        vm.result = ''