Ionic v3 使用 Token Based Authentication 實作Login頁面

  • 369
  • 0
  • 2018-09-07

Ionic v3 使用 Token Based Authentication 實作Login頁面

此篇文章是參照下面兩篇文章改寫的:

前端程式:

auth.ts (Ionic Providers 或叫Service也行)

import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Injectable } from '@angular/core';

@Injectable()
export class AuthProvider {
  readonly rootUrl = 'http://localhost:57086';
  constructor(public http: HttpClient) {}

  login(userName: string, password: string) {
    var data = "username=" + userName + "&password=" + password + "&grant_type=password";
    // Header指定'No-Auth': 'True' ,當攔截器(HttpInterceptor)看到這個,就跳過不會在Header動手腳了
    var reqHeader = new HttpHeaders({ 
    	'Content-Type': 'application/x-www-urlencoded', 'No-Auth': 'True' 
    });
    return this.http.post(this.rootUrl + '/token', data, { headers: reqHeader });
  }

  //Header沒有加上'No-Auth': 'True',Request送出前,攔截器(HttpInterceptor)會自動在Header加上access_token
  getEmail() {
    return this.http.get(this.rootUrl + '/api/account/GetUser');
  }
}

auth.interceptor.ts (HTTP攔截器,在Request送出前攔截下來,然後任你處置...)

HttpInterceptort參考

import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent } from "@angular/common/http";
import { Observable } from "rxjs/Observable";
import { Injectable } from "@angular/core";
import { Storage } from '@ionic/storage';

import { fromPromise } from 'rxjs/observable/fromPromise';
import { mergeMap } from 'rxjs/operators/mergeMap';

//處理Request header
@Injectable()
export class AuthInterceptor implements HttpInterceptor {

  constructor(public storage: Storage) { }

	//取得token
  getToken(): Promise<any> {
    return this.storage.get('userToken');
  }

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
  	//若headers有'No-Auth'='True',Request就原封不動送出
    if (req.headers.get('No-Auth') == "True")
      return next.handle(req.clone());

	//在Request header加上token
    return fromPromise(this.getToken()).pipe(
      mergeMap(token => {
        // Use the token in the request
        req = req.clone({
          headers: req.headers.set("Authorization", "Bearer " + token)
        });

        // Handle the request
        return next.handle(req);
      }));
  }
}

app.module.ts


  providers: [
    ...
    { provide: HTTP_INTERCEPTORS, useClass: AuthInterceptor, multi: true},
    ...
  ]

login.html (Ionic Login頁面)

<ion-header>
  <ion-navbar>
    <ion-title>登入</ion-title>
  </ion-navbar>
</ion-header>

<ion-content>
  <ion-card *ngIf="isLoginError">
    <ion-card-content>
      <p class="error-message">
        <ion-icon name='remove-circle' is-active="false"></ion-icon> 帳號密碼錯誤
      </p>
    </ion-card-content>
  </ion-card>
  <form #loginForm="ngForm" class="col s12 white" (ngSubmit)="OnSubmit(userId.value,password.value)">
    <ion-list>

      <ion-item>
        <ion-input type="text" placeholder="帳號" #userId name="userId" (click)="hideError()" required></ion-input>
      </ion-item>

      <ion-item>
        <ion-input type="password" #password ngModel name="password" (click)="hideError()" placeholder="密碼" required></ion-input>
      </ion-item>

    </ion-list>

    <div padding>
      <button ion-button color="primary" block [disabled]="!loginForm.valid" icon-start type="submit">
          <ion-icon ios="ios-log-in" md="md-log-in"></ion-icon>
        Sign In
      </button>
    </div>
  </form>
</ion-content>

login.ts

import { Component } from '@angular/core';
import { HttpErrorResponse } from '@angular/common/http';
import { NavController, NavParams } from 'ionic-angular';
import { HomePage } from '../home/home';
import { AuthProvider } from '../../providers/auth/auth';
import { Storage } from '@ionic/storage';

@Component({
  selector: 'page-login',
  templateUrl: 'login.html',
})
export class LoginPage {
  isLoginError = false;

