liujia 2019-06-28
序言:本文主要介绍了使用 Ionic 和 Cordova 开发混合应用时如何添加用户身份认证。教程简易,对于 Ionic 入门学习有一定帮助。因为文章是去年发表,所以教程内关于 Okta 的一些使用步骤不太准确,但是通过 Okta 的官网也可以找到对应的内容。另外,使用 npm 安装 Ionic starter 模板可能会有安装失败的情况,建议不要在这方面浪费太多时间,可以直接在 Ionic 的 GitHub 仓库 中下载 starter 模板。原文:How to Sprinkle ReactJS into an Existing Web Application
译者:nzbin
使用 Okta 和 OpenID Connect (OIDC),可以很轻松的在 Ionic 应用中添加身份认证,完全不需要自己实现。 OIDC 允许你直接使用 Okta Platform API 进行认证,本文的目的就是告诉你如何在一个 Ionic 应用中使用这些 API。我将演示如何使用 OIDC 重定向、Okta 的 Auth SDK 以及基于 Cordova 内嵌浏览器的 OAuth 进行登录; 由于功能还在开发中,所以省略了用户注册。
Ionic 是一个用于开发原生及先进 web 应用的开源的移动端 SDK。它使用 Angular 和 Apache Cordova ,可以用 HTML、CSS、和 JavaScript 来开发移动应用。Apache Cordova 将 HTML 代码嵌入到一个设备上的原生 WebView 中, 通过外部功能接口来访问原生资源。你可能听说过 PhoneGap —— 这是 Adobe Cordova 的商业版本。
Cordova 和 PhoneGap 允许你使用一套代码开发多个平台的应用 (比如 Android 和 iOS) 。除此之外,应用程序和原生程序相差无尽并且和原生体验一样好。如果你需要开发原生功能,使用 web 技术是无法实现的,但是有些原生插件可以实现。 Ionic Native 是这些插件的精选集。
我第一次使用 Ionic 是在 2013 年底。当时我做的项目是开发一款原生应用,但是打算使用 HTML 来开发适配多个屏幕的应用,这样 web 开发者也可以参与开发。我在 2014 年的三月写了我的经历。我喜欢使用 Ionic,我发现使用 Ionic 移植现有的应用程序更多的就是修改 HTML 和调整 CSS。
Ionic 2 在 一月份发布, 可以使用 Angular 开发 Ionic 应用。 Ionic 3 在 四月份发布,允许使用 Angular 4 进行开发。
注意: "Angular" 是 Angular 2+ 的通用名称。AngularJS 是 1.x 版本的名称。之所以用 Angular 命名是因为在 2017 年的三月发布了 Angular 4 。可以查看 Branding Guidelines for Angular and AngularJS 了解更多信息。
本文会演示如何创建一个简单的 Ionic 应用以及如何添加用户身份认证。大多数的应用都需要身份认证,这样才能知道用户是谁。一旦 app 知道你的身份,它就可以保存你的信息及个性化的功能。
为了设置 Ionic 的开发环境,需要完成以下几步:
npm install -g cordova ionic
在 terminal 窗口中,使用以下命令创建一个新的应用程序:
ionic start ionic-auth
命令行会提示选择一个 starter 项目并且可以选择是否将应用连接到 Ionic Dashboard。对于本教程,选择 tabs starter 项目,不需要将项目连接到 Ionic Dashboard。
相关教程:Getting Started with Angular v2+
项目创建需要花费一到两分钟,这取决于你的网络连接速度。运行以下命令来打开你的 Ionic 应用。
cd ionic-auth ionic serve
这个命令默认打开浏览器的 http://localhost:8100。你可以使用 Chrome 的设备模式查看应用程序在 iPhone 6 中的效果。
使用 Ionic serve
命令的特点是它会在浏览器中显示编译错误,而不是(有时会隐藏)在开发控制台。比如,给 app.component.ts
组件中的 rootPage
变量设置一个非法类型,你将看到以下错误。
Ionic Cloud 提供了免费的 Auth 服务。它允许使用邮箱及密码验证身份,也可以使用社交提供商比如 Facebook、Google 和 Twitter 登录。你可以使用 @ionic/cloud-angular
依赖中提供的类创建身份认证。它也支持 自定义身份认证,但是 "需要你自己的服务器处理身份认证"。
目前还没有太多关于这方面的教程,不过从去年开始有了一些。
你可能注意到所有的教程都需要很多的代码。另外,关于如何在后端的 Auth 服务中验证用户身份的文档也不多。
OpenID Connect (OIDC) 基于 OAuth 2.0 协议。它允许客户端验证用户的身份并获得他们的基本配置文件信息。为了将 Okta 的身份认证平台整合到用户身份认证中,需要以下步骤:
http://localhost:8100
作为重定向的 URI 并点击 Finish。你会看到以下设置信息:为了创建身份认证的登录页,先创建 src/pages/login.ts
和 src/pages/login.html
。在 login.html
中,添加一个具有 username 和 password 的表单。
<ion-header> <ion-navbar> <ion-title> Login </ion-title> </ion-navbar> </ion-header> <ion-content padding> <form #loginForm="ngForm" (ngSubmit)="login()" autocomplete="off"> <ion-row> <ion-col> <ion-list inset> <ion-item> <ion-input placeholder="Email" name="username" id="loginField" type="text" required [(ngModel)]="username" #email></ion-input> </ion-item> <ion-item> <ion-input placeholder="Password" name="password" id="passwordField" type="password" required [(ngModel)]="password"></ion-input> </ion-item> </ion-list> </ion-col> </ion-row> <ion-row> <ion-col> <div *ngIf="error" class="alert alert-danger">{{error}}</div> <button ion-button class="submit-btn" full type="submit" [disabled]="!loginForm.form.valid">Login </button> </ion-col> </ion-row> </form> </ion-content>
你可以利用几个开源库来完成实际的身份验证。第一个是 Manfred Steyer's angular-oauth2-oidc. 这个库可以很容易的与 identity tokens 和 access tokens 交互。第二个是 Okta Auth SDK。由于 OIDC 和 OAuth 不是身份认证协议,所以这是使用 JavaScript 完成身份验证所必需的,不必重定向到 Okta 。
使用 npm 安装 angular-oauth2-oidc
npm install angular-oauth2-oidc --save
Okta Auth SDK 目前不支持 TypeScript,可以将以下代码添加到 src/index.html
底部。
<script src="https://ok1static.oktacdn.com/assets/js/sdk/okta-auth-js/1.5.0/OktaAuth.min.js"></script>
在 src/pages/login/login.ts
中, 添加 LoginPage
类的基本结构,在构造器函数中使用 OAuthService
(来自于 angular-oauth2-oidc) 配置了 OIDC 的设置。 你需要使用 Okta OIDC 设置中的 Client ID 替换 "[client-id]" 以及你账户的当前 URI 替换 "[dev-id]"。
import { Component, ViewChild } from '@angular/core'; import { NavController } from 'ionic-angular'; import { OAuthService } from 'angular-oauth2-oidc'; declare const OktaAuth: any; @Component({ selector: 'page-login', templateUrl: 'login.html' }) export class LoginPage { @ViewChild('email') email: any; private username: string; private password: string; private error: string; constructor(private navCtrl: NavController, private oauthService: OAuthService) { oauthService.redirectUri = window.location.origin; oauthService.clientId = '[client-id]'; oauthService.scope = 'openid profile email'; oauthService.oidc = true; oauthService.issuer = 'https://dev-[dev-id].oktapreview.com'; } ionViewDidLoad(): void { setTimeout(() => { this.email.setFocus(); }, 500); } }
修改 src/app/app.component.ts
验证用户是否登录。如果没有,将 LoginPage
设置为 rootPage。
import { OAuthService } from 'angular-oauth2-oidc'; import { LoginPage } from '../pages/login/login'; @Component({ templateUrl: 'app.html' }) export class MyApp { rootPage: any = TabsPage; constructor(platform: Platform, statusBar: StatusBar, splashScreen: SplashScreen, oauthService: OAuthService) { if (oauthService.hasValidIdToken()) { this.rootPage = TabsPage; } else { this.rootPage = LoginPage; } platform.ready().then(() => { statusBar.styleDefault(); splashScreen.hide(); }); } }
更新 src/app/app.module.ts
,在 declarations
和 entryComponents
中添加 LoginPage
。你也要将 OAuthService
添加到 providers
中。
@NgModule({ declarations: [ ... LoginPage ], ... entryComponents: [ ... LoginPage ], providers: [ OAuthService, ... ] })
运行 ionic serve
,确认 LoginPage
在 app 首次加载后可以展示出来。app 加载时会有以下报错:
No provider for Http!
出现这个错误是因为 OAuthService
需要依赖 Angular 的 Http
模块,但是还没有将该模块导入到项目中。在 src/app/app.module.ts
中导入 HttpModule
。
import { HttpModule } from '@angular/http'; @NgModule({ ... imports: [ BrowserModule, HttpModule, IonicModule.forRoot(MyApp) ], ... })
现在登录页已经展示出来了。你可以使用 Chrome 的设备模式查看在 iPhone 6 上的效果。
为了解决缺少 TypeScript 支持的问题,你需要在 src/app/pages/login/login.ts
的顶部添加以下代码。
declare const OktaAuth: any;
TIP: 要了解更多关于在 TypeScript 项目引用外部 JavaScript 库的知识,可以阅读 Nic Raboy 写的关于这方面的文章。
在 src/app/pages/login/login.ts
中添加一个 login()
方法,它使用 Okta Auth SDK 进行: 1) 登录; 2) 将 session token 转换成 identity 和 access token。 一个 ID token 类似于身份证,它是标准的 JWT 格式,由 OpenID 提供者签名。Access tokens 是 OAuth 规范的一部分。一个 access token 可以是一个 JWT。它们用于访问被保护的资源,通常是在发送请求时将它们添加到 Authentication
请求头中。
login(): void { this.oauthService.createAndSaveNonce().then(nonce => { const authClient = new OktaAuth({ clientId: this.oauthService.clientId, redirectUri: this.oauthService.redirectUri, url: this.oauthService.issuer }); authClient.signIn({ username: this.username, password: this.password }).then((response) => { if (response.status === 'SUCCESS') { authClient.token.getWithoutPrompt({ nonce: nonce, responseType: ['id_token', 'token'], sessionToken: response.sessionToken, scopes: this.oauthService.scope.split(' ') }) .then((tokens) => { // oauthService.processIdToken doesn't set an access token // set it manually so oauthService.authorizationHeader() works localStorage.setItem('access_token', tokens[1].accessToken); this.oauthService.processIdToken(tokens[0].idToken, tokens[1].accessToken); this.navCtrl.push(TabsPage); }) .catch(error => console.error(error)); } else { throw new Error('We cannot handle the ' + response.status + ' status'); } }).fail((error) => { console.error(error); this.error = error.message; }); }); }
通过 identity token 你可以了解用户的更多信息。通过 access token 你可以访问需要 Bearer token 的受保护的 API。比如, 在 在 Angular PWA 中添加身份认证中,有一个 BeerService
,它用于在发送 API 请求时携带 access token 。
import { Injectable } from '@angular/core'; import { Http, Response, Headers, RequestOptions } from '@angular/http'; import 'rxjs/add/operator/map'; import { Observable } from 'rxjs'; import { OAuthService } from 'angular-oauth2-oidc'; @Injectable() export class BeerService { constructor(private http: Http, private oauthService: OAuthService) { } getAll(): Observable<any> { const headers: Headers = new Headers(); headers.append('Authorization', this.oauthService.authorizationHeader()); let options = new RequestOptions({ headers: headers }); return this.http.get('http://localhost:8080/good-beers', options) .map((response: Response) => response.json()); } }
您可以(可选)在表单上方添加图标来美化登录页。下载 这张图片,将它拷贝到 src/assets/image/okta.png
,在 login.html
的 <form>
标签中添加以下代码。
<ion-row> <ion-col text-center> <img src="assets/image/okta.png" width="300"> </ion-col> </ion-row>
当你尝试使用 Okta 的用户证书登录应用程序,你将在浏览器的控制台看到跨域报错。
XMLHttpRequest cannot load https://dev-158606.oktapreview.com/api/v1/authn. Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8100' is therefore not allowed access.
为了修复这一问题,在 Okta 修改 Trusted Origins (在 Security > API 下面), 将你的 client's URL 添加进去 (比如 http://localhost:8100
)。检查 CORS 和重定向的 origin 类型。
现在登录可以正常工作了,但是 UI 界面并没有提示。在首页的右上角添加一个 "Logout" 按钮。用以下 HTML 替换 src/pages/home/home.html
中的 <ion-header>
。
<ion-header> <ion-navbar> <ion-title>Home</ion-title> <ion-buttons end> <button ion-button (click)="logout()"> Logout </button> </ion-buttons> </ion-navbar> </ion-header>
在 src/pages/home/home.ts
中,添加一个 logout()
方法, 用于在 identity token 中获取姓名及 claims 。ID token 中的 claims 是关于颁发者、用户、目标受众、过期时间及颁发时间的信息。你可以阅读 OIDC 规范中的标准 claims。
import { Component } from '@angular/core'; import { NavController } from 'ionic-angular'; import { LoginPage } from '../login/login'; import { OAuthService } from 'angular-oauth2-oidc'; @Component({ selector: 'page-home', templateUrl: 'home.html' }) export class HomePage { constructor(public navCtrl: NavController, public oauthService: OAuthService) { } logout() { this.oauthService.logOut(); this.navCtrl.setRoot(LoginPage); this.navCtrl.popToRoot(); } get givenName() { const claims = this.oauthService.getIdentityClaims(); if (!claims) { return null; } return claims.name; } get claims() { return this.oauthService.getIdentityClaims(); } }
为了在 home 标签页上展示信息,将以下 HTML 添加到 src/app/home/home.html
文件的第二段之后。
<div *ngIf="givenName"> <hr> <p>You are logged in as: <b>{{ givenName }}</b></p> <div class="claims"> <strong>Claims from Identity Token JWT:</strong> <pre>{{claims | json}}</pre> </div> </div>
更新 src/app/home/home.scss
,添加一些 CSS 让原始的 JSON 看起来舒服一点。
page-home { .claims { pre { color: green; } } pre { border: 1px solid silver; background: #eee; padding: 10px; } }
现在登录之后你会看到你的姓名及声明信息。
你可以退出之后看一下带标识的登录页。
注意: 你可能注意到退出之后标签页并没有消失。我正在查找 没有正常工作 的原因。
使用 Ionic 在浏览器中开发移动应用是非常酷的事情。很高兴你能看到自己的劳动成果以及优秀的手机应用。但是它的外观和表现还不是原生应用。
为了查看应用程序在不同设备上的效果,你可以运行 ionic serve --lab
。--lab
标识会在浏览器中打开一个页面让你查看在不同设备中的效果。
LoginPage
在加载时会自动聚焦到 email
输入框。为了自动激活键盘,你需要告诉 Cordova 没有用户交互的情况下显示键盘是可以的。你可以在根路径的 config.xml
中添加以下代码。
<preference name="KeyboardDisplayRequiresUserAction" value="false" />
为了模拟或者部署到 iOS 设备上,你需要一个 Mac 以及一个新安装的 Xcode。如果你喜欢在 Windows 中创建 iOS 应用,Ionic 提供了一个 Ionic Package 服务。
确保打开 Xcode 完成安装 ,然后运行 ionic cordova emulate ios
在模拟器中打开应用。
可能会提示你安装 @ionic/cli-plugin-cordova
插件。当出现提示时输入 "y",按回车。
TIP: 我发现在模拟器中运行应用程序时的最大问题是键盘很难弹出。为了解决这一问题,当我需要在输入框输入文本时,我使用 Hardware > Keyboard > Toggle Software Keyboard 。
如果你在登录页输入凭证,可能什么也不会发生。打开 Safari 转到 Develop > Simulator > MyApp / Login,你会看到控制台有一条错误信息。如果你看不到开发菜单,重新执行 这篇文章 中的方法使其生效。
如果打开 Network 标签,你会看到只发送了一条请求 (to /authn
),它和在浏览器中发送的两条请求 (to /authn
and /authorize
) 有所不同。
我相信使用 Cordova 打包 app 之后并不会正常工作,因为通过内嵌的 iframe 向服务端发送请求,然后使用 postMessage 将结果返回当前窗口。Ionic/Cordova 似乎并不支持这种方式。为了解决这个问题,你可以使用 Cordova 提供的 in-app 浏览器直接与 Okta 的 OAuth 服务通信。Nic Raboy 演示了在 Facebook 中的操作方法,他在 Ionic 2 移动 App 中使用了 OAuth 2.0 服务。
使用以下命令安装 Cordova In-App Browser plugin :
ionic cordova plugin add cordova-plugin-inappbrowser
打开 src/app/pages/login/login.html
,用一个 <div>
包裹 <form>
,为了只在浏览器中运行时显示登录表单。添加一个新的 <div>
,它会在模拟器或设备上运行时显示。
<ion-content padding> <ion-row> <!-- optional logo --> </ion-row> <div showWhen="core"> <form> ... </form> </div> <div hideWhen="core"> <button ion-button full (click)="redirectLogin()">Login with Okta</button> </div> </ion-content>
打开 src/pages/login/login.ts
,在 imports 下面添加一个 window
的引用。
declare const window: any;
为了更容易的使用 OAuth 登录,可以添加以下方法。
redirectLogin() { this.oktaLogin().then(success => { localStorage.setItem('access_token', success.access_token); this.oauthService.processIdToken(success.id_token, success.access_token); this.navCtrl.push(TabsPage); }, (error) => { this.error = error; }); } oktaLogin(): Promise<any> { return this.oauthService.createAndSaveNonce().then(nonce => { let state: string = Math.floor(Math.random() * 1000000000).toString(); if (window.crypto) { const array = new Uint32Array(1); window.crypto.getRandomValues(array); state = array.join().toString(); } return new Promise((resolve, reject) => { const oauthUrl = this.buildOAuthUrl(state, nonce); const browser = window.cordova.InAppBrowser.open(oauthUrl, '_blank', 'location=no,clearsessioncache=yes,clearcache=yes'); browser.addEventListener('loadstart', (event) => { if ((event.url).indexOf('http://localhost:8100') === 0) { browser.removeEventListener('exit', () => {}); browser.close(); const responseParameters = ((event.url).split('#')[1]).split('&'); const parsedResponse = {}; for (let i = 0; i < responseParameters.length; i++) { parsedResponse[responseParameters[i].split('=')[0]] = responseParameters[i].split('=')[1]; } const defaultError = 'Problem authenticating with Okta'; if (parsedResponse['state'] !== state) { reject(defaultError); } else if (parsedResponse['access_token'] !== undefined && parsedResponse['access_token'] !== null) { resolve(parsedResponse); } else { reject(defaultError); } } }); browser.addEventListener('exit', function (event) { reject('The Okta sign in flow was canceled'); }); }); }); } buildOAuthUrl(state, nonce): string { return this.oauthService.issuer + '/oauth2/v1/authorize?' + 'client_id=' + this.oauthService.clientId + '&' + 'redirect_uri=' + this.oauthService.redirectUri + '&' + 'response_type=id_token%20token&' + 'scope=' + encodeURI(this.oauthService.scope) + '&' + 'state=' + state + '&nonce=' + nonce; }
把在构造器中设置的 redirectUri
替换成硬编码 http://localhost:8100
。如果省略这一步,当 app 在设备上运行时, window.location.origin
会跳转到 file://
。为了将它设置成已知的 URL,我们可以通过 in-app browser 的 "loadstart" 事件查找它。
constructor(private navCtrl: NavController, private oauthService: OAuthService) { oauthService.redirectUri = 'http://localhost:8100'; ... }
更改之后,需要将 app 重新部署到手机上。
ionic cordova emulate ios
现在可以点击 "Login with Okta" 按钮,然后输入合法的凭证进行登录。
使用这项技术的好处就是 Okta 的登录页具有“记住我”和“忘记密码”的功能,所以不需要自己编写代码。
为了将 app 部署到 iPhone,首先将手机插到电脑上。然后运行以下命令安装 ios-deploy、构建 app 并在你的设备上运行。
npm install -g ios-deploy ionic cordova run ios
如果你之前没有为应用程序设置代码签名,则此命令可能会失败。
Signing for "MyApp" requires a development team. Select a development team in the project editor. Code signing is required for product type 'Application' in SDK 'iOS 10.3'
在 Xcode 中打开你的项目,运行以下命令。
open platforms/ios/MyApp.xcodeproj
Ionic's 开发文档 有解决这一问题的说明。
选择你的手机作为 Xcode 的目标,然后点击 play 按钮运行 app。如果你是第一次做,Xcode 可能会加载一段时间,上方会显示一条 "Processing symbol files" 的信息。
只要你已经设置了你的手机、电脑以及 Apple ID,你就可以打开应用并登录。以下是在我的手机上的展示效果。
为了模拟或者部署到 Android 设备上,你首先要安装 Android Studio。在安装过程中,它会提示你将 Android SDK 安装到哪里。将这个路径设置为 ANDROID_HOME 的环境变量。在 Mac 上,it should be ~/Library/Android/sdk/
。
如果你已经安装了Android Studio,请确保打开它以完成安装。
为了部署到 Android 模拟器,运行 ionic cordova emulate android
。这个命令将安装 Android 支持并打印关于如何创建模拟图像的说明。
Error: No emulator images (avds) found. 1. Download desired System Image by running: /Users/mraible/Library/Android/sdk/tools/android sdk 2. Create an AVD by running: /Users/mraible/Library/Android/sdk/tools/android avd HINT: For a faster emulator, use an Intel System Image and install the HAXM device driver
运行第一条建议并下载您想要的系统映像。然后运行第二个命令并用以下设置创建一个 AVD(Android 虚拟设备):
AVD Name: TestPhone Device: Nexus 5 Target: Android 7.1.1 CPU/ABI: Google APIs Intel Axom (x86_64) Skin: Skin with dynamic hardware controls
警告: 这些设置不适用于 Mac 上的 Android Studio 2.3.2 版本。当你尝试运行第一条命令时,它会显示以下内容:
************************************************************************* The "android" command is deprecated. For manual SDK, AVD, and project management, please use Android Studio. For command-line tools, use tools/bin/sdkmanager and tools/bin/avdmanager *************************************************************************
为了解决这个问题,打开 Android Studio,选择 "Open an existing Android Studio project",然后选择 ionic-auth/platforms/android
的路径。如果提示升级,选择 "OK",然后继续创建一个新的 AVD ,和 Android Studio 文档描述的那样.
执行完这些步骤之后,你可以运行 ionic cordova emulate android
查看运行在 AVD 中的 app。
注意: 如果应用程序显示错误 "连接服务器失败 (file:///android/www/index.html
)",在 config.xml
中添加以下代码。这行代码将默认超时时间设置为 60 秒 (默认 20)。感谢 Stack Overflow 社区 对此问题的解答。
<preference name="loadUrlTimeoutValue" value="60000"/>
Ionic 支持创建 progressive web apps (PWAs)。这意味着你可以将 Ionic app 部署成 web app (不是移动端 app) ,它可以在离线的 支持 service workers 的浏览器 中运行。
想要了解如何使用 service workers 并把 app 转换成 PWA ,可以阅读 如何使用 Ionic 和 Spring Boot 开发移动应用 的 PWAs 部分 。PWA 是可以安装在系统中的 web 应用程序。它可以在离线情况下工作,使用的是你最后一次与 app 交互的数据缓存。添加 PWA 功能可以让 app 加载更快,提供更好的用户体验。想要了解更多关于 PWA 的知识,可以阅读 The Ultimate Guide to Progressive Web Applications.
我希望你喜欢这篇关于 Ionic、Angular 及 Okta 的教程。我喜欢 Ionic 是因为它可以将你的 web 开发技能提升一个档次,并且它可以快速创建仿原生的移动应用。
你可以在 GitHub 上查看本教程的完整代码。如果你有问题,可以通过 Twitter @mraible 或者在 Okta's Developer Forums 上联系我。
想要了解更多关于 Ionic、Angular 或者 Okta 的知识,可以查看以下资源: