[筆記][Angular 2][Angular 4][Login][登入][WebAPI]Angular 2/4 結合 WebAPI 的登入驗證機制範例。

前端的 Angular 2/4 與後端的 WebAPI 結合起來,才是完整的系統架構,而這樣的系統架構下,如何進行登入的驗證呢?本篇就以實際的範例來說明。

緣起

小喵維護的系統架構,慢慢的從之前的 ASP.NET WebForm 搭配 COM+的架構,慢慢的演化,走向前端 Angular 2/4 與後端 WCF+WebAPI 的架構。在這樣的架構中,如何進行登入驗證?這是本篇範例所要進行的。

預期效果

小喵預期在 Angular 2 (Angular 4) 專案中,有幾個頁面的 Component  (prod, shipper, order, login)
其中:

  • prod, shipper 這兩個 component 畫面不用驗證就可以直接瀏覽~
  • order 這個 component,則需要登入驗證過,才能進行瀏覽。未登入的狀況下,會自動導向到 login 這個 component 進行登入驗證,驗證完成後,會自動導向回 order 這個 component 頁面進行瀏覽。
  • 有登入的狀況下,order 這個 component,會從 cookie 中(安全註解1)取回 驗證過的 UToken ,透過 http 的 header 傳回進行驗證(安全註解2)

安全註解:

目前這個範例,並非完全完整的範例,使用請特別注意:

  1. WebAPI屬於無狀態的一種服務,因此狀態勢必要存於Client端,存於Client端,無論是 Cookie 或者其他 html5 Web Storage,都有被竄改、假冒的風險,因此還要配合其他的機制進行防護才能完整。(例如:CSRF, XSS的風險)
  2. 透過 header 傳遞資訊在 WebAPI 機制中常見,但內容容易被拮(劫)取,因此,正式使用時,請至少務必搭配「https」進行,較為安全。
  3. 本篇為了關注於『登入驗證』的機制,以上的安全風險先說明,後續再以其他文章補充。

接下來,就來介紹「登入驗證」機制的相關內容,介紹將以以下順序來介紹相關部分。

  1. 資料庫表與欄位設計
  2. WebAPI的部分
    1. Models
    2. Controllers
  3. Angular 2 (Angular 4)
    1. 因應 Angular 4 的 AppModule 補充
    2. components準備
    3. guard
    4. service
    5. routing
    6. login component
    7. 測試

 

資料庫表與欄位設計

首先,先設計資料表的部分,分別設計兩個資料別

  • 使用者:帳號、密碼(加密儲存)、與其他使用者相關個人資訊(姓名、EMail、...)
  • UToken : 單一帳號可同時有多個 UToken (單一使用者多裝置、瀏覽器同時上線的設計)

「使用者」資料表:

主索引 欄位名稱 型態大小 預設值 說明
PK USRID varchar(20)   使用者代號
  PW varchar(100)   密碼(加密後存放)
  USRName nvarchar(20) N'' 使用者姓名
  TEL1 varchar(30) '' 電話分機
  Email varchar(70) '' Email
  AddTime varchar(14) '' 加入時間
  ChgTime varchar(14) '' 最後維護時間

 

「UToken」 資料表:

主索引 欄位名稱 型態大小 預設值 說明
PK UToken varchar(50)   UToken
  USRID varchar(20) '' 使用者帳號
  UTokenTimeOut varchar(14) '' UToken逾時時間
  ClientIP varchar(30) '' Client的IP
  LastInTime varchar(14) '' 最後進入時間

以上的資料表欄位設計,只是一個範例,實際的設計,需依據自己的需求,去做調整。

WebAPI的部分

Models

ConnStrInfo:連接字串的類別,用以取得連接字串。

Public Class ConnStrInfo
    '這裡存放您的連接字串
    Private m_ConnStr As String = "Data Source=.\SqlExpress;Initial Catalog=你的資料庫名稱;Integrated Security=True"

    '唯讀的屬性,用以傳送出連接字串。
    Public ReadOnly Property ConnStr As String
        Get
            Return m_ConnStr
        End Get
    End Property
End Class

 

ErrMsgInfo:用以處理錯誤訊息傳回的類別。

Imports System.Net.Http
Imports System.Net

