0%

AWS Serverless Authentication

身份驗證和身份辨識 (Authentication and identity) 幾乎是所有應用程序不可或缺的部分。知道用戶是誰,他們擁有什麼權限,是否登錄以及該用戶的唯一標識符,你的應用程序就可以正確的呈現畫面並為當前登錄的用戶返回正確的數據。

大多數應用程序都需要機制來處理用戶註冊、用戶登錄、密碼加密、和更新,以及圍繞身份管理的其他無數任務。 現代應用程序經常需要諸如,開放式身份驗證 (OAUTH)、多因素身份驗證 (MFA)、和基於時間的一次性密碼 (TOTP) 之類的東西。

過去,開發人員必須從頭開始開發所有身份驗證功能。 僅此一項任務就可能需要一組開發人員花費數週甚至數月的時間才能正確並安全地完成任務。 如今,有諸如 Auth0、Okta、和 Amazon Cognito 之類的完全託管的身份驗證服務可以為我們處理所有這一切。

在此示例中,你將學習如何透過 AWS Amplify 使用 Amazon Cognito 在 React 應用程序中正確安全地實現身份驗證。

這裡要構建一個基本的應用程序,需要進行身份驗證才能查看一個具有有關已登錄用戶的 profile 文件信息頁面。 如果用戶已登錄,則他們可以在 public 公共路由、帶有身份驗證表單的 profile 文件路由和僅對已登錄用戶開放的 protected 受保護路由之間導航。

如果他們尚未登錄,則只能查看公共路由和 profile 文件路由上的身份驗證表單。 如果用戶在未登錄時嘗試訪問受保護的路由,我們會將其重導向到身份驗證表單,以便他們可以登錄,然後在身份驗證後訪問該受保護的路由。

此應用程序還將保持用戶狀態,因此,如果他們刷新該應用程序、或離開該應用程序導航並在稍後返回,他們將會保持在登錄狀態。

Amazon Cognito

Amazon Cognito 是 AWS 完整的身份託管服務。Cognito 允許簡單安全的用戶註冊、登錄、訪問控制 (access control)、和用戶身份管理。Cognito 可以擴展到數百萬用戶,並且還支持使用社交身份提供商 (例如 Facebook、Google 和 Amazon)登錄。 對於任何應用程序的前 50,000 個用戶,它也是免費的。

Amazon Cognito 的運作方式

Cognito 有兩部分: user poolsidentity pools

  • User pools
    這提供了一個安全的用戶目錄,該目錄存儲了你的所有用戶,並可以擴展到數億用戶。 這是一項完全託管的服務。 作為無伺服器技術,很簡單的就可以設置 user pools,而不必擔心任何基礎設施。 User pools 管理所有註冊和登錄帳戶的用戶的資源。

  • Identity pools
    這允許您授權登錄到你的應用程序的用戶訪問各種其他 AWS 服務。 假設您想允許用戶訪問 Lambda 函式,以便他們可以從另一個 API 取得數據,您可以在創建 identity pools 時指定。 這些 identities 的資源可以是來自 Cognito user pools,甚至是 Facebook 或 Google。

Cognito user pools 允許你的應用程序針對服務調用各種方法來管理用戶身份,包括以下各項:

  • 註冊用戶
  • 登錄用戶
  • 登出用戶
  • 修改用戶密碼
  • 重置用戶密碼
  • 確認 MFA 代碼

Amazon Cognito 與 AWS Amplify

AWS Amplify 以各種方式支援 Amazon Cognito。 首先,你可以直接從 Amplify CLI 創建和配置 Amazon Cognito 服務。 透過 CLI 創建身份驗證服務後,你就可以使用 Amplify JavaScript client 客戶端程式庫從 JavaScript 應用程序調用各種方法。例如 signUp、signIn、和 signOut。

Amplify 還具有預配置的 UI 組件,使你可以僅使用幾行代碼來構建整個身份驗證流程,同時支援 React、React Native、Vue、和 Angular 等前端框架。

在此示例中,你將結合使用 Amplify CLI、Amplify JavaScript client 和 Amplify React UI 組件來構建一個示範路由,身份驗證和受保護路由(routing, authentication, and protected routes)的應用程序。 還將使用 React Router 路由器建立路由和 Ant Design 來為應用程序提供一些基本樣式。

創建 React 應用程序與 Amplify 專案

創建一個新的 React 應用程序:

yarn create react-app basic-authentication

cd basic-authentication

yarn add aws-amplify @aws-amplify/ui-react antd react-router-dom

現在,在新應用程序的根目錄下,創建 Amplify 專案:

amplify init
# 請按照步驟為專案命名,環境名稱並設置預設的文本編輯器。
# 接受其他所有設置的預設值,然後選擇你的 AWS Profile。

現在,Amplify 專案已初始化,我們可以創建身份驗證服務。

amplify add auth
Do you want to use the default authentication and security configuration? Default configuration
How do you want users to be able to sign in? Username
Do you want to configure advanced settings? No, I am done.

現在,身份驗證服務已配置完畢,你可以使用 amplify push 命令部署它:

amplify push
? Are you sure you want to continue? Yes

現在已經部署了身份驗證服務,我們可以開始對其進行應用。

客戶端身份驗證概述

使用 Amplify,有兩種主要方法可以在客戶端上實現身份驗證:

  • Auth class
    Amplify client 客戶端程式庫 Auth 類提供 30 多種不同的方法,使你可以處理與用戶管理相關的所有事情。 例如 Auth.signUp、Auth.signIn、和 Auth.signOut。

    你也可以使用此 Auth 類根據你的應用程序需求,創建完全自定義的身份驗證流程(custom authentication flow)。為此,你則必須自己管理所有的樣式和應用程序狀態。

  • 特定於框架的身份驗證 UI 組件
    Amplify 針對 React、React Native、Vue、和 Angular 等框架提供特定於框架更高層次的身份驗證技術。 使用這些組件,你僅需用幾行代碼,即可呈現完整的(可自定義的)身份驗證流程。

在此示例中,將使用 AWS Amplify React 程式庫中名為 withAuthenticator 的高階組件 (higher-order component HOC)以及路由,來創建受保護的路由和僅在用戶登錄後才能查看的 profile 配置文件。

構建前端應用程序

在你的應用程序專案根目錄的 src 目錄中創建以下文件:

Container.js
Nav.js
Profile.js
Protected.js
Public.js
Router.js

這些文件將執行以下的操作:

  • Container.js
    該文件包含一個組件,使用該組件可將可重複使用的樣式應用於其他組件。
  • Nav.js
    在此組件中,你將創建一個導航 UI。
  • Profile.js
    該組件將提供有關已登錄用戶的 profile 配置文件信息。 這也是我們添加身份驗證組件以進行註冊和登錄的組件。
  • Protected.js
    這是我們用來示範受保護路由的組件。 如果用戶已登錄,他們將能夠查看此路由。 如果未登錄,他們將重新導向到登錄表單。
  • Public.js
    這是一個基本路由,無論用戶是否登錄都可以查看。
  • Router.js
    該文件將保存路由器狀態和確定當前路由名稱。
Container 組件

首先,讓我們創建將用於此應用程序的最簡單的組件,即 Container 組件。 我們將使用此組件來包裝所有其他組件,以便在組件之間應用一些可重用的樣式:

src/Container.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'

const Container = ({ children }) => (
<div style={styles.container}>
{ children }
</div>
)

const styles = {
container: {
margin: '0 auto',
padding: '50px 100px'
}
}

export default Container

你現在可以在整個應用程序中使用此組件以應用一致的樣式,而不必在每個組件中重寫樣式。 你可以像這樣使用它:

1
2
3
<Container>
<h1>Hello Tainan</h1>
</Container>

Container 組件的子組件的樣式都將使用 Container 組件中設置的樣式進行呈現。 這樣做可以讓你擁有一個可以控制樣式的地方。 如果你以後想要更改樣式,則只需調整一個組件。

Public 組件

這個組件僅簡單的呈現路由名稱到 UI 介面,無論用戶是否登錄均可訪問。在此組件中,將使用 Container 組件添加一些 padding 和 margin 樣式。

src/Public.js
1
2
3
4
5
6
7
8
9
10
import React from 'react'
import Container from './Container'

const Public = () => (
<Container>
<h1>Public route</h1>
</Container>
)

export default Public

Nav(導航)組件將利用 Ant Design 程式庫和 React Router。 Ant Design 將提供 Menu 和 Icon 組件,以使菜單看起來更加美觀;而 React Router 將提供 Link 組件,以便我們可以鏈接和導航至應用程序的不同部分。

你還會注意到,有一個 current 的屬性傳遞到組件中,該屬性代表當前路由的名稱。 對於此應用程序,該值將為 home、profile、或 protected。 current 的值用於 Menu 組件的 selectedKeys 陣列中,以在導航欄中突顯當前的路由。
這個 current 值將在 Router 組件中計算,並作為屬性傳遞給這個 Nav 組件:

