Ionic v3 使用 Token Based Authentication 實作Login頁面
此篇文章是參照下面兩篇文章改寫的:
- Angular 5 Login and Logout with Web API Using Token Based Authentication
- Use a promise in Angular HttpClient Interceptor (stackoverflow)
前端程式:
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送出前攔截下來,然後任你處置...)
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}"
);
}