Public Class ErrMsgInfo
    Private m_ErrCode As String = ""
    Private m_ErrMsg As String = ""
    Private m_ErrTime As String = ""
    Private m_ErrJSON As String = ""

    Private m_Ex As Exception = Nothing
    Private m_RepMsg As HttpResponseMessage

    Public WriteOnly Property Ex As Exception
        Set(value As Exception)
            m_Ex = value
            If m_Ex IsNot Nothing Then
                m_ErrCode = m_Ex.HResult
                m_ErrMsg = m_Ex.Message
                GenErrJSON()
            End If
        End Set
    End Property

    Public ReadOnly Property RepMsg As HttpResponseMessage
        Get
            Return m_RepMsg
        End Get
    End Property


    Private Sub GenErrJSON()
        If m_ErrCode <> "" And m_ErrMsg <> "" Then
            '產生錯誤回傳的JSON 
            m_ErrJSON = "{" & Chr(34) & "ErrCode" & Chr(34) & ":" & Chr(34) & m_ErrCode & Chr(34) & "," & Chr(34) & "ErrMsg" & Chr(34) & ":" & Chr(34) & m_ErrMsg & Chr(34) & "}"
            '產生迴船的HttpResponseMessage 
            m_RepMsg = New HttpResponseMessage(HttpStatusCode.ExpectationFailed)
            m_RepMsg.Content = New StringContent(m_ErrJSON)
            m_RepMsg.Content.Headers.ContentType = New Headers.MediaTypeHeaderValue("application/json")
        End If
    End Sub
End Class

 

UserInfo:用以對映User資料表的類別

