本文将简单介绍开发从自有App跳转到Alexa App账号下获取技能的应用账号登录链接服务。
背景信息
Alexa音箱要使用Skill控制自有App下的设备,需要从自有App跳转登录到Amazon Alexa语音平台上,可以通过开发一个App Account Linking,用于直接登录Alexa账号的服务来获取并可以开启Alexa账号下的Skill,该服务避免用户手动进入Alexa App内查找到Skill,再进行点击输入账号密码得操作。
概述
通过开发一个App to App Account Linking服务实现从自有App中直接登录Alexa应用账号获取该账号下的Skill,可以有以下两种打开链接的方式:
从iOS和Android应用程序启动Alexa App。
使用Amazon Web登录,并作为iOS和Android的后备方案。
App to App Account Linking允许用户从自有App或网站开始,将其Alexa用户身份信息与另一服务中的身份信息进行链接。当您从自有App启动App to App Account Linking时,您的用户可以:
通过自有App发现你的Alexa技能。
从自有App中启动技能并和帐户链接。
当他们在移动设备上登录自有App和Alexa App两个应用程序时,无需输入他们的帐户密码即可直接链接他们的帐户。
当Alexa App未安装在他们的移动设备上时,可以使用Login with Amazon(LWA)从自有App链接他们的帐户,详见Login with Amazon(LWA)。
实现App to App Account Linking的其他工作流程还有以下两种情况:
从Alexa App开始的App to App Account Linking:用户通过从Alexa App而不是自有App开始链接其帐户。有关详细信息,请参阅App-to-App Account Linking (Starting From the Alexa App)。
仅使用Alexa App(App内打开浏览器):用户完全在Alexa App内完成帐户链接,这是最常见的流程。有关详细信息,请参见Add Account Linking to Your Alexa Skill。
如果您有一个应用程序或网站,我们建议您在Alexa App(浏览器)之外实施一个App to App Account Linking服务,即本文所指的App to App Account Linking。因此在本文中,App to App Account Linking专指从自有App开始的App to App Account Linking。
开发对象
Alexa App的工作流程可用于iOS和Android。因此,您实施的流程取决于您是在开发iOS应用程序、Android应用程序还是网站:
iOS和Android
将Alexa App流作为主要流,将LWA流作为未安装Alexa App时的备用流。
网站(Websites)
实施LWA流程。
使用流程
在使用App to App Account Linking期间,用户将执行以下工作流程。
您的自有App为用户提供了启用您的技能并将其帐户与Alexa链接的选项。如果用户选择链接其帐户,出现以下两种情况之一,具体取决于移动设备上是否安装了Alexa App。
Alexa App
如果设备上安装了Alexa App,Alexa App将启动并要求用户确认帐户链接请求。确认请求后,用户将返回到您的应用程序。
LWA
如果设备上未安装Alexa App,将打开一个带有LWA的浏览器窗口,用户可以输入其Amazon账号信息或创建Amazon帐户。然后要求用户授予您链接帐户的技能权限。确认请求后,用户将返回到自有App中。
在链接帐户并且用户使用您的技能后,该技能使用与仅使用Alexa App有相同的工作流程,详见Alexa App only。在任何情况下,禁用该技能都会导致帐户取消链接。
以下是App to App Account Linking的示例屏幕截图。如上所述,App to App Account Linking工作流程取决于用户是否安装了Alexa App。
流程设计
App to App Account Linking通过使用OAuth 2.0工作。
底层交互流程介绍:
用户在其移动设备上安装自有App,并登录到自有App。
您的自有App向用户提供了启用您的技能并将他们的帐户与Alexa链接的选项,并提供示例信息(例如,"您现在可以通过Alexa语音通过云智能App订购智能灯泡")。用户确认链接请求。
接下来会发生什么取决于用户设备上是否安装了Alexa App。
如果安装了Alexa App:
在自有App中使用Alexa App URL和授权请求参数启动Alexa App。
Alexa App启动并询问用户是否希望将Alexa链接到您的服务。
用户确认链接请求。
Alexa App使用redirect URL将用户返回自有App中,并将用户的Amazon授权代码作为redirect URL的一部分发送。
如果未安装Alexa App:
自有App使用应用程序内置浏览器页签(非本机内浏览器App)中的LWA fallback URL启动LWA,并使用相关授权请求参数。
LWA启动并要求用户登录他们的Amazon帐户。
LWA询问用户是否希望将Alexa链接到您的服务。
用户确认链接请求。
LWA使用您的redirect URL将用户发送回您的应用程序,并将用户的Amazon授权码作为redirect URL的一部分发送。
您的后端服务器调用LWA授权服务URL,并将在上一步中检索到的Amazon授权代码交换为Amazon访问令牌。
您的后端服务器调用您的授权服务器以获取用户的授权代码(用于他们在您的服务中的帐户)。
后端服务器使用用户的Amazon访问令牌和用户的服务授权代码调用Alexa Skill激活API,以启用技能并链接帐户。
Alexa转到自有App的Access token URL,将您的服务的用户授权码交换为您的服务的访问令牌,从而完成App to App Account Linking。
开发步骤介绍
建议开发使用以下库和语言提供iOS应用(10.0或更高版本)和Android应用的代码示例:
iOS app
Android app
Backend server
在自有App中显示帐户链接选项。
自有App需要有一个启动页面,用户可以从中启动技能启用和帐户链接。此页面应显示用户可以通过将其帐户链接到Alexa来做什么,例如:通过语音订购智能彩灯。您允许用户启动帐户链接的方式取决于您希望为用户创建的体验。您可以在多个位置向用户提供帐户链接:例如,您可以在用户注册自有App账号后为其提供启用帐户链接的选项,您可以在App内设置页面的"链接到Alexa"页面上为其提供按钮,等等。在任何情况下,请务必解释与Alexa链接帐户的方便有效之处。
将App to App Account Linking按钮添加到iOS应用内,请执行以下操作:
以使用Xcode IDE为例,打开storyboard。
将UIbutton添加到视图控制器,并将其命名为"Link your account with Alexa"。考虑使用一个名字来表示用户通过链接他们的帐户将得到的好处,例如"用Alexa听你的音乐","订购一个懂你的智能烤箱"。
在Xcode IDE的左上角,单击"Show the assistant editor"。
通过将按钮拖动到代码编辑器,为按钮创建操作连接。
将App to App Account Linking按钮添加到Android App,请执行以下操作:
在Android Studio中,在Design Mode下,打开要添加App to App Account Linking按钮的页面布局。
从选项板中,将新按钮拖放到布局中。
为按钮定义一个ID。
在页面的代码中,初始化App to App Account Linking按钮,并定义其
onClick
侦听器行为用以启动,如下例所示。class AppToAppFragment : Fragment() { private lateinit var appToAppButton: Button override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? ): View? { val root = inflater.inflate(R.layout.appToAppPage, container, false) initComponents(root) appToApp.setOnClickListener { doAppToApp() } } private fun initComponents(root: View) { appToAppButton = root.findViewById(R.id.appToAppAccountLinkingButton) } }
为自有App启用Universal Link(iOS)或App Link(Android)。要允许Alexa App将用户重新跳转向到自有App,您必须为iOS应用程序启用Universal Link或为Android应用程序启用App Link。
说明Universal Link和App Link必须符合指定的URI语法。记住您添加的域和路径,因为在Alexa开发者控制台中将redirect URL添加到Skill详细信息时需要它们。
要启用Universal Link,请查看iOS链接开发文档中的示例及说明进行操作。
要启用Android App Link,请查看Android中关于处理Android App Link的说明进行操作。相关示例,请参阅Android文档中的创建应用程序内容的深度链接和验证应用程序链接。
配置使用App to App Account Linking的Skill。
您可以使用Alexa Developer console(Alexa开发者控制台)、Alexa Skills Kit命令行界面(ASK CLI)或Alexa skill Management API(SMAPI)来配置您的Skill(技能)。在此配置过程中,您可以指定自有App的redirect URL、access token URL等内容。
使用Alexa Developer console配置。
登录Alexa开发者控制台。
在列表中找到你的Skill。在你的Skill下操作,选择编辑。
在左侧,单击
TOOLS
,然后单击Account Link
。如果您以前为您的Skill配置了Account Link,请启用"Allow users to link their account to your skill from within your application or website"。
对于授权授予类型,如果尚未选择授权代码授予,请选择授权代码授予。App to App Account Linking仅支持授权代码授予。
对于其他设置,请使用配置Account Link中描述的设置,并在填写名为"redirect URL"的字段时注意以下附加说明:
对于redirect URL,请添加URL和相关参数说明中描述的redirect URL。
redirect URL必须符合指定的URI语法。Alexa App将打开此Universal Link和App Link URL以启动自有App。
要处理不同的场景,可以添加多个redirect URL。
如果您的App不支持Universal Link和App Link,你应该有一个redirect URL指向的有效网页。这应该支持相同的过程来接收用户的Amazon授权代码,如上流程设计所述。
请记住为iOS应用程序、Android应用程序和网站添加redirect URL。
使用ASK CLI配置。
要使用ASK CLI启用App to App Account Linking,请根据以下步骤使用更新帐户链接信息中的子命令:
允许用户在没有帐户链接的情况下启用技能选择
Y
或N
。Authorization URL输入授权服务器的URL,该URL用于常规帐户链接。授权服务器必须接受用户的凭据,对用户进行身份验证,并生成一个授权代码,Alexa App稍后可以传递给您的授权服务器,以检索一个访问令牌,该令牌将用户与您的服务唯一标识。
Client ID客户端ID,在您的登录页面输入,将用来识别请求来自您的技能的标识符。
Scopes(用逗号分隔)输入表示用户帐户所需的访问权限的字符,例如用户ID。对于智能家居技能,这个是必填项。您最多可以指定15个
scopes
。Domains(用逗号分隔)输入登录页面从中获取内容的其他域列表。您最多可以指定15个域。
授权授予类型选择
AUTH_CODE
。访问令牌URI输入访问令牌URL,详见URL和相关参数说明。
Client Secret输入一个凭据,允许Alexa服务使用访问令牌URI进行身份验证。这与Client ID(客户端ID)相结合,以识别来自Alexa的请求。
客户端身份验证方案选择
HTTP_BASIC
或REQUEST_BODY_CREDENTIALS
。(可选)默认访问令牌到期时间(以秒为单位)输入访问令牌有效的时间(以秒为单位)。
(可选)交互访问令牌URL输入URI,该URI将使用授权代码调用,这些授权代码可以交换Alexa访问令牌。
(可选)App to App Account Linking的redirect URL输入URL和相关参数说明中描述的App redirect URL,App to App Account Linking需要此设置。
使用Alexa skill Management API(SMAPI)配置。
要使用Alexa skill Management API(SMAPI)来配置App to App Account Linking,请根据此Account linking request进行结构设置。
以下是使用SMAPI添加redirect URL的请求示例。
名称
描述
类型
skipOnEnablement
设置为true,允许用户在不启动帐户链接流的情况下启用该技能。当用户启用该技能时,设置为false,以要求正常的帐户链接流。有关更多信息,请参阅让用户在不链接账户的情况下启用您的技能。
Boolean
type
指定OAuth授权授予类型。对于App to App Account Linking,您必须使用
AUTH_CODE
。String
authorizationUrl
您的授权服务器的URL,该URL必须接受用户的凭据,对用户进行身份验证,并生成授权代码,Alexa App稍后可以传递给您的授权服务器,以检索唯一标识用户与您的服务的访问令牌。
String
domains
您的登录页面从中获取内容的其他域列表。您最多可以指定15个域。
Array of String
clientId
您的登录页面用于识别请求来自您的技能的标识符。
String
scopes
字符串,指示用户帐户所需的访问权限,例如
user_id
。对于智能家居技能,这个领域是必填项。您最多可以指定15个scopes
。Array of String
accessTokenUrl
用于请求授权令牌的URI。仅当
type
是AUTH_CODE
时才需要。String
clientSecret
您提供的凭据,允许Alexa服务使用访问令牌URI进行身份验证。这与
clientId
相结合,以识别来自Alexa的请求。String
accessTokenScheme
使用的身份验证类型。例如
HTTP_BASIC
,或REQUEST_BODY_CREDENTIALS
。对于App to App Account Linking,这是必需的,因为授权授予类型是AUTH_CODE
。String
redirectUrls
用Universal Link或App Link来启动自有App,redirectURL必须符合指定的URI语法。
Array of String
{ "accountLinkingRequest": { "accessTokenScheme": "HTTP_BASIC", "accessTokenUrl": "https://api.amazon.com/auth/o2/token/", "authorizationUrl": "https://www.amazon.com/ap/oa/", "clientId": "yourSMAPIClientId", "clientSecret": "yourSMAPIClientSecret", "domains": [], "redirectUrls": ["yourRedirectURL1","yourRedirectURL2"], "scopes": ["profile"], "skipOnEnablement": true, "type": "AUTH_CODE" } }
获取用户的Amazon授权码。
自有App中需要获取用户的Amazon授权码,以便以后可以将其交换为Amazon访问令牌。Amazon访问令牌将使自有App能够启用技能并完成帐户链接。Amazon授权码的有效期为5分钟。
通过打开Alexa App URL(对于Alexa App流)或向LWA授权服务器(对于LWA流)提出
HTTP GET
请求,您可以获得用户的Amazon授权代码。对于iOS和Android,我们强烈建议您实施Alexa App流,并仅将LWA用作后备。要获取用户的Amazon授权代码,您需要组装授权请求,然后打开Alexa App URL(如果没有安装Alexa App,则打开LWA fallback URL),并附上URL和相关参数说明中描述的参数。我们强烈建议您在后端服务器中组装这些URL,并将组装的URL传递给您的应用程序。这样,您就可以快速更改参数(如阶段),而无需重建应用程序。
当用户确认链接请求时,Alexa App会将用户重新定向到应用程序的redirect URL,这是Alexa或LWA fallback URL中的一个参数。redirect URL包括接下来描述的成功或错误参数。如果用户的设备无法使用您提供的redirect URL启动您的应用程序,用户将被发送到其默认浏览器中的同一redirect URL。
例如,当以下情况下,可能会发生这种情况:
您的应用程序中没有启用Universal Link或App Link。
您的应用程序中没有为您提供的redirect URL启用Universal Link或App Link。
用户设备上的应用程序版本是不支持Universal Link或App Link的旧版本。
在这些情况下,您应该有一个网页(与Universal Link或App Link的URL相同)来接收授权代码。当用户进入您的网页时,他们可能需要登录,以便您可以从您的服务中获得授权代码。
有关可能出现的错误和要向用户显示的相关消息,请参阅获取Amazon授权代码时的错误。
Alexa AppURL的HTTP GET请求的成功响应如果成功,响应是一个redirect URL,其中包括用户的Amazon授权代码和接下来显示的其他参数。要处理这个问题,请参阅苹果关于支持应用程序中通用链接的准则和处理安卓应用程序链接的安卓指南。
// Example success response from the Alexa app
https://yourRedirectURL?code={Amazon Authorization code}&state={your state}
// Example success response from LWA
https://yourRedirectURL?code={Amazon Authorization code}&scope={permission scope}&state={your state}
地址
code:Alexa App或LWA返回的Amazon授权代码。
state:您在授权请求中传递的状态。您应该验证状态,以避免跨站点请求伪造。
scope:权限范围。LWA returnalexaalexa::skills:account_linking scope。Alexa App不返回范围参数。
有关OAuth2.0授权响应的更多详细信息,请参阅授权响应。
Alexa AppURL的HTTP GET请求的错误响应
如果出现错误,响应是redirect URL,接下来显示错误和其他参数。您的应用程序必须显示适当的错误信息(错误页面、UIAlertController等)。有关要显示的错误消息,请参阅获取Amazon授权代码时的错误码。
// Example error response
HTTP 200
https://yourRedirectURL?error_description={Error description}&state={your state}&error={OAuth Error Code}
补充说明
error:单个ASCII错误代码。可能的值:invalid_request、unauthorized_client、access_denied、unsupported_response_type、invalid_scope、server_error、temporarily_unavailable。
error_description:错误描述。
有关OAuth2.0错误响应的更多详细信息,请参阅错误响应。
iOS 示例
示例:获取URL(iOS)
在本例中,该应用程序同时获得Alexa AppURL和LWA fallback URL。
import Alamofire
import SwiftyJSON
class AlamofireHelper {
// Your backend base URL from which you're requesting the URL
static let BASE_URL = "https://exampleBackEndURL"
/*! Get Alexa app Universal Link and LWA fallback URL
@param userAccessToken for your backend server
@param state maintained between client and server, caller should generate it
*/
static func getAlexaAppUrl(userAccessToken:String, state: String) -> DataRequest {
let alexaAppUrl = "/alexaurl"
let inputstate = "?state=" + state
var urlRequest = URLRequest(url: URL(string: BASE_URL + alexaAppUrl + inputstate)!)
urlRequest.addValue("Bearer " + userAccessToken, forHTTPHeaderField: "Authorization")
return Alamofire.request(urlRequest)
}
private init() {}
}
// Call to your backend to get the authorization request URLs (Alexa app / LWA)
AlamofireHelper.getAlexaAppUrl(userAccessToken: savedSession.accessToken, state: StateHelper.generateState()).responseJSON {
response in
guard let status = response.response?.statusCode, 200..<300 ~= status else {
self.createAlert(title: "Failed to get Alexa URL", message: "Failed to get Alexa URL")
return
}
guard let responseValue = response.result.value else {
self.createAlert(title: "Invalid response", message: "Invalid response")
return
}
let reponseInJSON = JSON(responseValue)
guard let companionApp = reponseInJSON["companionAppURL"].string,let lwaFallback = reponseInJSON["LWAFallBackURL"].string else {
self.createAlert(title: "Error response", message: "Error response")
return
}
guard let companionAppURL = URL(string: companionApp), let lwaFallbackURL = URL(string: lwaFallback) else {
self.createAlert(title: "Incorrect URL format", message: "Incorrect URL format")
return
}
// The openUniversalLinks code snippet can be found in another example
self.openUniversalLinks(companionAppURL: companionAppURL, lwaFallbackURL: lwaFallbackURL)
}
示例:打开URL(iOS)
在本例中,该应用程序将打开Alexa AppURL,如果没有安装Alexa App,则打开LWA fallback URL。
注意以下内容:
本示例使用UIApplication.shared.openAPI使用universalLinksOnly选项打开URL。
只有当应用程序(此处为Alexa App)配置为使用universalLinksOnly选项处理通用链接时,URL才会打开。因此,您可以通过关闭表达式
completionHandler
处理Alexa App无法启动的情况,关闭表达式请参阅Closure Expressions(例如,Alexa App没有安装或版本不支持通用链接)。
如果您的应用程序在使用universalLinksOnly选项时无法启动Alexa App,您的应用程序需要使用LWA获取Amazon授权代码。您可以在iOS 11上使用SFAuthenticationSession或iOS 12.0或更高版本上的ASWebAuthenticationSession。这将获得Safari用户会话和cookie,当用户已经在Safari上登录Amazon.com时,这避免了额外的登录。这将导致您的应用程序显示一个窗口,要求用户同意共享网站信息。该消息的内容由iOS定义。
对于低于11.0的iOS版本,此示例使用SFSafariViewController将用户发送到LWA页面。SFSafariViewController不要求用户同意共享网站信息;因此,Safari浏览器不会与SFSafariViewController共享cookie。
import SafariServices
// Open the Alexa URL with option universalLinksOnly
// If it doesn't open the Universal Link, open the LWA fallback
private func openUniversalLinks(companionAppURL: URL, lwaFallbackURL: URL) {
UIApplication.shared.open(companionAppURL, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly:true]) {
companionAppLaunched in
if !companionAppLaunched {
if #available(iOS 12.0, *) {
WebSession.initWebAuthenticationSession(authURL: lwaFallbackURL)
} else if #available(iOS 11.0, *) {
WebSession.initAuthenticationSession(authURL: lwaFallbackURL)
} else {
let safariViewController = SFSafariViewController(url: lwaFallbackURL)
self.present(safariViewController, animated: true, completion: nil)}
}
}
}
// Web session
class WebSession {
@available(iOS, introduced: 11.0, deprecated: 12.0)
private static var authenticationSession: SFAuthenticationSession?
@available(iOS 12.0, *)
private static var webAuthenticationSession : ASWebAuthenticationSession?
private static let CALLBACK_URL_SCHEME = "https://yourAppsUniversalLinkURL"
@available(iOS, introduced: 11.0, deprecated: 12.0)
static func initAuthenticationSession(authURL:URL) {
if let session = authenticationSession {
session.cancel()
}
self.authenticationSession = SFAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
(callBack:URL?, error:Error?) in
//Callback and error handling
})
self.authenticationSession?.start()
}
@available(iOS 12.0, *)
static func initWebAuthenticationSession(authURL:URL) {
if let session = webAuthenticationSession {
session.cancel()
}
self.webAuthenticationSession = ASWebAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{
(callBack:URL?, error:Error?) in
// Callback and error handling
})
self.webAuthenticationSession?.start()
}
private init(){}
}
示例:打开LWA URL(iOS)
在网站的情况下打开LWA URL,作为iOS设备上没有安装Alexa App的fallback。
在本例中,该应用程序在视图控制器中打开LWA。
import SafariServices
// Open Login with Amazon in Safari View controller
private func openSafariView(lwaFallbackURL: URL) {
let safariViewController = SFSafariViewController(url: lwaFallbackURL)
self.present(safariViewController, animated: true, completion: nil)}
}
示例:处理通用链接响应(iOS)
在本例中,该应用程序验证并提取Alexa App对其通用链接的调用中的参数。
// Handler for Universal Links in AppDelegate
// Add this method to handle incoming Universal Links
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let incomingURL = userActivity.webpageURL else {
return false
}
let handlers : [UniversalLinkHandler] = [AuthResponseHandler(), ErrorResponseHandler()]
var validatedResponse : ValidatedResponse?
for handler in handlers {
if handler.canHandle(incomingURL: incomingURL) {
validatedResponse = handler.getValidatedResponse()
let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController
initialViewController.response = validatedResponse
self.window?.rootViewController = initialViewController
self.window?.makeKeyAndVisible()
return true
}
}
return false
}
// Incoming Universal Links handler protocol
protocol UniversalLinkHandler {
//Validate the incoming URL scheme
func canHandle(incomingURL:URL)-> Bool
//Get the validated response
func getValidatedResponse() -> ValidatedResponse?
}
extension UniversalLinkHandler {
func validateState(state:String)->Bool {
return StateHelper.validateState(state: state)
}
}
// Successfully receiving the Amazon Authorization code
class AuthResponseHandler : UniversalLinkHandler {
private let AUTH_RESPONSE_PARAMETERS: Set = ["code", "state", "scope"]
private var validatedResponse: ValidatedResponse?
func canHandle(incomingURL: URL) -> Bool {
guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let queryParameters = components.queryItems else {
return false
}
var validatedParameters : [String: String] = [:]
for queryParameter in queryParameters {
//duplicated parameters in URL
if validatedParameters.keys.contains(queryParameter.name) {
return false
}
//Unrecognized parameters in URL
if !AUTH_RESPONSE_PARAMETERS.contains(queryParameter.name) {
return false
}
validatedParameters[queryParameter.name] = queryParameter.value
}
guard let code = validatedParameters["code"], let state = validatedParameters["state"] else {
return false
}
//validated the state
if !validateState(state: state) {
self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
return true
}
self.validatedResponse = AuthResponse(url: incomingURL,code: code, state: state, scope: validatedParameters["scope"])
return true
}
func getValidatedResponse() -> ValidatedResponse? {
return validatedResponse
}
}
// Error authorization response handler
class ErrorResponseHandler : UniversalLinkHandler {
private let ERROR_RESPONSE_PARAMETERS: Set = ["error", "error_description", "state"]
private var validatedResponse: ValidatedResponse?
func canHandle(incomingURL: URL) -> Bool {
guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true),
let queryParameters = components.queryItems else {
return false
}
// Validate all query parameters
var validatedParameters : [String: String] = [:]
for queryParameter in queryParameters {
// Duplicated parameters in URL
if validatedParameters.keys.contains(queryParameter.name) {
return false
}
// Unrecognized parameters in URL
if !ERROR_RESPONSE_PARAMETERS.contains(queryParameter.name) {
return false
}
validatedParameters[queryParameter.name] = queryParameter.value
}
guard let error = validatedParameters["error"], let errorDescription = validatedParameters["error_description"], let state = validatedParameters["state"] else {
return false
}
// validate the state
if !validateState(state: state) {
self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state")
return true
}
self.validatedResponse = ErrorResponse(url: incomingURL, error: error, errorDescription: errorDescription)
return true
}
func getValidatedResponse() -> ValidatedResponse? {
return validatedResponse
}
}
// Validated response
class ValidatedResponse {
// Universal Link URL that launched the App
private(set) var url: URL
init (url: URL) {
self.url = url
}
}
// Amazon authorization code response per the OAuth2 specification
class AuthResponse : ValidatedResponse{
private(set) var code : String
private(set) var state: String
//scope will be returned in Login with Amazon, will not be present in Alexa Companion flow
private(set) var scope: String?
init(url:URL, code:String, state:String, scope: String?) {
self.code = code
self.state = state
self.scope = scope
super.init(url:url)
}
}
// Error response per the OAuth2 specification
class ErrorResponse : ValidatedResponse {
private(set) var error : String
private(set) var errorDescription: String
init(url: URL, error:String, errorDescription:String) {
self.error = error
self.errorDescription = errorDescription
super.init(url: url)
}
}
Android示例
示例:获取URL(安卓)
向您的后端服务提出请求,以获取Alexa App URL和LWA fallback URL。您可以以任何您喜欢的方式实现此请求,例如使用RetroFit或任何其他Android库。
以下代码是一个简单的例子。
fun doAppToApp(){
val appToAppUrls: AppToAppUrls = yourBackendService.getAppToAppUrls();
// This function is shown in a different example
openAlexaAppToAppUrl(appToAppUrls.alexaAppUrl, appToAppUrls.lwaFallBackUrl)
}
data class AppToAppUrls(
@SerializedName("alexaAppUrl")
val alexaAppUrl: String,
@SerializedName("lwaFallBackUrl")
val lwaFallBackUrl : String
)
interface BackendService {
fun getAppToAppUrls() : AppToAppUrls
}
示例:打开URL(Android)
在本例中,该应用程序将打开Alexa AppURL,如果没有安装Alexa App,则打开LWA fallback URL。
注意以下内容:
本示例使用PackageManager验证Alexa App是否已安装,并验证已安装的Alexa App版本是否可以处理App to App Account Linking。
如果Alexa App没有安装或无法处理App to App Account Linking,您的应用程序必须使用LWA流程获得Amazon授权代码。有关LWA fallback URL格式,请参阅下文URL和相关参数说明。
此示例使用AndroidACTIONACTION_VIEW意图打开URL。应用程序到应用程序的URL将在Alexa App中打开,LWA fallback URL将在用户的默认浏览器中打开。
private fun openAlexaAppToAppUrl(alexaAppUrl: String, lwaFallbackUrl: String){
if (AlexaAppUtil.isAlexaAppSupportAppLink(fragmentContext!!)) {
val alexaAppToAppIntent = getAppToAppIntent(alexaAppUrl);
startActivity(alexaAppToAppIntent)
} else {
val lwaAppToAppIntent = getAppToAppIntent(lwaFallbackUrl);
startActivity(lwaAppToAppIntent)
}
}
private fun getAppToAppIntent(appToAppUrl: String): Intent {
return Intent(Intent.ACTION_VIEW, Uri.parse(appToAppUrl))
}
/**
* Utility to check if the Alexa app is installed and supports app-to-app.
*/
object AlexaAppUtil {
private const val ALEXA_PACKAGE_NAME = "com.amazon.dee.app"
private const val ALEXA_APP_TARGET_ACTIVITY_NAME = "com.amazon.dee.app.ui.main.MainActivity"
private const val REQUIRED_MINIMUM_VERSION_CODE = 866607211
/**
* Check if the Alexa app is installed and supports App Links.
*
* @param context Application context.
*/
@JvmStatic
fun doesAlexaAppSupportAppToApp(context: Context): Boolean {
try {
val packageManager: PackageManager = context.packageManager
val packageInfo = packageManager.getPackageInfo(ALEXA_PACKAGE_NAME, 0)
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
packageInfo.longVersionCode > REQUIRED_MINIMUM_VERSION_CODE
} else {
packageInfo != null
}
} catch (e: PackageManager.NameNotFoundException) {
// The Alexa App is not installed
return false
}
}
}
示例:处理应用程序链接响应(Android)
在本例中,该应用程序验证并提取Alexa App对其应用程序链接的调用中的参数。
/**
* App-to-app result page landing activity.
*/
class AppToAppResultPageActivity : AppCompatActivity() {
private lateinit var statusText: TextView
private lateinit var goBackButton: Button
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val appLinkData = intent.data
setContentView(R.layout.app_to_app_result)
initializeComponents()
statusText.text = "Linking your account"
// Send the data returned from the Alexa App / LWA to your backend
// to call the Skill Activation API to link the accounts
val accountLinking = linkAccountInBackend(appLinkData)
displayAccountLinkingStatus(result);
}
private fun initializeComponents() {
statusText = findViewById(R.id.text_status)
goBackButton = findViewById(R.id.btn_goback)
goBackButton.setOnClickListener {
val intent = Intent(this, MainActivity::class.java)
startActivity(intent)
}
}
}
状态生成和验证
示例:State Helper
在本例中,应用程序生成并验证请求和响应之间的状态。您必须使用状态验证传入的请求,以防止跨站点请求伪造,请参阅Cross-Site Request Forgery。
import Foundation
import Security
class StateHelper {
private static let STATE_VALID_FOR = 3600
// Base64 (iOS + TimeStamp + Secure Random UUID)
private static let NUMBER_OF_PARAMETER = 3
static func generateState(session: Session)-> String {
var buffer = Data(count: 30)
let _ = buffer.withUnsafeMutableBytes {
SecRandomCopyBytes(kSecRandomDefault, 30, $0)
}
let state = ("iOS." + String(Int64(Date().timeIntervalSince1970)) + "." + buffer.base64EncodedString()).encodeToURISafeBase64()
// store state in session manager for future validation
session.state = state
LocalSessionManager.saveSession(session: session)
return state
}
static func validateState(state: String) -> Bool {
guard let originalState = LocalSessionManager.loadSession()?.state else {
return false
}
if state != originalState {
return false
}
if let timeStampString = originalState.decodeFromURISafeBase64()?.split(separator: "."), timeStampString.count == NUMBER_OF_PARAMETER, let timeStamp = Int64(timeStampString[1]){
if Int64(Date().timeIntervalSince1970) - timeStamp <= STATE_VALID_FOR {
return true
}
}
if let session = LocalSessionManager.loadSession() {
session.state = nil
LocalSessionManager.saveSession(session: session)
}
return false
}
private init() {}
}
extension String {
func decodeFromURISafeBase64() -> String? {
let base64String = String(self.map {
// replace unsafe characters
character in
if character == "." {
return "+"
} else if character == "_" {
return "/"
} else if character == "-" {
return "="
}
return character
})
guard let data = Data(base64Encoded: base64String) else {
return nil
}
return String(data: data, encoding: .utf8)
}
func encodeToURISafeBase64() -> String {
return String(Data(self.utf8).base64EncodedString().map{
// replace back unsafe characters
character in
if character == "+" {
return "."
} else if character == "/" {
return "_"
} else if character == "=" {
return "-"
}
return character
})
}
}
将Amazon授权码交换为Amazon访问令牌。
在自有App中收到用户的Amazon授权码后,您需要使用它来获取Amazon访问令牌。您需要访问令牌来调用Alexa服务以启用技能并完成帐户链接。
使用下图所示格式提出HTTP POST请求。有关可能出现的错误和要向用户显示的相关消息,请参阅将Amazon授权代码交换为Amazon访问令牌时的错误码。
请求参数
POST /auth/o2/token HTTP/1.1 Host: api.amazon.com Content-Type: application/x-www-form-urlencoded;charset=UTF-8 grant_type=authorization_code &code=amazon authorization code &client_id=yourClientId &client_secret=yourClientSecret &redirect_uri=yourRedirectUri
返回参数
HTTP 200 Content-Type: application/json { "access_token": "AmazonAccessToken", "expires_in": "3600", "refresh_token": "YourRefreshToken", "token_type": "bearer" }
有关在LWA的情况下获取访问令牌的详细信息,请参阅访问令牌请求、访问令牌响应和访问令牌错误。
示例:获取用户的Amazon访问令牌
在本例中,该应用程序将用户的Amazon授权代码交换为Amazon访问令牌。
const axios = require('axios'); // Amazon OAuth token request in the backend const OAuthRequests = (function () { const header = { "headers": { "Content-Type": "application/json" } }; const accessTokenBody = function (amazonAuthCode, state) { return { "grant_type": "authorization_code", "code": amazonAuthCode, "redirect_uri": config.skillConfig.redirect_uri, "client_id": config.skillConfig.client_id, "client_secret": config.skillConfig.client_secret }; }; return { accessTokenRequest: (authcode, state) => { return { header: header, body: accessTokenBody(authcode, state) } } } })(); // Request the Amazon access/refresh tokens from the Amazon OAuth token server function getAccessTokenByAuthcode(amazonAuthCode, state) { const accessTokenRequest = OAuthRequests.accessTokenRequest(amazonAuthCode, state); return axios.post(config.endpoints.oauthEndpoint, accessTokenRequest.body, accessTokenRequest.header); }
启用Skill并完成帐户链接。
当您的自有App具有用户的Amazon访问令牌,您的后端服务器可以调用Alexa Skill激活API来启用该技能并完成帐户链接。
使用以下URL格式向Alexa Skill激活API提出请求:
[BASEURL]/v1/users/~current/skills/{yourSkillId}/enablement
,其中[BASE URL]
取决于用户所在的地区:https://api.amazonalexa.com
、https://api.eu.amazonalexa.com
或https://api.fe.amazonalexa.com
。您使用的安全提供商必须授权从应用程序对应用程序后端服务器的调用,并提供服务器端API,以获取当前登录您应用程序的用户授权代码。(也就是说,它需要在您的服务中提供用户帐户的授权代码。)Alexa将使用此代码交换用户访问令牌,以访问用户的资源。有关可能出现的错误和要向用户显示的相关消息,请参阅Errors when calling the Alexa Skill Activation API。有关Alexa技能激活API的详细信息,请参阅Alexa Skill激活API。说明如果您使用LWA作为安全提供商,请记住,您用于获取userauthCode的LWA Client ID与Alexa开发者控制台在配置应用程序到应用程序帐户链接技能时显示的Alexa客户端ID不同。您无法使用用于获取Amazon访问令牌(步骤5)的Alexa客户端ID为您的用户生成anauthCode。要生成新的userauthCode,请使用与LWA安全配置文件关联的LWA Client ID。
如果您拥有智能家居技能,并且已为该技能启用了发送Alexa Events权限,则您的Lambda函数必须处理AcceptGrant指令。否则,当用户尝试启用您的技能时,帐户链接失败。有关详细信息,请参阅使用权限将客户身份验证到Alexa。请注意,一般来说,将事件发送到事件网关的工作方式仍然像今天一样工作。也就是说,在启用技能后,该技能可以从指令中获得访问令牌。
要启用该技能,请提出以下请求:
请求参数
POST /v1/users/~current/skills/{skillId}/enablement HTTP/1.1 Host: api.amazonalexa.com, api.eu.amazonalexa.com, api.fe.amazonalexa.com Content-Type: application/json Authorization: "Bearer {Amazon Access Token}" { "stage": "skill stage", "accountLinkRequest": { "redirectUri": "https://yourRedirectURI", "authCode": "Your user's authorization code from your authorization server", "type": "AUTH_CODE" } }
在下一个显示的请求中,根据用户所在地区将HOST设置为以下之一:api.amazonalexa.com、api.eu.amazonalexa.com,或api.fe.amazonalexa.com
成功响应
HTTP 201 Content-Type: application/json { "skill": { "stage": "your skill stage", "id": "your skill id" }, "user": { "id": "User ID" }, "accountLink": { "status": "LINKED" }, "status": "ENABLED" }
请注意,您可以通过向Alexa技能激活API提出ADELETE请求来禁用该技能并取消用户帐户的链接。
示例:启用技能并完成帐户链接
在本例中,该应用程序启用了该技能并完成了帐户链接。
const _ = require('lodash'); const axios = require('axios'); const {config} = require('../config/skillconfig'); const {getAccessTokenByAuthcode} = require('./oauthrequests'); // Request to the Alexa Skill Activation API in the backend const AlexaServiceRequest = (() => { const header = (amazonAccessToken) => { return { "headers": { "Content-Type": "application/json", "Authorization": "Bearer " + amazonAccessToken } }; }; const body = (userAuthCode) => { return { "stage": config.skillConfig.stage, "accountLinkRequest": { "redirectUri": config.skillConfig.redirect_uri, "authCode": userAuthCode, "type": "AUTH_CODE" } } } return { createEnablement: (amazonAccessToken, userAuthCode) => { return { header: header(amazonAccessToken), body: body(userAuthCode) } } } })(); function createEnablementWithAccountLink(amazonAuthCode, userAuthcode, state) { // Exchange Amazon OAuth Code for Amazon Access Token const tokenPromise = getAccessTokenByAuthcode(amazonAuthCode, state); return tokenPromise.then((res) => { if (!_.has(res, 'data.access_token')) { throw Error("Amazon AccessToken is invalid"); } console.log(res); const amazonAccessToken = res.data.access_token; // Call the Alexa Skill Activation API to create enablement const createEnablementRequest = AlexaServiceRequest.createEnablement(amazonAccessToken, userAuthcode); return sendPostRequests(config.endpoints.alexaServiceEndpoints, createEnablementRequest.body, createEnablementRequest.header); }); } // Post enablement to the Alexa Skill Activation API function sendPostRequests(endpoints, body, header) { var alexaServicePromises = [] // Post for each Alexa Skill Activation API regional endpoint for the user endpoints.forEach((endpoint)=> { alexaServicePromises.push(axios.post(endpoint, body, header)); }); return new Promise((resolve, reject)=> { var failures = 0; alexaServicePromises.forEach((promise)=> { promise.then((res)=> { if (res.status == 201) { resolve(res.data); } else { if (++failures == alexaServicePromises.length) { reject(res.data); } } }).catch((err)=> { if (++failures == alexaServicePromises.length) { reject(err.data); } }) }) }); }
在自有App中显示Account Linking状态。
通过在自有App中显示帐户链接状态,例如在"用户体验和错误"中的屏幕截图中,让用户了解帐户链接状态。您可以使用不同的用户界面组件(例如表或页面视图)来显示帐户链接状态。
示例:显示帐户链接状态
在本例中,该应用程序使用UIViewController显示帐户链接状态。
在Xcode IDE中,前往主故事板添加新的视图控制器。此视图控制器的故事板ID是"linkingStatusScreen"。
添加一个新的Cocoa Touch类文件,选择UIViewController的子类,并在身份检查器中将您的类文件与UIViewController连接起来。
添加标题、显示帐户链接状态的UILabel状态、关闭此页面的UIButton和UIActivityIndicatorView,以便在应用程序调用Alexa服务时显示加载动画。
import UIKit class LinkingStatusViewController: UIViewController { @IBOutlet weak var header: UILabel! @IBOutlet weak var status: UILabel! @IBOutlet weak var closeButton: UIButton! @IBOutlet weak var indicator: UIActivityIndicatorView! var response : ValidatedResponse? override func viewDidLoad() { super.viewDidLoad() if let res = self.response { passValidatedResponse(validatedResponse: res) } } private func updateView(header:String, status:String, animating: Bool,closeButton:Bool) { if animating { self.indicator?.startAnimating() } else { self.indicator?.stopAnimating() } self.indicator?.isHidden = !animating self.header?.text = header self.status?.text = status self.closeButton.isEnabled = closeButton if closeButton { self.closeButton.backgroundColor = #colorLiteral(red: 0.1960784314, green: 0.6039215686, blue: 0.8392156863, alpha: 1) } else { self.closeButton.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1) } } } extension LinkingStatusViewController { func passValidatedResponse(validatedResponse: ValidatedResponse) { if validatedResponse is AuthResponse { let response: AuthResponse = validatedResponse as! AuthResponse; updateView(header: "Status", status: "Loading", animating: true, closeButton: false) usingAuthCodeToGetAccessToken(amazonAuthCode: response.code) } else if (validatedResponse is ErrorResponse) { let response: ErrorResponse = validatedResponse as! ErrorResponse; updateView(header: response.error, status: response.errorDescription, animating: false, closeButton: true) } else { updateView(header: "Unknown request", status: validatedResponse.url.absoluteString, animating: false, closeButton: true) } } // Complete account linking private func usingAuthCodeToGetAccessToken(amazonAuthCode: String) { guard let session = LocalSessionManager.loadSession(), session.sessionValid else { updateView(header: "Status", status: "User not login", animating: false, closeButton: true) return; } AlamofireHelper.completeAccountLinking(userAccessToken: LocalSessionManager.loadSession()!.accessToken, amazonAuthCode: amazonAuthCode, state: StateHelper.generateState(session: session)).responseJSON(queue: DispatchQueue.global(qos: .userInitiated), options: []) { response in if let status = response.response?.statusCode, 200..<300 ~= status { DispatchQueue.main.sync { self.updateView(header: "Status", status: "Account Linking Succeed", animating: false, closeButton: true) } return } else { DispatchQueue.main.sync { self.updateView(header: "Failed", status: "Account Linking failed", animating: false, closeButton: true) } } } } } // Open the account linking status page let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController initialViewController.response = validatedResponseself.window?.rootViewController = initialViewController self.window?.makeKeyAndVisible()
使用Skill中的访问令牌。
用户成功启用您的Skill并将Alexa链接到您的服务后,发送到您的Skill的请求包括用户的访问令牌。您的Skill代码需要从请求中获取访问令牌,验证它,并使用它从资源服务器检索必要的用户信息。有关如何验证和使用访问令牌的详细信息,请参阅技能类型:
以云智能开发为例(安卓、IOS)
云智能Android App部分
App-Alexa-App
获取Alexa App-To-App参数。
调用云端api:/living/voice/alexa/apptoapp/config/get 获取alex 相关配置。
返回的实体对象:
public class AlexaApptoappConfigGetDTO { private String alexaAppUrl; private String lwaFallbackUrl;
拉起Alexa。
private void openAlexaAppToAppUrl(String alexaAppUrl, String lwaFallbackUrl) { Intent alexaAppToAppIntent; if (doesAlexaAppSupportAppToApp(getApplicationContext())) { alexaAppToAppIntent = getAppToAppIntent(alexaAppUrl); } else { alexaAppToAppIntent = getAppToAppIntent(lwaFallbackUrl); } startActivity(alexaAppToAppIntent); finish(); }
private Intent getAppToAppIntent(String appToAppUrl) { Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(appToAppUrl)); return intent; }
接收Alexa的技能绑定结果。
<activity android:name=".activity.AuthActivity" android:launchMode="singleTask" android:screenOrientation="portrait"> <!--Amazon Alexa App--> <intent-filter android:autoVerify="true" tools:ignore="UnusedAttribute"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="yourHost" android:scheme="yourScheme" android:pathPrefix="yourPathPrefix" /> </intent-filter> </activity>
private void handleIntent(@NonNull Intent intent) { ALog.d(TAG, "handleIntent: "); // amazon alexa 回调回来 if (intent.getData() == null) return; String appLinkData = intent.getData().toString(); ILog.d(TAG, "appLinkData:" + appLinkData); // 从alexa回调回来,去云端鉴权,这个设置一个标签,以防当前云智能账号被挤掉的情况发生。 String domain = "https://open-living.iot.aliyun.com"; if (GlobalConfig.API_ENV_PRE.equals(GlobalConfig.getInstance().getApiEnv())) { domain = "https://open-living-beta.iot.aliyun.com"; } if (appLinkData.startsWith(domain) && !appLinkData.contains("error=") && !appLinkData.contains("error_description=")) { if (null == mHandler) { mHandler = new TmallGenieHandler(this); } // 链接回调 parsingUrl(appLinkData); } }
private void parsingUrl(String url) { String redirect_uri; String code = ""; String state = ""; url = url.trim(); String[] urlParts = url.split("\\?"); redirect_uri = urlParts[0]; if (urlParts.length > 1) { String[] params = urlParts[1].split("&"); Map<String, String> map = new HashMap<>(); for (String param : params) { String[] keyValue = param.split("="); if (keyValue.length > 1) { map.put(keyValue[0], keyValue[1]); } } state = map.get("state"); code = map.get("code"); } ILog.e(TAG, " redirect_uri = " + redirect_uri + ",state = " + state, ",code = " + code); }
云端查询技能是否绑定成功。
public void queryAlexaApptoappLaunchGet(String state, String redirectUri, String code) { ILog.d("TmallGenieBusiness", "queryAlexaApptoappLaunchGet"); if (null == mIoTAPIClient) { return; } Map<String, Object> params = new HashMap<>(); params.put("state", state); params.put("redirectUri", redirectUri); params.put("code", code); IoTRequest request = buildRequest(params, "/living/voice/alexa/apptoapp/launch"); ILog.d("TmallGenieBusiness", "requestMap:" + JSON.toJSONString(params)); IoTAPIClient mIoTAPIClient = new IoTAPIClientFactory().getClient(); mIoTAPIClient.send(request, mListener); }
Alexa-App-Alexa
接收Alexa的认证请求。
<activity android:name=".activity.AuthActivity" android:launchMode="singleTask" android:screenOrientation="portrait"> <!--Amazon Alexa App--> <intent-filter android:autoVerify="true" tools:ignore="UnusedAttribute"> <action android:name="android.intent.action.VIEW" /> <category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.BROWSABLE" /> <data android:host="yourHost" android:scheme="yourScheme" android:pathPrefix="yourPathPrefix" /> </intent-filter> </activity>
private void handleIntent(@NonNull Intent intent) { needKeepUserClick = false; ALog.d(TAG, "action: " + intent.getAction()); ALog.d(TAG, "data: " + intent.getData()); String clientId = null; String state = null; final ArrayList<String> scopeList = new ArrayList<>(); String redirectUri = null; Uri appLinkData = intent.getData(); if (appLinkData == null) { Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: empty app link data")); finish(); return; } if (appLinkData.getPathSegments().contains("oauth2") || appLinkData.getPathSegments().contains("alexa")) { if (appLinkData.getPathSegments().contains("alexa")) { needKeepUserClick = true; } mScope = Scope.Alexa; clientId = appLinkData.getQueryParameter("client_id"); state = appLinkData.getQueryParameter("state"); String _scope = appLinkData.getQueryParameter("scope"); if (_scope != null) { scopeList.add(_scope); } redirectUri = appLinkData.getQueryParameter("redirect_uri"); } else { Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: unknown path segments")); } if (mScope == null) { Log.w(TAG, "handleIntent: ", new Throwable("Cloud Intelligence: wrong scope")); finish(); return; } String finalClientId = clientId; String finalState = state; String finalRedirectUri = redirectUri;
去云端获取code。
private void accountAuth2CodeGet(@NonNull String clientId, @NonNull String state, @NonNull String redirectUri, @Nullable List<String> scope) { showLoading(); JSONObject params = new JSONObject(); params.put("clientId", clientId); params.put("state", state); params.put("redirectUri", redirectUri); if (scope != null) { params.put("scope", scope); } IoTRequestBuilder builder = new IoTRequestBuilder().setAuthType(MineConstants.APICLIENT_IOTAUTH).setApiVersion(LIVING_ACCOUNT_OAUTH2_CODE_GET_VERSION); IoTRequest ioTRequest = builder .setPath("/living/account/oauth2/code/get") .setParams(params.getInnerMap()) .setScheme(Scheme.HTTPS) .build(); new IoTAPIClientFactory().getClient().send(ioTRequest, new IoTCallback() { @Override public void onFailure(IoTRequest request, Exception e) { ALog.w(TAG, "onFailure: " + (e == null ? "" : e.getMessage())); String message = e != null ? e.getMessage() : null; if (message == null) { message = e != null ? e.getLocalizedMessage() : null; } onAuthFailed(message, redirectUri, state); } @Override public void onResponse(IoTRequest request, IoTResponse response) { if (response != null && response.getData() != null && response.getCode() == 200) { ALog.d(TAG, "onResponse succeed"); onAuthSucceed(response.getData().toString(), redirectUri, state); } else { String message = response != null ? response.getMessage() : null; if (message == null) { message = response != null ? response.getLocalizedMsg() : null; } ALog.d(TAG, "onResponse error; " + message); onAuthFailed(message, redirectUri, state); } } }); }
云端鉴权成功。
private void onAuthSucceed(@NonNull String code, @NonNull String redirectUri, @NonNull String state) { Intent intent = new Intent(); ThreadPool.MainThreadHandler.getInstance().post(() -> { if (isFinishing() || isDestroyed()) return; dismissLoading(); switch (mScope) { case Alexa: intent.setAction(Intent.ACTION_VIEW); intent.setData(getAlexaSuccessfulUri(redirectUri, code, state)); try { if (intent.resolveActivity(AApplication.getInstance().getPackageManager()) != null) { startActivity(intent); } else { ALog.d(TAG, "not systemAction->" + intent.getAction()); } } catch (Exception e) { e.printStackTrace(); } break; default: break; } finish(); }); }
云端鉴权失败。
private void onAuthSucceed(@NonNull String code, @NonNull String redirectUri, @NonNull String state) { Intent intent = new Intent(); ThreadPool.MainThreadHandler.getInstance().post(() -> { if (isFinishing() || isDestroyed()) return; dismissLoading(); switch (mScope) { case Alexa: intent.setAction(Intent.ACTION_VIEW); intent.setData(getAlexaSuccessfulUri(redirectUri, code, state)); try { if (intent.resolveActivity(AApplication.getInstance().getPackageManager()) != null) { startActivity(intent); } else { ALog.d(TAG, "not systemAction->" + intent.getAction()); } } catch (Exception e) { e.printStackTrace(); } break; default: break; } finish(); }); }
云智能iOS App部分
开发步骤-云智能iOS App部分
建议开发使用以下库和语言提供iOS应用(9.0或更高版本):
iOS app
Backend server
在云智能iOS App中显示帐户链接选项。
云智能iOS App需要有一个启动页面,用户可以从中启动技能启用和帐户链接。此页面应显示用户可以通过将其帐户链接到Alexa来做什么,例如:通过语音订购智能彩灯。
您允许用户启动帐户链接的方式取决于您希望为用户创建的体验。您可以在多个位置向用户提供帐户链接:例如,您可以在用户注册云智能iOS App账号后为其提供启用帐户链接的选项,您可以在App内设置页面的"链接到Alexa"页面上为其提供按钮,等等。在任何情况下,请务必解释与Alexa链接帐户的方便有效之处。
将App to App Account Linking按钮添加到iOS应用内,请执行以下操作:
以使用Xcode IDE为例,打开storyboard。
将UIbutton添加到视图控制器,并将其命名为"Link your account with Alexa"。考虑使用一个名字来表示用户通过链接他们的帐户将得到的好处,例如"用Alexa听你的音乐","订购一个懂你的智能烤箱"。
在Xcode IDE的左上角,单击"Show the assistant editor"。
通过将按钮拖动到代码编辑器,为按钮创建操作连接。
为云智能iOS App启用Universal Link(iOS)。
要允许Alexa App将用户重新跳转向到云智能iOS App,您必须为iOS应用程序启用Universal Link。
配置使用App to App Account Linking的Skill。
您可以使用Alexa Developer console(Alexa开发者控制台)、Alexa Skills Kit命令行界面(ASK CLI)或Alexa skill Management API(SMAPI)来配置您的Skill(技能)。在此配置过程中,您可以指定云智能iOS App的redirect URL、access token URL等内容。
使用Alexa Developer console配置。
登录Alexa开发者控制台。
在列表中找到你的Skill。在你的Skill下操作,选择编辑。
在左侧,单击
TOOLS
,然后单击Account Link
。如果您以前为您的Skill配置了Account Link,请启用"Allow users to link their account to your skill from within your application or website"。
对于授权授予类型,如果尚未选择授权代码授予,请选择授权代码授予。App to App Account Linking仅支持授权代码授予。
对于其他设置,请使用配置Account Link中描述的设置,并在填写名为"redirect URL"的字段时注意以下附加说明:
对于redirect URL,请添加URL和相关参数说明中描述的redirect URL。
redirect URL必须符合指定的URI语法。Alexa App将打开此Universal Link和App Link URL以启动云智能iOS App。
要处理不同的场景,可以添加多个redirect URL。
如果您的App不支持Universal Link和App Link,你应该有一个redirect URL指向的有效网页。这应该支持相同的过程来接收用户的Amazon授权代码,如上流程设计所述。
请记住为iOS应用程序、Android应用程序和网站添加redirect URL。
使用ASK CLI配置。
要使用ASK CLI启用App to App Account Linking,请根据以下步骤使用更新帐户链接信息中的子命令:
允许用户在没有帐户链接的情况下启用技能选择
Y
或N
。Authorization URL输入授权服务器的URL,该URL用于常规帐户链接。授权服务器必须接受用户的凭据,对用户进行身份验证,并生成一个授权代码,Alexa App稍后可以传递给您的授权服务器,以检索一个访问令牌,该令牌将用户与您的服务唯一标识。
Client ID客户端ID,在您的登录页面输入,将用来识别请求来自您的技能的标识符。
Scopes(用逗号分隔)输入表示用户帐户所需的访问权限的字符,例如用户ID。对于智能家居技能,这个是必填项。您最多可以指定15个
scopes
。Domains(用逗号分隔)输入登录页面从中获取内容的其他域列表。您最多可以指定15个域。
授权授予类型选择
AUTH_CODE
。访问令牌URI输入访问令牌URL,详见URL和相关参数说明。
Client Secret输入一个凭据,允许Alexa服务使用访问令牌URI进行身份验证。这与Client ID(客户端ID)相结合,以识别来自Alexa的请求。
客户端身份验证方案选择
HTTP_BASIC
或REQUEST_BODY_CREDENTIALS
。(可选)默认访问令牌到期时间(以秒为单位)输入访问令牌有效的时间(以秒为单位)。
(可选)交互访问令牌URL输入URI,该URI将使用授权代码调用,这些授权代码可以交换Alexa访问令牌。
(可选)App to App Account Linking的redirect URL输入URL和相关参数说明中描述的App redirect URL,App to App Account Linking需要此设置。
使用Alexa skill Management API(SMAPI)配置。
要使用Alexa skill Management API(SMAPI)来配置App to App Account Linking,请根据此Account linking request进行结构设置。
以下是使用SMAPI添加redirect URL的请求示例。
名称
描述
类型
skipOnEnablement
设置为true,允许用户在不启动帐户链接流的情况下启用该能。当用户启用该技能时,设置为false,以要求正常的帐户链接流。有关更多信息,请参阅让用户在不链接账户的情况下启用您的技能。
Boolean
type
指定OAuth授权授予类型。对于App to App Account Linking,您必须使用
AUTH_CODE
。String
authorizationUrl
您的授权服务器的URL,该URL必须接受用户的凭据,对用户进行身份验证,并生成授权代码,Alexa App稍后可以传递给您的授权服务器,以检索唯一标识用户与您的服务的访问令牌。
String
domains
您的登录页面从中获取内容的其他域列表。您最多可以指定15个域。
Array of String
clientId
您的登录页面用于识别请求来自您的技能的标识符。
String
scopes
字符串,指示用户帐户所需的访问权限,例如
user_id
。对于智能家居技能,这个领域是必填项。您最多可以指定15个scopes
。Array of String
accessTokenUrl
用于请求授权令牌的URI。仅当
type
是AUTH_CODE
时才需要。String
clientSecret
您提供的凭据,允许Alexa服务使用访问令牌URI进行身份验证。这与
clientId
相结合,以识别来自Alexa的请求。String
accessTokenScheme
使用的身份验证类型。例如
HTTP_BASIC
,或REQUEST_BODY_CREDENTIALS
。对于App to App Account Linking,这是必需的,因为授权授予类型是AUTH_CODE
。String
redirectUrls
用Universal Link或App Link来启动云智能iOS App,redirectURL必须符合指定的URI语法。
Array of String
{ "accountLinkingRequest": { "accessTokenScheme": "HTTP_BASIC", "accessTokenUrl": "https://api.amazon.com/auth/o2/token/", "authorizationUrl": "https://www.amazon.com/ap/oa/", "clientId": "yourSMAPIClientId", "clientSecret": "yourSMAPIClientSecret", "domains": [], "redirectUrls": ["yourRedirectURL1","yourRedirectURL2"], "scopes": ["profile"], "skipOnEnablement": true, "type": "AUTH_CODE" } }
获取用户的Amazon授权码。
云智能iOS App中需要获取用户的Amazon授权码,以便以后可以将其交换为Amazon访问令牌。Amazon访问令牌将使云智能iOS App能够启用技能并完成帐户链接。Amazon授权码的有效期为5分钟。
通过打开Alexa App URL(对于Alexa App流)或向LWA授权服务器(对于LWA流)提出
HTTP GET
请求,您可以获得用户的Amazon授权代码。对于iOS和Android,我们强烈建议您实施Alexa App流,并仅将LWA用作后备。要获取用户的Amazon授权代码,您需要组装授权请求,然后打开Alexa App URL(如果没有安装Alexa App,则打开LWA fallback URL),并附上URL和相关参数说明中描述的参数。我们强烈建议您在后端服务器中组装这些URL,并将组装的URL传递给您的应用程序。这样,您就可以快速更改参数(如阶段),而无需重建应用程序。
当用户确认链接请求时,Alexa App会将用户重新定向到应用程序的redirect URL,这是Alexa或LWA fallback URL中的一个参数。redirect URL包括接下来描述的成功或错误参数。
如果用户的设备无法使用您提供的redirect URL启动您的应用程序,用户将被发送到其默认浏览器中的同一redirect URL。例如,当以下情况下,可能会发生这种情况:
您的应用程序中没有启用Universal Link或App Link。
您的应用程序中没有为您提供的redirect URL启用Universal Link或App Link。
用户设备上的应用程序版本是不支持Universal Link或App Link的旧版本。
在这些情况下,您应该有一个网页(与Universal Link或App Link的URL相同)来接收授权代码。当用户进入您的网页时,他们可能需要登录,以便您可以从您的服务中获得授权代码。
有关可能出现的错误和要向用户显示的相关消息,请参阅获取Amazon授权代码时的错误。
Alexa AppURL的HTTP GET请求的成功响应如果成功,响应是一个redirect URL,其中包括用户的Amazon授权代码和接下来显示的其他参数。要处理这个问题,请参阅苹果关于支持应用程序中通用链接的准则和处理安卓应用程序链接的安卓指南。
// Example success response from the Alexa app https://yourRedirectURL?code={Amazon Authorization code}&state={your state} // Example success response from LWA https://yourRedirectURL?code={Amazon Authorization code}&scope={permission scope}&state={your state}
地址
code:Alexa App或LWA返回的Amazon授权代码。
state:您在授权请求中传递的状态。您应该验证状态,以避免跨站点请求伪造。
scope:权限范围。LWA
returnalexaalexa::skills:account_linking scope
。Alexa App不返回范围参数。
有关OAuth2.0授权响应的更多详细信息,请参阅授权响应。Alexa AppURL的HTTP GET请求的错误响应如果出现错误,响应是redirect URL,接下来显示错误和其他参数。您的应用程序必须显示适当的错误信息(错误页面、UIAlertController等)。有关要显示的错误消息,请参阅获取Amazon授权代码时的错误码。
// Example error response HTTP 200 https://yourRedirectURL?error_description={Error description}&state={your state}&error={OAuth Error Code}
补充说明
error:单个ASCII错误代码。可能的值:invalid_request、unauthorized_client、access_denied、unsupported_response_type、invalid_scope、server_error、temporarily_unavailable。
error_description:错误描述。
有关OAuth2.0错误响应的更多详细信息,请参阅授权响应。
示例:获取URL(iOS)
在本例中,该应用程序同时获得Alexa App URL和LWA fallback URL。
import Alamofire import SwiftyJSON class AlamofireHelper { // Your backend base URL from which you're requesting the URL static let BASE_URL = "https://exampleBackEndURL" /*! Get Alexa app Universal Link and LWA fallback URL @param userAccessToken for your backend server @param state maintained between client and server, caller should generate it */ static func getAlexaAppUrl(userAccessToken:String, state: String) -> DataRequest { let alexaAppUrl = "/alexaurl" let inputstate = "?state=" + state var urlRequest = URLRequest(url: URL(string: BASE_URL + alexaAppUrl + inputstate)!) urlRequest.addValue("Bearer " + userAccessToken, forHTTPHeaderField: "Authorization") return Alamofire.request(urlRequest) } private init() {} } // Call to your backend to get the authorization request URLs (Alexa app / LWA) AlamofireHelper.getAlexaAppUrl(userAccessToken: savedSession.accessToken, state: StateHelper.generateState()).responseJSON { response in guard let status = response.response?.statusCode, 200..<300 ~= status else { self.createAlert(title: "Failed to get Alexa URL", message: "Failed to get Alexa URL") return } guard let responseValue = response.result.value else { self.createAlert(title: "Invalid response", message: "Invalid response") return } let reponseInJSON = JSON(responseValue) guard let companionApp = reponseInJSON["companionAppURL"].string,let lwaFallback = reponseInJSON["LWAFallBackURL"].string else { self.createAlert(title: "Error response", message: "Error response") return } guard let companionAppURL = URL(string: companionApp), let lwaFallbackURL = URL(string: lwaFallback) else { self.createAlert(title: "Incorrect URL format", message: "Incorrect URL format") return } // The openUniversalLinks code snippet can be found in another example self.openUniversalLinks(companionAppURL: companionAppURL, lwaFallbackURL: lwaFallbackURL) }
示例:打开URL(iOS)
在本例中,该应用程序将打开Alexa App URL,如果没有安装Alexa App,则打开LWA fallback URL。
注意以下内容:
本示例使用UIApplication.shared.openAPI使用universalLinksOnly选项打开URL。
只有当应用程序(此处为Alexa App)配置为使用universalLinksOnly选项处理通用链接时,URL才会打开。因此,您可以通过关闭表达式
completionHandler
处理Alexa App无法启动的情况,关闭表达式请参阅Closure Expressions(例如,Alexa App没有安装或版本不支持通用链接)。如果您的应用程序在使用universalLinksOnly选项时无法启动Alexa App,您的应用程序需要使用LWA获取Amazon授权代码。您可以在iOS 11上使用SFAuthenticationSession或iOS 12.0或更高版本上的ASWebAuthenticationSession。这将获得Safari用户会话和cookie,当用户已经在Safari上登录Amazon.com时,这避免了额外的登录。这将导致您的应用程序显示一个窗口,要求用户同意共享网站信息。该消息的内容由iOS定义。
对于低于11.0的iOS版本,此示例使用SFSafariViewController将用户发送到LWA页面。SFSafariViewController不要求用户同意共享网站信息;因此,Safari浏览器不会与SFSafariViewController共享cookie。
也可通过WKWebView打开LWA fallback URL,在WebView授权完成后会通过Universal Link打开您的App,拦截此URL获取授权参数。
import SafariServices // Open the Alexa URL with option universalLinksOnly // If it doesn't open the Universal Link, open the LWA fallback private func openUniversalLinks(companionAppURL: URL, lwaFallbackURL: URL) { UIApplication.shared.open(companionAppURL, options: [UIApplication.OpenExternalURLOptionsKey.universalLinksOnly:true]) { companionAppLaunched in if !companionAppLaunched { if #available(iOS 12.0, *) { WebSession.initWebAuthenticationSession(authURL: lwaFallbackURL) } else if #available(iOS 11.0, *) { WebSession.initAuthenticationSession(authURL: lwaFallbackURL) } else { let safariViewController = SFSafariViewController(url: lwaFallbackURL) self.present(safariViewController, animated: true, completion: nil)} } } } // Web session class WebSession { @available(iOS, introduced: 11.0, deprecated: 12.0) private static var authenticationSession: SFAuthenticationSession? @available(iOS 12.0, *) private static var webAuthenticationSession : ASWebAuthenticationSession? private static let CALLBACK_URL_SCHEME = "https://yourAppsUniversalLinkURL" @available(iOS, introduced: 11.0, deprecated: 12.0) static func initAuthenticationSession(authURL:URL) { if let session = authenticationSession { session.cancel() } self.authenticationSession = SFAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{ (callBack:URL?, error:Error?) in //Callback and error handling }) self.authenticationSession?.start() } @available(iOS 12.0, *) static func initWebAuthenticationSession(authURL:URL) { if let session = webAuthenticationSession { session.cancel() } self.webAuthenticationSession = ASWebAuthenticationSession.init(url: authURL, callbackURLScheme: CALLBACK_URL_SCHEME, completionHandler:{ (callBack:URL?, error:Error?) in // Callback and error handling }) self.webAuthenticationSession?.start() } private init(){} }
示例:打开LWA URL(iOS)
在网站的情况下打开LWA URL,作为iOS设备上没有安装Alexa App的fallback 。
在本例中,该应用程序在视图控制器中打开LWA。
import SafariServices // Open Login with Amazon in Safari View controller private func openSafariView(lwaFallbackURL: URL) { let safariViewController = SFSafariViewController(url: lwaFallbackURL) self.present(safariViewController, animated: true, completion: nil)} }
示例:处理通用链接响应(iOS)
在本例中,该应用程序验证并提取Alexa App对其通用链接的调用中的参数。
// Handler for Universal Links in AppDelegate // Add this method to handle incoming Universal Links func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool { guard userActivity.activityType == NSUserActivityTypeBrowsingWeb, let incomingURL = userActivity.webpageURL else { return false } let handlers : [UniversalLinkHandler] = [AuthResponseHandler(), ErrorResponseHandler()] var validatedResponse : ValidatedResponse? for handler in handlers { if handler.canHandle(incomingURL: incomingURL) { validatedResponse = handler.getValidatedResponse() let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController initialViewController.response = validatedResponse self.window?.rootViewController = initialViewController self.window?.makeKeyAndVisible() return true } } return false } // Incoming Universal Links handler protocol protocol UniversalLinkHandler { //Validate the incoming URL scheme func canHandle(incomingURL:URL)-> Bool //Get the validated response func getValidatedResponse() -> ValidatedResponse? } extension UniversalLinkHandler { func validateState(state:String)->Bool { return StateHelper.validateState(state: state) } } // Successfully receiving the Amazon Authorization code class AuthResponseHandler : UniversalLinkHandler { private let AUTH_RESPONSE_PARAMETERS: Set = ["code", "state", "scope"] private var validatedResponse: ValidatedResponse? func canHandle(incomingURL: URL) -> Bool { guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), let queryParameters = components.queryItems else { return false } var validatedParameters : [String: String] = [:] for queryParameter in queryParameters { //duplicated parameters in URL if validatedParameters.keys.contains(queryParameter.name) { return false } //Unrecognized parameters in URL if !AUTH_RESPONSE_PARAMETERS.contains(queryParameter.name) { return false } validatedParameters[queryParameter.name] = queryParameter.value } guard let code = validatedParameters["code"], let state = validatedParameters["state"] else { return false } //validated the state if !validateState(state: state) { self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state") return true } self.validatedResponse = AuthResponse(url: incomingURL,code: code, state: state, scope: validatedParameters["scope"]) return true } func getValidatedResponse() -> ValidatedResponse? { return validatedResponse } } // Error authorization response handler class ErrorResponseHandler : UniversalLinkHandler { private let ERROR_RESPONSE_PARAMETERS: Set = ["error", "error_description", "state"] private var validatedResponse: ValidatedResponse? func canHandle(incomingURL: URL) -> Bool { guard let components = NSURLComponents(url: incomingURL, resolvingAgainstBaseURL: true), let queryParameters = components.queryItems else { return false } // Validate all query parameters var validatedParameters : [String: String] = [:] for queryParameter in queryParameters { // Duplicated parameters in URL if validatedParameters.keys.contains(queryParameter.name) { return false } // Unrecognized parameters in URL if !ERROR_RESPONSE_PARAMETERS.contains(queryParameter.name) { return false } validatedParameters[queryParameter.name] = queryParameter.value } guard let error = validatedParameters["error"], let errorDescription = validatedParameters["error_description"], let state = validatedParameters["state"] else { return false } // validate the state if !validateState(state: state) { self.validatedResponse = ErrorResponse(url: incomingURL,error: "Invalid state", errorDescription: "The request has invalid state") return true } self.validatedResponse = ErrorResponse(url: incomingURL, error: error, errorDescription: errorDescription) return true } func getValidatedResponse() -> ValidatedResponse? { return validatedResponse } } // Validated response class ValidatedResponse { // Universal Link URL that launched the App private(set) var url: URL init (url: URL) { self.url = url } } // Amazon authorization code response per the OAuth2 specification class AuthResponse : ValidatedResponse{ private(set) var code : String private(set) var state: String //scope will be returned in Login with Amazon, will not be present in Alexa Companion flow private(set) var scope: String? init(url:URL, code:String, state:String, scope: String?) { self.code = code self.state = state self.scope = scope super.init(url:url) } } // Error response per the OAuth2 specification class ErrorResponse : ValidatedResponse { private(set) var error : String private(set) var errorDescription: String init(url: URL, error:String, errorDescription:String) { self.error = error self.errorDescription = errorDescription super.init(url: url) } }
状态生成和验证
示例:State Helper
在本例中,应用程序生成并验证请求和响应之间的状态。您必须使用状态验证传入的请求,以防止跨站点请求伪造,请参阅Cross-Site Request Forgery。
import Foundation import Security class StateHelper { private static let STATE_VALID_FOR = 3600 // Base64 (iOS + TimeStamp + Secure Random UUID) private static let NUMBER_OF_PARAMETER = 3 static func generateState(session: Session)-> String { var buffer = Data(count: 30) let _ = buffer.withUnsafeMutableBytes { SecRandomCopyBytes(kSecRandomDefault, 30, $0) } let state = ("iOS." + String(Int64(Date().timeIntervalSince1970)) + "." + buffer.base64EncodedString()).encodeToURISafeBase64() // store state in session manager for future validation session.state = state LocalSessionManager.saveSession(session: session) return state } static func validateState(state: String) -> Bool { guard let originalState = LocalSessionManager.loadSession()?.state else { return false } if state != originalState { return false } if let timeStampString = originalState.decodeFromURISafeBase64()?.split(separator: "."), timeStampString.count == NUMBER_OF_PARAMETER, let timeStamp = Int64(timeStampString[1]){ if Int64(Date().timeIntervalSince1970) - timeStamp <= STATE_VALID_FOR { return true } } if let session = LocalSessionManager.loadSession() { session.state = nil LocalSessionManager.saveSession(session: session) } return false } private init() {} } extension String { func decodeFromURISafeBase64() -> String? { let base64String = String(self.map { // replace unsafe characters character in if character == "." { return "+" } else if character == "_" { return "/" } else if character == "-" { return "=" } return character }) guard let data = Data(base64Encoded: base64String) else { return nil } return String(data: data, encoding: .utf8) } func encodeToURISafeBase64() -> String { return String(Data(self.utf8).base64EncodedString().map{ // replace back unsafe characters character in if character == "+" { return "." } else if character == "/" { return "_" } else if character == "=" { return "-" } return character }) } }
将Amazon授权码交换为Amazon访问令牌,启用Skill并完成帐户链接。
将
code
、redirectUri
、state
通过/living/voice/alexa/apptoapp/launch
接口传给云端完成帐户链接。在云智能iOS App中显示Account Linking状态。通过在云智能iOS App中显示帐户链接状态,例如在”用户体验和错误“中的屏幕截图中,让用户了解帐户链接状态。您可以使用不同的用户界面组件(例如表或页面视图)来显示帐户链接状态。
示例:显示帐户链接状态。
在本例中,该应用程序使用
UIViewController
显示帐户链接状态。在Xcode IDE中,前往主故事板添加新的视图控制器。此视图控制器的故事板ID是
linkingStatusScreen
。添加一个新的
Cocoa Touch
类文件,选择UIViewController
的子类,并在身份检查器中将您的类文件与UIViewController
连接起来。添加标题、显示帐户链接状态的UILabel状态、关闭此页面的
UIButton
和UIActivityIndicatorView
,以便在应用程序调用Alexa服务时显示加载动画。import UIKit class LinkingStatusViewController: UIViewController { @IBOutlet weak var header: UILabel! @IBOutlet weak var status: UILabel! @IBOutlet weak var closeButton: UIButton! @IBOutlet weak var indicator: UIActivityIndicatorView! var response : ValidatedResponse? override func viewDidLoad() { super.viewDidLoad() if let res = self.response { passValidatedResponse(validatedResponse: res) } } private func updateView(header:String, status:String, animating: Bool,closeButton:Bool) { if animating { self.indicator?.startAnimating() } else { self.indicator?.stopAnimating() } self.indicator?.isHidden = !animating self.header?.text = header self.status?.text = status self.closeButton.isEnabled = closeButton if closeButton { self.closeButton.backgroundColor = #colorLiteral(red: 0.1960784314, green: 0.6039215686, blue: 0.8392156863, alpha: 1) } else { self.closeButton.backgroundColor = #colorLiteral(red: 0.6000000238, green: 0.6000000238, blue: 0.6000000238, alpha: 1) } } } extension LinkingStatusViewController { func passValidatedResponse(validatedResponse: ValidatedResponse) { if validatedResponse is AuthResponse { let response: AuthResponse = validatedResponse as! AuthResponse; updateView(header: "Status", status: "Loading", animating: true, closeButton: false) usingAuthCodeToGetAccessToken(amazonAuthCode: response.code) } else if (validatedResponse is ErrorResponse) { let response: ErrorResponse = validatedResponse as! ErrorResponse; updateView(header: response.error, status: response.errorDescription, animating: false, closeButton: true) } else { updateView(header: "Unknown request", status: validatedResponse.url.absoluteString, animating: false, closeButton: true) } } // Complete account linking private func usingAuthCodeToGetAccessToken(amazonAuthCode: String) { guard let session = LocalSessionManager.loadSession(), session.sessionValid else { updateView(header: "Status", status: "User not login", animating: false, closeButton: true) return; } AlamofireHelper.completeAccountLinking(userAccessToken: LocalSessionManager.loadSession()!.accessToken, amazonAuthCode: amazonAuthCode, state: StateHelper.generateState(session: session)).responseJSON(queue: DispatchQueue.global(qos: .userInitiated), options: []) { response in if let status = response.response?.statusCode, 200..<300 ~= status { DispatchQueue.main.sync { self.updateView(header: "Status", status: "Account Linking Succeed", animating: false, closeButton: true) } return } else { DispatchQueue.main.sync { self.updateView(header: "Failed", status: "Account Linking failed", animating: false, closeButton: true) } } } } } // Open the account linking status page let initialViewController = UIStoryboard(name: "Main", bundle: nil).instantiateViewController(withIdentifier: "linkingStatusScreen") as! LinkingStatusViewController initialViewController.response = validatedResponseself.window?.rootViewController = initialViewController self.window?.makeKeyAndVisible()
使用Skill中的访问令牌。
用户成功启用您的Skill并将Alexa链接到您的服务后,发送到您的Skill的请求包括用户的访问令牌。您的Skill代码需要从请求中获取访问令牌,验证它,并使用它从资源服务器检索必要的用户信息。有关如何验证和使用访问令牌的详细信息,请参阅技能类型:
云端接口说明
/living/voice/alexa/apptoapp/config/get
获取Alexa App To App 的跳转参数
版本号:1.0.0
入参列表
无入参:
入参名称 | 数据类型 | 结构类型 | 是否必须 | 缺省默认值 | 入参示例 | 入参描述 | 子级参数类型 |
没有数据 |
出参列表
标准:
参数名称 | 数据类型 | 结构类型 | 后端参数名称 | 参数描述 | 子级参数类型 |
code | 整型 | code | 响应码,200: 成功。 | ||
message | 字符串 | message | 错误消息。 | ||
localizedMsg | 字符串 | localizedMsg | 本地语言错误消息。 | ||
data | JSON | 结构体 | data | 响应结果。 | |
lwaFallbackUrl | 字符串 | lwaFallbackUrl | LWA Falback URL 当用户手机没有安装Alexa APP时,需要弹出webview 进入该URL进行LWA授权获取。 | ||
alexaAppUrl | 字符串 | alexaAppUrl | 当用户手机已经安装了 Alexa APP 时打开该URL。 |
/living/account/oauth2/code/get
根据OAuth2规则生成当前登录账号的对外授权AuthCode
。
版本号:1.0.0
入参列表
入参名称 | 数据类型 | 结构类型 | 是否必须 | 缺省默认值 | 入参示例 | 入参描述 | 子级参数类型 |
redirectUri | 字符串 | 是 | Oauth2认证过程完成后回调请求授权第三方的回跳地址。 | ||||
state | 字符串 | 否 | Oauth2认证过程中请求授权第三方携带的state。 | ||||
clientId | 字符串 | 是 | Oauth2 认证过程中请求授权第三方服务商的client_id。 | ||||
scope | JSON | 数组 | 否 | Oauth2授权过程中,请求授权的第三方服务商请求的授权权限范围。 | 字符串 |
出参列表
标准:
参数名称 | 数据类型 | 结构类型 | 后端参数名称 | 参数描述 | 子级参数类型 |
code | 整型 | code | 响应码, 200: 成功。 | ||
message | 字符串 | message | 错误消息。 | ||
localizedMsg | 字符串 | localizedMsg | 本地语言错误消息。 | ||
data | 字符串 | data | 生成的AuthCode。 |
/living/voice/alexa/apptoapp/launch
Alexa App-To-App流程获取到配置,跳转Alexa APP或者跳出LWA页面获取到Alexa颁发的AuthCode
之后提交至云端完成剩下的绑定流程。
版本号:1.0.1
入参列表
入参名称 | 数据类型 | 结构类型 | 是否必须 | 缺省默认值 | 入参示例 | 入参描述 | 子级参数类型 |
code | 字符串 | 是 | Alexa LWA授权authCode。 | ||||
state | 字符串 | 是 | Oauth2 认证参数state。 |
出参列表
标准:
参数名称 | 数据类型 | 结构类型 | 后端参数名称 | 参数描述 | 子级参数类型 |
code | 整型 | code | 响应码,200: 成功。 | ||
message | 字符串 | message | 错误消息。 | ||
localizedMsg | 字符串 | localizedMsg | 本地语言错误消息。 |
附录:名词解释
相关术语
Service
您提供的服务。例如,您可能有一个基于Web的服务"Ride Hailer",允许用户订购出租车。
App
您的用户用于与您的服务进行交互的应用程序。继续上一个示例,您可能有一个"Ride Hailer"应用程序。
Skill
Alexa的技能,使用户能够使用Alexa与您的服务交互。
Alexa app
用户可以为其移动设备下载的Amazon Alexa App。下载方式请查看Alexa App
Login with Amazon (LWA)
一种身份验证系统,允许用户登录并授予访问其用户配置文件数据的权限。就App to App Account Linking而言,LWA是一种后备方案,您可以实施它来处理用户在其移动设备上没有安装Alexa App的情况。有关LWA的一般信息,请参阅Login with Amazon(LWA)。
OAuth 2.0
一种身份验证标准,通过该标准,您的服务可以允许Alexa在用户许可的情况下访问用户与您建立的帐户中的信息。有关OAuth 2.0标准,请参见OAuth 2.0。
App Link
Android上的一个深度链接,用户点击该链接即可启动应用程序。有关App Link的详细信息,请参阅Android文档。
Universal Link
iOS上的一个深度链接,用户点击该链接即可启动应用程序。有关Universal Link的详细信息,请参阅iOS文档。
名称 | 解释 | 示例 |
Alexa App URL | 您的自有App使用此Universal Link(iOS)或App Link(Android)将用户跳转到Alexa App以确认链接请求。有关Universal Link(iOS)的一般信息,请参阅Universal Link Content 。 您可以在后端存储Alexa App URL和LWA fallback URL,并通过 请求检索它们。您还可以在本地存储URL,以避免额外的网络调用;如果将来修改技能安全配置文件,这可能需要重新构建。 |
|
LWA fallback URL | 将用户跳转到LWA以输入其Amazon账户信息的链接。它适用于iOS、Android和网站。如果用户的设备上没有安装Alexa App,则自有App需要使用此选项。 |
|
App redirect URLs | Alexa App(或LWA,如果未安装Alexa App)在用户确认Alexa App或LWA中的链接请求后,使用此Universal Link(iOS)或App Link(Android)将用户跳转回自有App。 返回的参数为自有App提供了用户的Amazon授权码,有效期为5分钟。 | 您可以使用Alexa开发者控制台、ASK CLI或SMAPI指定其中一个或多个。请参阅URI规范。 |
授权URL | 授权服务器的URL。授权服务器必须接受用户的凭据,对用户进行身份验证,并生成一个授权代码,Alexa App稍后可以将该代码传递给授权服务器,以检索访问令牌,该令牌通过您的服务为唯一标识。 | 此链接仅用于常规(非应用到应用)帐户链接。您可以使用开发者控制台、ASK CLI或SMAPI来指定这一点。 |
Access token URL | Alexa使用您的令牌服务器将用户的授权码(用于您的服务)交换为访问令牌,以完成Account Linking。 | 您可以使用Alexa开发者控制台、ASK CLI或SMAPI来指定这一点。 |
LWA授权服务 | 您的后端服务器使用它将用户的Amazon授权代码交换为Amazon访问令牌。这使自有App能够调用Alexa服务来启用技能并为用户链接帐户。 | Host: |
Alexa Skill API激活 | 您的后端服务器调用此函数以启用用户的技能。有关Alexa技能激活API的详细信息,请参阅Alexa Skill API激活。 |
取决于用户激活是否在本地,原始URL可以是https://api.amazonalexa.com,https://api.eu.amazonalexa.com或https://api.fe.amazonalexa.com |
URL和相关参数说明
Alexa App URL和LWA fallback URL的参数说明
字段 | 描述 |
Client ID | 在Alexa开发者控制台中启用App to App Account Linking时,Alexa开发者控制台提供的客户端ID。 |
Client secret | 在Alexa开发者控制台中启用App to App Account Linking时,Alexa开发者控制台提供的客户端机密。 |
Redirect URL | 在Alexa开发者控制台中启用App to App Account Linking时指定的Universal Link 或App Link。Alexa App和LWA在确认帐户链接请求后将用户重新跳回此URL。 |
Scope | 范围,自有App必须将其设置为 |
Skill ID | 您技能的唯一标识符。您可以在开发者控制台中找到它。 |
Stage | 技能阶段,这取决于您的技能是否已发布。在你的技能发布之前,设置 |
response type | 响应类型必须为 |
state | 应用程序用于维护当前请求和响应之间状态的不透明值。对于Alexa App URL,状态值是必需的。Alexa使用redirect URL将用户转向回自有App时,在响应中包含此状态。您必须使用状态验证传入请求,以防止跨站点请求伪造,请参阅Cross-Site Request Forgery。 确保生成的状态不包含 |