在前一篇文章中,我們讓Android、iOS、Windows Phone及Windows 8應用程式的使用者可以透過ACS整合Facebook Identity Provider完成使用者初步的帳密驗證動作,但這只是前半部故事,
當完成驗證動作後,接下來該做什麼事呢?
文/黃忠成
What’s Next?
在前一篇文章中,我們讓Android、iOS、Windows Phone及Windows 8應用程式的使用者可以透過ACS整合Facebook Identity Provider完成使用者初步的帳密驗證動作,但這只是前半部故事,
當完成驗證動作後,接下來該做什麼事呢?
在透過ACS整合Identity Providers後,應用程式取得的其實只是一串Secure Token,依據Identity Provider及設定,這串Token裡面可能會包含Email或是使用者名稱,但也可能只包含一個
ID(Windows Live ID就只提供一個唯一識別碼),這意味著應用程式通常必須接續下來做一些有意義的事,例如要求使用者輸入EMAIL或送貨地址等個人資料。
是的,ACS與Identity Providers只提供帳密的驗證機制,相關後續動作還是得由應用程式來處理。
那當使用者通過驗證,且輸入了應用程式所需要的個人資料後,接下來應用程式該做些什麼?
Creating Secure WCF Service
當應用程式設計為要使用者先通過驗證後方能使用時,其接下來的動作必定是透過某些通道與後端Server做溝通。舉個例來說,我們設計了一個購物應用程式,當使用者首次登入後,
應用程式會詢問使用者的一些個人資料(不包括帳密),接著應用程式發出一個Web Service呼叫來將這些個人資料傳送到後端儲存後,應用程式再發出一個Web Service呼叫來取得商品列表。
圖1
就正規設計來說,這個WCF Service必然得設計為需要通過驗證方能呼叫,未經授權的呼叫會被擋在門外,這時由ACS所取得的Secure Token就成為了驗證碼。
那如何撰寫這樣的WCF Service呢?首先得先安裝Windows Identity Foundation Runtime及Windows Identity Foundation SDK。
Windows Identity Foundation Runtime
http://msdn.microsoft.com/en-us/security/aa570351.aspx
Windows Identity Foundation SDK
http://www.microsoft.com/en-us/download/details.aspx?id=4451
安裝完成後,建立一個Web Application Project,添加WAToolkitForWP7\Samples\WP7.1\Libraries\DPE.Oauth及Microsof.IdentityModel的Reference,
然後建立WCF Service。
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using System.ServiceModel.Web;
namespace WebApplication15
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the interface name "IService1" in both code and config file together.
[ServiceContract]
public interface IService1
{
[OperationContract]
[WebInvoke(Method = "GET", UriTemplate = "/HelloWorld", RequestFormat = WebMessageFormat.Json,
ResponseFormat = WebMessageFormat.Json, BodyStyle=WebMessageBodyStyle.WrappedResponse)]
string HelloWorld();
}
}
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
using System.ServiceModel;
using System.Text;
using System.ServiceModel.Activation;
using Microsoft.IdentityModel.Claims;
using System.Web;
namespace WebApplication15
{
// NOTE: You can use the "Rename" command on the "Refactor" menu to change the class name "Service1" in code, svc and config file together.
[AspNetCompatibilityRequirements(RequirementsMode = AspNetCompatibilityRequirementsMode.Required)]
public class Service1 : IService1
{
private bool IsAuthenticated()
{
return HttpContext.Current.User.Identity.IsAuthenticated;
}
public string HelloWorld()
{
if (IsAuthenticated())
return "hello world.";
else
throw new Exception("Error.");
}
}
}
修改web.config
<?xml version="1.0"?>
<!--
For more information on how to configure your ASP.NET application, please visit
http://go.microsoft.com/fwlink/?LinkId=169433
-->
<configuration>
<configSections>
<section name="microsoft.identityModel" type="Microsoft.IdentityModel.Configuration.MicrosoftIdentityModelSection, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</configSections>
<system.web>
<compilation debug="true" targetFramework="4.0" />
<!--<httpModules>
<add name="ProtectedResourceModule" type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ProtectedResourceModule" />
<add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
</httpModules>-->
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false" />
<modules runAllManagedModulesForAllRequests="true">
<add name="ProtectedResourceModule" type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ProtectedResourceModule" />
<add name="WSFederationAuthenticationModule" type="Microsoft.IdentityModel.Web.WSFederationAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
<add name="SessionAuthenticationModule" type="Microsoft.IdentityModel.Web.SessionAuthenticationModule, Microsoft.IdentityModel, Version=3.5.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" preCondition="managedHandler" />
</modules>
<handlers />
</system.webServer>
<system.serviceModel>
<services>
<service name="WebApplication15.Service1">
<endpoint address="" binding="basicHttpBinding"
contract="WebApplication15.IService1" />
<endpoint address="json" binding="webHttpBinding" behaviorConfiguration="jsonBehavior" contract="WebApplication15.IService1"/>
<endpoint address="mex" binding="mexHttpBinding" contract="IMetadataExchange" />
</service>
</services>
<behaviors>
<serviceBehaviors>
<behavior name="">
<serviceMetadata httpGetEnabled="true" />
<serviceDebug includeExceptionDetailInFaults="true" />
</behavior>
</serviceBehaviors>
<endpointBehaviors>
<behavior name="jsonBehavior">
<webHttp/>
</behavior>
</endpointBehaviors>
</behaviors>
<serviceHostingEnvironment aspNetCompatibilityEnabled="true" multipleSiteBindingsEnabled="true" />
</system.serviceModel>
<microsoft.identityModel>
<service name="OAuth">
<audienceUris>
<add value="http://www.code6421.com" />
</audienceUris>
<securityTokenHandlers>
<add type="Microsoft.Samples.DPE.OAuth.Tokens.SimpleWebTokenHandler, Microsoft.Samples.DPE.OAuth" />
</securityTokenHandlers>
<issuerTokenResolver type="Microsoft.Samples.DPE.OAuth.ProtectedResource.ConfigurationBasedIssuerTokenResolver, Microsoft.Samples.DPE.OAuth">
<serviceKeys>
<add serviceName="http://www.code6421.com" serviceKey="<your key>k+PzV04dEGgQ/9/vYP7rKrTAtTDUMhx42IWoLq/----=" />
</serviceKeys>
</issuerTokenResolver>
<issuerNameRegistry type="Microsoft.Samples.DPE.OAuth.ProtectedResource.SimpleWebTokenTrustedIssuersRegistry, Microsoft.Samples.DPE.OAuth">
<trustedIssuers>
<add issuerIdentifier="https://demoacs23.accesscontrol.windows.net/" name="demoacs23" />
</trustedIssuers>
</issuerNameRegistry>
</service>
</microsoft.identityModel>
</configuration>
有三個地方要注意
<audienceUris> <addvalue="http://www.code6421.com" /> </audienceUris> |
這裡要填入ACS中信賴憑證者應用程式領域的設定。
圖2
<addserviceName="http://www.code6421.com"serviceKey="<your key>k+PzV04dEGgQ/9/vYP7rKrTAtTDUMhx42IWoLq/----="/> |
這裡要填入憑證的對稱金鑰。
圖3
<trustedIssuers> <addissuerIdentifier="https://demoacs23.accesscontrol.windows.net/"name="demoacs23"/> </trustedIssuers> |
這裡要填入ACS的網址。
完成後將此Web應用程式部屬到IIS,就算完成一個僅能供擁有ACS所發出的Secure Token應用程式呼叫的WCF Service。
(PS: 注意,我特別把這個WCF Service設定為支援SOAP及REST,後者是為了方便Android/iOS呼叫)。
Windows Phone Consumer
接下來修改我們的DemoACS這個Windows Phone應用程式,加入對此WCF Service的Service Reference,然後撰寫呼叫WCF Service的程式碼。
private void PhoneApplicationPage_Loaded(object sender, RoutedEventArgs e)
{
if (!_rstrStore.ContainsValidRequestSecurityTokenResponse())
{
NavigationService.Navigate(new Uri("/SignInControl.xaml", UriKind.Relative));
}
else
{
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
var store = App.Current.Resources["rstrStore"] as SL.Phone.Federation.Utilities.RequestSecurityTokenResponseStore;
using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
{
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + store.SecurityToken;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
client.HelloWorldCompleted += (s, args) =>
{
Dispatcher.BeginInvoke(() =>
{
MessageBox.Show(args.Result);
});
};
client.HelloWorldAsync();
}
textBlock1.Text = "thanks for your login";
}
}
當呼叫這個透過Windows Identity Foundation整合,需Secure Token方能呼叫的WCF Service時,呼叫端必須把Secure Token放在HTTP Header中的Authorization區段,如下所示。
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " +
store.SecurityToken;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
一切正常的話,應該可以看到以下結果。
圖4
讀者們可以嘗試不經過驗證直接呼叫此WCF Service,會出現以下畫面。
圖5
Android Consumer
由於Android中並沒有很方便的SOAP Toolkit可以快速的使用SOAP來呼叫WCF Service,因此在先前設計這個WCF Service時,我們加入了REST協定,這樣Android就可以
輕易地呼叫這個WCF Servcie了。
package com.example.demoacsb;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;
import org.json.JSONObject;
import com.microsoft.samples.windowsazure.android.accesscontrol.core.IAccessToken;
import com.microsoft.samples.windowsazure.android.accesscontrol.login.AccessControlLoginActivity;
import android.os.Bundle;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.AlertDialog.Builder;
import android.view.Menu;
import android.view.MenuItem;
import android.support.v4.app.NavUtils;
public class SuccessLoginActivity extends Activity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_success_login);
IAccessToken accessToken = null;
Bundle extras = getIntent().getExtras();
if(extras != null) {
accessToken = (IAccessToken)extras.getSerializable(AccessControlLoginActivity.AuthenticationTokenKey);
try {
CallService(accessToken.getRawToken());
} catch (ClientProtocolException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (JSONException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
private void CallService(String token) throws ClientProtocolException, IOException, JSONException{
DefaultHttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet("http://192.168.1.124/WebSite1/Service1.svc/json/HelloWorld");
request.setHeader("Accept", "application/json");
request.setHeader("Content-type", "application/json");
request.setHeader("Authorization","OAuth " + token );
HttpResponse response = client.execute(request);
HttpEntity entity = response.getEntity();
if(entity.getContentLength() != 0) {
Reader jsonReader = new InputStreamReader(response.getEntity().getContent());
char[] buffer = new char[(int) response.getEntity().getContentLength()];
jsonReader.read(buffer);
jsonReader.close();
JSONObject jsonValues = new JSONObject(new String(buffer));
String s = jsonValues.getString("HelloWorldResult");
Builder MyAlertDialog = new AlertDialog.Builder(this);
MyAlertDialog.setTitle("Result");
MyAlertDialog.setMessage(s);
MyAlertDialog.show();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.activity_success_login, menu);
return true;
}
}
圖6
iOS Consumer
想透過iOS來呼叫REST/JSON的WCF Service,得先安裝一組JSON Framework,可以由以下網址取得。
https://github.com/stig/json-framework
DemoACSViewController.m
- (IBAction)loginAction:(id)sender {
WACloudAccessControlClient *acsClient = [WACloudAccessControlClient accessControlClientForNamespace:ACSNamespace realm:ACSRealm];
[acsClient showInViewController:self allowsClose:NO withCompletionHandler:^(BOOL authenticated) {
if (!authenticated) {
UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:@"Info" message:@"Login Fail" delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[dialog show];
} else {
NSURL *url = [NSURL URLWithString:@"http://192.168.1.124/WebSite1/Service1.svc/json/HelloWorld"];
NSMutableURLRequest *request = [[NSMutableURLRequest alloc] initWithURL:url];
NSString *oauthHeader = [[NSString alloc] initWithString:@"OAuth "];
NSString *s = [[NSString alloc] initWithString:[oauthHeader stringByAppendingString:[[WACloudAccessControlClient sharedToken] securityToken]]];
[request setValue:s forHTTPHeaderField:@"Authorization"];
NSURLConnection *conn = [[NSURLConnection alloc] initWithRequest:request delegate:self];
}
}];
}
-(void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
NSString *jsonString = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSDictionary *returnData = [jsonString JSONValue];
UIAlertView *dialog = [[UIAlertView alloc] initWithTitle:@"Info" message:[returnData objectForKey:@"HelloWorldResult"] delegate:self cancelButtonTitle:@"OK" otherButtonTitles:nil];
[dialog show];
}
圖7
Windows 8 Consumer
Windows 8的寫法與Windows Phone差不多,如下。
async void login_OnLogin(object sender, LoginEventArgs e)
{
ServiceReference1.Service1Client client = new ServiceReference1.Service1Client();
using (OperationContextScope scope = new OperationContextScope(client.InnerChannel))
{
var httpRequestProperty = new HttpRequestMessageProperty();
httpRequestProperty.Headers[System.Net.HttpRequestHeader.Authorization] = "OAuth " + e.Result.Token;
OperationContext.Current.OutgoingMessageProperties[HttpRequestMessageProperty.Name] = httpRequestProperty;
new MessageDialog(await client.HelloWorldAsync()).ShowAsync();
}
}
圖8