Public Class UserInfo
    ''' <summary> 
    ''' 使用者代號 
    ''' </summary> 
    Public Property USRID() As String = ""

    ''' <summary> 
    '''  密碼
    ''' </summary> 
    Public Property PW() As String = ""

    ''' <summary> 
    ''' 使用者姓名 
    ''' </summary> 
    Public Property USRName() As String = ""

    ''' <summary> 
    ''' 分機 
    ''' </summary> 
    Public Property TEL1() As String = ""

    ''' <summary> 
    ''' EMail 
    ''' </summary> 
    Public Property EMail() As String = ""

    ''' <summary> 
    ''' 加入時間 
    ''' </summary> 
    Public Property AddTime() As String = ""

    ''' <summary> 
    ''' 最後維護時間 
    ''' </summary> 
    Public Property ChgTime() As String = ""
End Class

 

UTokenInfo:用以對映UToken資料表的類別

Public Class UTokenInfo
    Public Property UToken As String = ""
    Public Property USRID As String = ""
    Public Property UTokenTimeOut As String = ""
    Public Property IP As String = ""
    Public Property LastInTime As String = ""
End Class

 

LoginInInfo:用以透過 Post Body 傳入 WebAPI 的類別

Public Class LoginInInfo
    Public Property USRID As String = ""
    Public Property PW As String = ""
End Class

 

LoginOutInfo:用以登入驗證成功後,傳回物件的類別

Public Class LoginOutInfo
    Public Property Rc As String = ""
    Public Property UToken As String = ""
End Class

 

UserDao:用以登入驗證(帳密)、UToken驗證的類別。

Imports System.Data
Imports System.Data.SqlClient
Imports Dapper

Public Class UserDao
    ''' <summary>
    ''' 登入驗證,並傳回Token
    ''' </summary>
    ''' <param name="UsrID">使用者代號</param>
    ''' <param name="PW">密碼</param>
    ''' <param name="IP">IP</param>
    ''' <param name="UToken">回傳UToen</param>
    ''' <returns>
    ''' 1.取得傳入資料
    ''' 2.密碼加密(SHA256)
    ''' 3.驗證使用者代號與加密後的密碼
    ''' 4.如果驗證失敗,傳回錯誤
    ''' 5.驗證成功:
    '''     5-1.產生新的UToken
    '''     5-2.取得UToken逾時時間設定
    '''     5-3.計算UToken逾時時間
    '''     5-4.刪除過期的UToken
    '''     5-5.維護本次UToken
    ''' 6.傳回
    ''' </returns>
    Public Function Login(ByVal UsrID As String, ByVal PW As String, ByVal IP As String, ByRef UToken As String) As String
        Try
            Dim Rc As String = ""
            Dim oConnStr As New ConnStrInfo
            Using Conn As New SqlConnection(oConnStr.ConnStr)
                Conn.Open()
                Dim SqlTxt As String = ""
                '**     1.取得傳入資料
                '**     2.密碼加密(SHA256)
                Dim oEnc As New PUtilEncrypt.CUtilEncrypt
                Dim PWEnc As String = oEnc.SHA256_Encrypt(PW)

                '**     3.驗證使用者代號與加密後的密碼
                SqlTxt &= " SELECT USRID "
                SqlTxt &= " FROM TCATUser (NOLOCK) "
                SqlTxt &= " WHERE USRID = @USRID AND PW=@PW "
                SqlTxt &= "  "
                Dim Cmmd As New SqlCommand(SqlTxt, Conn)
                Cmmd.Parameters.AddWithValue("@USRID", UsrID)
                Cmmd.Parameters.AddWithValue("@PW", PWEnc)

                Dim Dr As SqlDataReader = Cmmd.ExecuteReader
                '**     4.如果驗證失敗,傳回錯誤
                If Not Dr.HasRows Then
                    Throw New Exception("帳號或密碼錯誤,請確認您的帳號密碼後重新登入,或者使用忘記密碼重新設定密碼~")
                End If
                Dr.Close()
                '**     5.驗證成功:
                '**         5-1.產生新的UToken
                UToken = Guid.NewGuid.ToString.ToUpper
                '**         5-2.取得UToken逾時時間設定

                Dim TimeoutMinute As Integer = 20   '預設20分鐘

                '**         5-3.計算UToken逾時時間
                Dim UTokenTime As String = Format(DateAdd(DateInterval.Minute, TimeoutMinute, Now), "yyyyMMddHHmmss")
                Dim NowTime As String = Format(Now, "yyyyMMddHHmmss")

                '**         5-4.刪除過期的UToken
                SqlTxt = ""
                SqlTxt &= " DELETE [dbo].[TCATUToken] "
                SqlTxt &= " WHERE UTokenTimeOut < @NowTime "
                SqlTxt &= "  "

                Conn.Execute(SqlTxt, New With {.NowTime = NowTime})

                '**         5-5.維護本次UToken
                SqlTxt = ""
                SqlTxt &= " INSERT INTO TCATUToken "
                SqlTxt &= " 	(UToken, USRID, UTokenTimeOut, IP, LastInTime) "
                SqlTxt &= " VALUES (@UToken, @USRID, @UTokenTimeOut, @IP, @LastInTime) "
                SqlTxt &= "  "

                Dim oUToken As New UTokenInfo
                oUToken.USRID = UsrID
                oUToken.UToken = UToken
                oUToken.UTokenTimeOut = UTokenTime
                oUToken.IP = IP
                oUToken.LastInTime = NowTime

                Conn.Execute(SqlTxt, oUToken)

                '**     6.傳回
                Return "Success"
            End Using

        Catch ex As Exception
            Throw New Exception(ex.Message)
        End Try
    End Function

    Public Function getUserByUToken(ByVal UToken As String) As UserInfo
        Try
            Dim oUser As New UserInfo
            Dim oConnStr As New ConnStrInfo
            Using Conn As New SqlConnection(oConnStr.ConnStr)
                Conn.Open()
                Dim SqlTxt As String = ""
                Dim NowTime As String = Format(Now, "yyyyMMddHHmmss")

                '依據傳入的UToken,取得User相關資料
                SqlTxt = ""
                SqlTxt &= " SELECT U.* "
                SqlTxt &= " FROM TCATUser U (NOLOCK) "
                SqlTxt &= " INNER JOIN TCATUToken UT(NOLOCK) "
                SqlTxt &= " 	ON U.USRID = UT.USRID "
                SqlTxt &= " WHERE UT.UToken = @UToken  "
                SqlTxt &= "     AND UT.UTokenTimeOut >= @NowTime "
                SqlTxt &= "  "

                oUser = Conn.QueryFirst(Of UserInfo)(SqlTxt, New With {.UToken = UToken, .NowTime = NowTime})

                '判斷是否有內容
                If oUser Is Nothing Or oUser.USRID = "" Then
                    Throw New Exception("無相關資料,或者資料已經逾時")
                End If

                '取得Token逾時分鐘數
                Dim TimeoutMinute As Integer = 20

                '計算UToken Timeout時間
                Dim UTokenTime As String = Format(DateAdd(DateInterval.Minute, TimeoutMinute, Now), "yyyyMMddHHmmss")

                '維護UToken Timeout時間
                SqlTxt = ""
                SqlTxt &= " UPDATE TCATUToken "
                SqlTxt &= " SET UTokenTimeOut = @TokenTimeOut "
                SqlTxt &= "     , [LastInTime] = @NowTime "
                SqlTxt &= " WHERE UToken = @UToken "
                Conn.Execute(SqlTxt, New With {.TokenTimeOut = UTokenTime, .NowTime = NowTime, .UToken = UToken})

            End Using
            Return oUser

        Catch ex As Exception
            Throw New Exception(ex.Message)
        End Try
    End Function
End Class

這裡有使用「Dapper」這個小型的 ORM 套件,可以讓我們精簡許多的 Code,請記得透過 Nuget 安裝「Dapper」套件。

 

Controllers

CORS套件安裝:

由於開發過程中,WebAPI 與 Angular 勢必因 Port 不同,而當作不同的 Domain,這部分還是需要將 WebAPI 處理成可以 CORS 的服務。請先透過 Nuget 安裝「WebAPI CORS」的套件。並在「App_Start」中的「WebApiConfig.vb」進行如下的設定:

Imports System
Imports System.Collections.Generic
Imports System.Linq
Imports System.Web.Http
Imports System.Web.Http.Cors '增加這個Import

Public Module WebApiConfig
    Public Sub Register(ByVal config As HttpConfiguration)
        ' Web API 設定和服務
        config.EnableCors() '加上這個設定

        ' Web API 路由
        config.MapHttpAttributeRoutes()

        config.Routes.MapHttpRoute(
            name:="DefaultApi",
            routeTemplate:="api/{controller}/{id}",
            defaults:=New With {.id = RouteParameter.Optional}
        )
    End Sub
End Module

 

UserController:

Imports System.Net
Imports System.Net.Http
Imports System.Web.Http
Imports System.Web.Http.Cors

Namespace Controllers
    <EnableCors("*", "*", "*")>
    Public Class UserController
        Inherits ApiController

        ' GET: api/User
        <HttpGet>
        <Route("api/User/UToken")>
        Public Function GetUserByUToken() As UserInfo
            Try
                Dim oUser As New UserInfo

                '從Request.Header取得UToken
                Dim UToken As String = ""
                If Request.Headers.Contains("UToken") Then
                    UToken = Request.Headers.GetValues("UToken").First()
                End If

                If UToken <> "" Then
                    Dim dao As New UserDao
                    oUser = dao.getUserByUToken(UToken)
                Else
                    Throw New Exception("無UToken傳入")
                End If

                Return oUser
            Catch ex As Exception
                Dim oErr As New ErrMsgInfo
                oErr.Ex = ex

                Dim RepMsg As HttpResponseMessage = oErr.RepMsg
                Throw New HttpResponseException(RepMsg)
            End Try
        End Function

        '' GET: api/User
        'Public Function GetValues() As IEnumerable(Of String)
        '    Return New String() {"value1", "value2"}
        'End Function

        '' GET: api/User/5
        'Public Function GetValue(ByVal id As Integer) As String
        '    Return "value"
        'End Function

        ' POST: api/User/Login
        <HttpPost>
        <Route("api/User/Login")>
        Public Function PostLogin(<FromBody()> ByVal oLoginIn As LoginInInfo) As LoginOutInfo
            Try
                '取得ClientIP
                Dim ClientIP As String = HttpContext.Current.Request.UserHostAddress

                Dim oLoginOut As New LoginOutInfo
                Dim dao As New UserDao
                Dim UToken As String = ""
                Dim Rc As String = dao.Login(oLoginIn.USRID, oLoginIn.PW, ClientIP, UToken)
                oLoginOut.Rc = Rc
                oLoginOut.UToken = UToken

                Return oLoginOut

            Catch ex As Exception
                Dim oErr As New ErrMsgInfo
                oErr.Ex = ex

                Dim RepMsg As HttpResponseMessage = oErr.RepMsg
                Throw New HttpResponseException(RepMsg)

            End Try
        End Function

        '' POST: api/User
        'Public Sub PostValue(<FromBody()> ByVal value As String)

        'End Sub

        '' PUT: api/User/5
        'Public Sub PutValue(ByVal id As Integer, <FromBody()> ByVal value As String)

        'End Sub

        '' DELETE: api/User/5
        'Public Sub DeleteValue(ByVal id As Integer)

        'End Sub
    End Class
End Namespace

 

Angular 2 (Angular 4) 的部分

因應 Angular 4 的 AppModule 補充

Angular 4 開始,有部分的設定預設在初始的「app.module.ts」拿掉,我們把必要的部分加回來。

app.module.ts

//Import的部分
import { FormsModule } from "@angular/forms";   //加回來
import { HttpModule } from "@angular/http";   //加回來

//...
//
imports:[
//...
    FormsModule,    //加回來
    HttpModule  //加回來
]

 

components:

首先,先準備好幾個後續要用到的 component ,透過 「ng g c prod」這樣的指令,分別產生「prod, shipper, order, login」這四個component。

 

Service:

登入驗證,呼叫 WebAPI ,裡面主要有兩個 function,一個是登入驗證(帳密),另一個是驗證UToken。

裡面會用到兩個公用的class,分別是存放server名稱的「util.ts」與cookie存取的「cookieUtil」

util.ts

export class Util {
    serverUrl:string='http://localhost:51218';
    constructor(){
    }
}

 

cookie-util.ts

export class CookieUtil {

  setCookie(cookieName, cookieValue, cookieExpire){
    let sCookie:string='';
    sCookie += cookieName + '=' + cookieValue + ';';
    sCookie += 'expires=' + cookieExpire + ';';
    sCookie += '';

    document.cookie = sCookie;
  }

  getCookie(cname:string):string {
      var name = cname + "=";
      var decodedCookie = decodeURIComponent(document.cookie);
      var ca = decodedCookie.split(';');
      for(var i = 0; i <ca.length; i++) {
          var c = ca[i];
          while (c.charAt(0) == ' ') {
              c = c.substring(1);
          }
          if (c.indexOf(name) == 0) {
              return c.substring(name.length, c.length);
          }
      }
      return "";
  }

}

接著,是本文核心的LoginService

login.service.ts

import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Util } from './util';
import { CookieUtil } from './cookie-util';
import { Router } from '@angular/router';
import { Http, RequestOptions, Headers, Response } from '@angular/http';

@Injectable()
export class LoginService {
  oUtil = new Util();
  oUtilCook = new CookieUtil();
  error:any;

  backUrl:string='';

  oLoginIn:any={UsrID:'',PW:''};
  oLoginOut:any={};
  oUser:any={};

  constructor(private http:Http,private router:Router) { }


  //進行登入驗證
  doLogin(){
    let url=this.oUtil.serverUrl +'/api/User/Login';
    let headers = new Headers({'Content-Type':'application/json'});
    let options = new RequestOptions({headers:headers});
    console.log(this.oLoginIn)
    this.http.post(url,this.oLoginIn, options)
    .subscribe(
      (value:Response)=>{
        this.oLoginOut = value.json();
        console.log(this.oLoginOut);
        if(this.oLoginOut.UToken!=''){
          console.log('backUrl:' + this.backUrl);
          
          //將UToken放入Cookie中
          this.UTokenSetCookie(this.oLoginOut.UToken);
          this.router.navigate([this.backUrl]);
        }
      },
      (error)=>{
        console.log(error.json());
        this.error = error.json();
      }
    );
  }

//判斷是否已經登入
isLoginedIn():Observable<boolean>{
  // let UToken:string=this.getCookie("UToken");
  let url=this.oUtil.serverUrl +'/api/User/UToken/';
  let headers = new Headers({'Content-Type':'application/json'});

  //從Cookie取得UToken
  let UToken:string=this.oUtilCook.getCookie('UToken');
  //console.log('UTokenFromCookie:' + UToken);
  if(UToken==''){
    UToken=this.oLoginOut.UToken;
    //console.log('UTokenFromLoginService:' + UToken);
  }
  //將UToken放入Header中
  headers.append('UToken',UToken);
  let options = new RequestOptions({headers:headers});
  return this.http.get(url, options)
    .map(() => {
      //成功,重新將UToken存入Cookie
      this.UTokenSetCookie(UToken);
      return true;
    })
    .catch(() => {
      // this is executed on a 401 or on any error
      //失敗,導入登入畫面。
      this.router.navigate(['/login']);
      return Observable.of(false);
    });
  }

  //傳入UToken,將UToken存入Cookie中
  UTokenSetCookie(UToken:string){
    this.oUtilCook.setCookie('UToken',UToken,20*60*1000);
  }

}

 

guard :

登入是否驗證,是透過「canActivate」必指定「guard」來處理。

所以我們先透過以下的指令,產生登入檢查的「guard」

ng g guard LoginRoutingGuard 

LoginRoutingGuard要搭配 LoginService一起作用~

import { LoginService } from './login.service';
import { Injectable } from '@angular/core';
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable';

@Injectable()
export class LoginRoutingGuard implements CanActivate {


  constructor(private svcLogin:LoginService){
  }

  canActivate(
    next: ActivatedRouteSnapshot,
    state: RouterStateSnapshot): Observable<boolean> {

      //console.log(state.url);
      //取得登入後導回的網址
      this.svcLogin.backUrl = state.url;

      return this.svcLogin.isLoginedIn()
        .map((allow:boolean) => {
           // do something here
           // user.access_level;
          //return true;
          return allow;
         });          

  }
}

 

routing

再搭配 routing 中,指定某些連界頁面,需要驗證才能運作

app-routing.module.ts

import { LoginRoutingGuard } from './login-routing.guard';
import { LoginComponent } from './login/login.component';
import { OrderComponent } from './order/order.component';
import { ShipperComponent } from './shipper/shipper.component';
import { ProdComponent } from './prod/prod.component';
import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';

const routes: Routes = [
  { path: '', redirectTo:'prod', pathMatch:'full'},
  { path: 'prod', component: ProdComponent},
  { path: 'shipper', component: ShipperComponent},
  { path: 'order', component: OrderComponent, canActivate:[LoginRoutingGuard]},
  { path: 'login', component: LoginComponent},
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

可以看到,routing 中,不需驗證的頁面,如同以往的撰寫設定即可。需要登入驗證才能瀏覽的頁面,例如: order 這個 component ,藉由後面的設定 canActivate:[LoginRoutingGuard] ,指定藉由LoginRoutingGuard來檢查。

 

login component內容

login.component.ts:主要要將 LoginService 注入進來

import { Component, OnInit } from '@angular/core';
import { LoginService } from "app/login.service";

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css']
})
export class LoginComponent implements OnInit {

  constructor(private svcLogin:LoginService) { }

  ngOnInit() {
  }

}

 

login.component.html

<div class="modal show">
  <div class="modal-dialog">
    <div class="modal-content">
      <div class="modal-header">
        <button class="close" aria-hidden="true" type="button" data-dismiss="modal">&times;</button>
        <h4 class="modal-title">Login</h4>
      </div>
      <div class="modal-body">

        <form class="form-horizontal" name="LoginForm" >
          <fieldset>
            <!--<legend>Legend</legend>-->
            <div class="form-group">
              <label class="col-lg-2 control-label" for="txtUsrID">帳號:</label>
              <div class="col-lg-10">
                <input class="form-control" id="txtUsrID" type="text" name="txtUsrID" placeholder="帳號" [(ngModel)]="this.svcLogin.oLoginIn.UsrID">
              </div>
            </div>
            <div class="form-group">
              <label class="col-lg-2 control-label" for="txtPassword">密碼</label>
              <div class="col-lg-10">
                <input class="form-control" id="txtPassword" name="txtPassword" type="password" placeholder="密碼" [(ngModel)]="this.svcLogin.oLoginIn.PW">
              </div>
            </div>
            <div *ngIf="this.svcLogin.error!=null" style="Color:red;">
              錯誤訊息:{{this.svcLogin.error.ErrMsg}}
            </div>
          </fieldset>
        </form>

      </div>
      <div class="modal-footer">
        <button class="btn btn-default" type="button" data-dismiss="modal">取消</button>
        <button class="btn btn-primary" type="button" (click)="this.svcLogin.doLogin()">登入</button>
      </div>
    </div>
  </div>
</div>

登入的畫面~帳密藉由雙向綁定,執行Login Service相關程式碼。

 

測試執行

 

相關程式碼:

https://github.com/topcattw/SLogin

 

 

 

 


以下是簽名:


Microsoft MVP
Visual Studio and Development Technologies
(2005~2019/6) 
topcat
Blog:http://www.dotblogs.com.tw/topcat