The Windows Azure Toolkits – Integrated ACS with iOS、Windows Phone、Android and Windows 8 – Secure WCF Service

在前一篇文章中,我们让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,添加WAToolkitForWP7SamplesWP7.1LibrariesDPE.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






  
    

有三个地方要注意

       

这里要填入ACS中信赖凭证者应用程序领域的设定。

图2

这里要填入凭证的对称金钥。

图3

         

这里要填入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