  constructor(public navCtrl: NavController, public navParams: NavParams,
    private auth: AuthProvider, public storage: Storage) {
  }

  ionViewDidLoad() {}

  OnSubmit(userId, password) {
    // 登入
    this.auth.login(userId, password).subscribe((data: any) => {
      this.storage.set('userToken', data.access_token);
      this.navCtrl.setRoot(HomePage);
    },
      (err: HttpErrorResponse) => {
        //console.log(err);
        this.isLoginError = true;
      });
  }

  hideError() {
    this.isLoginError = false;
  }
}

=========================================================

後端 Web API

Web.config (設定CROS)

  <system.webServer>
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="http://localhost:4200" />
        <add name="Access-Control-Allow-Headers" value="*" />
      </customHeaders>
    </httpProtocol>
    ...
  </system.webServer> 

Global.asax (設定CROS,跳過OPTIONS請求)

        protected void Application_BeginRequest()
        {
            if (Request.Headers.AllKeys.Contains("Origin", StringComparer.OrdinalIgnoreCase) &&
                Request.HttpMethod == "OPTIONS")
            {
                Response.End();
            }
        }

使用NuGet安裝下面套件  

  • Microsoft.Owin.Host.SystemWeb

在專案下新增Startup.cs (OWIN Startup class)

    public class Startup
    {
        public void Configuration(IAppBuilder app)
        {
            OAuthAuthorizationServerOptions option = new OAuthAuthorizationServerOptions
            {
            	//指定路徑以驗證使用者,若驗證通過,回傳 access_token
                TokenEndpointPath = new PathString("/token"),
                //指定用來驗證使用者的Provider(自己寫)
                Provider = new ApplicationOAuthProvider(),
                //指定Token過期時間
                AccessTokenExpireTimeSpan = TimeSpan.FromDays(1),
                AllowInsecureHttp = true
            };
            app.UseOAuthAuthorizationServer(option);
            app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions());
        }
    }

ApplicationOAuthProvider.cs (驗證使用者的Provider,最上面的參考範例是用ASP.NET Identity去驗證使用者,這邊的寫法改成自己寫個類別去判斷帳號密碼是否正確,然後回傳自訂使用者的Model)

    public class ApplicationOAuthProvider : OAuthAuthorizationServerProvider
    {
        public override async Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
        {
            context.Validated();
        }

        public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
        {
            AccountManager manager = new AccountManager();
            //驗證帳號密碼,若成功就回傳UserModel
            UserModel user = await manager.FindUserAsync(context.UserName, context.Password);
            if (user != null)
            {
            	//使用ClaimsIdentity儲存User的資料(用甚麼欄位名稱,存幾個欄位,視需求自訂)
                var identity = new ClaimsIdentity(context.Options.AuthenticationType);
                identity.AddClaim(new Claim("Username", user.UserName));
                identity.AddClaim(new Claim("Email", user.Email));
                identity.AddClaim(new Claim("FirstName", user.FirstName));
                identity.AddClaim(new Claim("LastName", user.LastName));
                identity.AddClaim(new Claim("LoggedOn", DateTime.Now.ToString()));
                context.Validated(identity);
            }
            else
                return;
        }
    }

AccountController.cs (Web API Controller)

    public class AccountController : ApiController
    {
        [Authorize]
        [ActionName("GetEmail")]
        public string GetEmail()
        {
        	//從ClaimsIdentity取回先前存的使用者資料
            var identityClaims = (ClaimsIdentity)User.Identity;
            IEnumerable<Claim> claims = identityClaims.Claims;
            return identityClaims.FindFirst("Email").Value;
        }
    }

WebApiConfig.cs (若呼叫網址要用Action Name,可加上下面這段)

        public static void Register(HttpConfiguration config)
        {
            ...

            // Controllers with Actions
            // To handle routes like `/api/Account/UserProfile`
            config.Routes.MapHttpRoute(
                name: "ControllerAndAction",
                routeTemplate: "api/{controller}/{action}"
            );
        }