src/Nav.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import React from 'react'
import { Link } from 'react-router-dom'
import { Menu } from 'antd'
import {
HomeOutlined, ProfileOutlined, FileProtectOutlined
} from '@ant-design/icons'

const Nav = ({ current = 'home' }) => (
<div>
<Menu selectedKeys={[current]} mode="horizontal">
<Menu.Item key="home">
<Link to={'/'}>
<HomeOutlined />Home
</Link>
</Menu.Item>
<Menu.Item key="profile">
<Link to={'/profile'}>
<ProfileOutlined />Profile
</Link>
</Menu.Item>
<Menu.Item key="protected">
<Link to={'/protected'}>
<FileProtectOutlined />Protected
</Link>
</Menu.Item>
</Menu>
</div>
)

export default Nav;
Protected 組件

Protected 組件將是受保護或專用的路由。 如果嘗試訪問此路由的用戶已登錄,則他們將能夠查看此路由。 如果未登錄,則將其重導向到 profile 頁面進行註冊或登錄。

在此組件中,將使用 React 的 useEffect 掛鉤和 AWS amplify 的 Auth 類:

  • useEffect
    這是一個 React hook,允許你在函式組件中執行有副作用(side effects)的運算。 該掛鉤接受一個函式,該函式在首次渲染時或(可選擇性的)在其每次渲染時都會被調用。 透過傳入一個空陣列作為第二個參數,則表示只會在組件初次載入時只觸發一次函式。

  • Auth
    AWS Amplify class 處理用戶身份管理。 你可以使用此類完成註冊用戶、登錄、到重新設置密碼的所有操作。 在此組件中,我們將調用 Auth.currentAuthenticatedUser 方法,該方法將檢查用戶當前是否已登錄,如果是,則返回有關已登錄用戶的數據。

src/Protected.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'
import Container from './Container'

const Protected = ({ history }) => {
useEffect(() => {
Auth.currentAuthenticatedUser()
.catch(() => history.push('/profile'))
}, [])
return (
<Container>
<h1>Protected route</h1>
</Container>
)
}

export default Protected

呈現組件後,我們透過在 useEffect 掛鉤中調用 Auth.currentAuthenticatedUser 來檢查用戶是否登錄到應用程序。 如果此 API 調用失敗,則意味著該用戶尚未登錄,我們需要將其重定向。 我們透過調用 history.push(‘/profile’) 重新定向。

在這個元件中我們使用 useEffect 掛鉤以保護需要身份驗證的路由。我們可以創建一個自定義的 protectedRoute 掛鉤用在需要保護的路由上,這樣,可以消除需要在各個組件上重複身份驗證的任何代碼。

在 src 目錄中,創建一個名為 protectedRoute.js 的新文件,並添加以下代碼:

src/protectedRoute.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { useEffect } from 'react'
import { Auth } from 'aws-amplify'

const protectedRoute = (Comp, route = '/profile') => (props) => {
async function checkAuthState() {
try {
await Auth.currentAuthenticatedUser()
} catch (err) {
props.history.push(route)
}
}

useEffect(() => {
checkAuthState()
}, []);

return <Comp {...props} />
}

export default protectedRoute

我們可以使用這個自定義的 protectedRoute 掛鉤來保護任何需要保護的組件:

1
2
3
4
5
export default protectedRoute(App)



export default protectedRoute(App, '/about-us')

現在,我們可以重構我們的應用程序,更新 Protected 組件以使用此新的 protectedRoute 掛鉤:

src/Protected.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import Container from './Container'
import protectedRoute from './protectedRoute'

const Protected = () => {
return (
<Container>
<h1>Protected route</h1>
</Container>
);
}

export default protectedRoute(Protected)
Router 組件

Router 組件將定義我們在應用程序中可用的組件和路由。

這個組件將用基於 window.location.href 屬性來設置當前(current)路由名稱,該名稱將用於 Nav 組件,以突顯當前的路由。

這裡還將使用 React Router 的組件 HashRouter、Switch 、和 Route:

  • HashRouter
    這是使用 URL 的 hash 部分(即 window.location.hash),使你的 UI 與 URL 保持同步的路由器。
  • Switch
    Switch 呈現與位置匹配的第一個子路由。 這與僅使用路由器的預設功能不同,Switch 可以設定渲染多個與位置匹配的路由。
  • Route
    這個組件使你可以基於 path 參數定義要渲染的組件:
src/Router.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import React, { useState, useEffect } from 'react'
import { HashRouter, Switch, Route } from 'react-router-dom'

import Nav from './Nav'
import Public from './Public'
import Profile from './Profile'
import Protected from './Protected'

const Router = () => {
const [current, setCurrent] = useState('home');

useEffect(() => {
setRoute()
window.addEventListener('hashchange', setRoute)
return () => window.removeEventListener('hashchange', setRoute)
}, [])

function setRoute() {
const location = window.location.href.split('/')
const pathname = location[location.length - 1]
setCurrent(pathname ? pathname : 'home')
}

return (
<HashRouter>
<Nav current={current} />
<Switch>
<Route exact path="/" component={Public} />
<Route exact path="/protected" component={Protected} />
<Route exact path="/profile" component={Profile} />
<Route component={Public} />
</Switch>
</HashRouter>
);
}

export default Router

在此組件的 useEffect 掛鉤內,我們透過調用 setRoute 來設置路由名稱。 我們還設置了一個事件偵聽器,以在路由更改時調用 setRoute,以反映當前的路由。

Profile 組件

我們要完成應用程序的最後一個組件是 Profile 組件。 這個組件將執行以下操作:

  • 如果用戶未登錄,則呈現身份驗證表單
  • 提供登出按鈕
  • 將用戶的個人資料信息呈現到 UI

這裡將使用 withAuthenticator 的高階組件(higher-order component HOC)包裝 Profile 組件來呈現身份驗證流程。如果用戶未登錄,這將顯示註冊/登錄表單,如果用戶已登錄,則將顯示帶有用戶個人資料詳細信息的 UI。

我們使用 AmplifySignOut UI 組件來讓用戶登出。該組件將登出用戶,然後重新呈現 UI 以顯示身份驗證表單。

要顯示用戶個人資料數據,我們使用 Auth.currentAuthenticatedUser 方法。 如果用戶已登錄,則此方法將返回用戶配置文件數據以及有關的信息。

src/Profile.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React, { useState, useEffect } from 'react'
import { Auth } from 'aws-amplify'
import { withAuthenticator, AmplifySignOut } from '@aws-amplify/ui-react'
import Container from './Container'

const Profile = () => {
const [user, setUser] = useState({})

useEffect(() => {
checkUser()
}, [])

async function checkUser() {
try {
const data = await Auth.currentUserPoolUser()
const userInfo = { username: data.username, ...data.attributes }
setUser(userInfo)
} catch (err) { console.log('error: ', err) }
}

return (
<Container>
<h1>Profile</h1>
<h2>Username: {user.username}</h2>
<h3>Email: {user.email}</h3>
<h4>Phone: {user.phone_number}</h4>
<AmplifySignOut />
</Container>
);
}

export default withAuthenticator(Profile)
樣式化 UI 組件

在背後,Amplify 使用 Web 組件實現它的 UI 組件,這意味著我們可以將它們當成 HTML 元素的第一等類別來使用 CSS 樣式。 我們希望我們的 UI 組件與應用程序其他部分中的顏色匹配,我們可以在 index.css 的底部添加以下 CSS 屬性,以定義我們要使用的顏色:

src/index.css
1
2
3
4
5
:root {
--amplify-primary-color: #1890ff;
--amplify-primary-tint: #1890ff;
--amplify-primary-shade: #1890ff;
}
配置應用程序

現在,應用程序組件已構建。 我們需要做的最後一件事是更新 index.js 以導入 Router 路由器並添加 Amplify 配置。以及為 Ant Design 程式庫導入必要的 CSS 樣式。

src/index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import React from 'react';
import ReactDOM from 'react-dom';
import './index.css';
import Router from './Router'
import 'antd/dist/antd.css'

import Amplify from 'aws-amplify'
import config from './aws-exports'
Amplify.configure(config)

ReactDOM.render(
<Router />,
document.getElementById('root')
);

現在啟動應用程序:

1
yarn start

現在測試一下你的應用程序,註冊一個用戶、確認 MFA 代碼 (驗證碼會透過 email 傳送)、登入、登出以及受保護的路由。

在此示例中,我們學到了幾點:

  • 使用 withAuthenticator HOC 可以快速啟動並運行預配置的身份驗證流程。

  • 使用 Auth 類可對身份驗證進行更細粒度的控制,並獲取有關當前登錄用戶的數據。

  • Ant Design 可幫助你進行預配置設計,而無需編寫任何特定於樣式的代碼。