构建具有用户身份认证的 Ionic 应用
序言:本文主要介绍了使用 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?
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
为了设置 Ionic 的开发环境,需要完成以下几步:
- 安装 Node.js
- 使用 npm 安装 Ionic 和 Cordova:
npm install -g cordova ionic
创建一个 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
依赖中提供的类创建身份认证。它也支持 自定义身份认证,但是 "需要你自己的服务器处理身份认证"。
目前还没有太多关于这方面的教程,不过从去年开始有了一些。
- Simon Reimler's Simple Ionic 2 Login with Angular 2
- Raymond Camden's An example of the Ionic Auth service with Ionic 2
- Josh Morony's Using JSON Web Tokens (JWT) for Custom Authentication in Ionic 2: Part 2
你可能注意到所有的教程都需要很多的代码。另外,关于如何在后端的 Auth 服务中验证用户身份的文档也不多。
在 Okta 中创建 OpenID Connect 应用
OpenID Connect (OIDC) 基于 OAuth 2.0 协议。它允许客户端验证用户的身份并获得他们的基本配置文件信息。为了将 Okta 的身份认证平台整合到用户身份认证中,需要以下步骤:
- 注册 并创建一个 OIDC 应用
- 登录 Okta 账户,然后导航到 Admin > Add Applications 并点击 Create New App
- 选择 Single Page App (SPA) 以及 OpenID Connect 作为登录方式
- 点击 Create 并给你的应用起个名字 (比如 "Ionic OIDC")
- 在下一页上,添加
http://localhost:8100
作为重定向的 URI 并点击 Finish。你会看到以下设置信息:
- 点击 Assignments 标签,然后选择 Assign > Assign to People
- 给自己分配一个用户,或者其它你授权的人。
创建登录页
为了创建身份认证的登录页,先创建 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
为了模拟或者部署到 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 设备上,你首先要安装 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 开发 PWAs
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 的知识,可以查看以下资源: