בעיית ה – Cross domain היא בעיה ידועה ומוכרת אשר חוסמת פניות AJAX מדומיין אחד לדומיין שני ללא הרשאות מפורשות, כאשר מארחים WCF על IIS, ניתן לפתור את הנושא בעזרת הוספת Headers רלוונטיים ב – Web.config, לעומת זאת כאשר מארחים WCF ב – Windows Service לדוגמא, הדברים קצת מסתבכים.

    לפני הכול נסביר את הבעיה של Cross domain, התשובה שנשלפת מהמותן  לשאלה של “מה הבעייה” היא “בעיית אבטחה”, אבל עם טיפה מחשבה, נבין שלכאורה אין שום בעיה מ – AJAX לפנות לשרת שלנו שיהווה פרוקסי והוא יפנה לשרת המרוחק, ואז אין שום בעייה (הרי כך עובד כל Web Service) אז מה מיוחד בפניות AJAX ישירות, אם אנחנו למעשה כן יכולים להפעיל את המתודה.

    התשובה היא די פשוטה, WEB עובד שבכל פניה לשרת נשלחים אוטומטית כל ה – Cookies של הדומיין, כך שאם לצורך הדוגמא בנק הפועלים היה מבצע העברות בין חשבונות ב – AJAX ולא בריענון של הדף, היה אפשר לגנוב לנו כסף בצורה הבאה: (גם בריענון יש את הבעייה של גניבת Cookie, אבל עם זה מתמודדים בצורה אחרת)

    1. גלשנו לאתר של הבנק, הזדהינו (ואולי אפילו סגרנו את הדפדפן, ללא לחצן “יציאה”)
    2. גלשנו לאתר זדוני שמנסה לעשות פניית AJAX לבנק הפועלים ולשלוח את הפרטים.
    3. ה – Cookies של ה – login לאתר (של הבנק) נשלחים אוטומטית לבנק (והם תקפים במידה ולא לחצנו יציאה, או עברו X דקות)
    4. הכסף היה עובר.

    לעומת זאת עם האתר הזדוני היה שולח לשרת שלו את הבקשה ומשם היה מנסה להגיע לבנק הפועלים, לא היה לו דרך לדעת מה הם ה – Cookies הרלונטיים.

     

    זוהי הסיבה לחסימת CROSS, (הסבר על כל האפשרויות, כאן)

    כדי לאפשר זאת יש מספר דברים שניתן לעשות (תלוי בטכנולוגיה בה אנחנו משתמשים) הדרך הרגילה ב – WEB היא להוסיף ל – web.config את ההגדרות הבאות:

    Code Snippet
    <httpProtocol>
      <customHeaders>
        <add name="Access-Control-Allow-Origin" value="*"/>
        <add name="Access-Control-Allow-Methods" value="*"/>
        <add name="Access-Control-Allow-Headers" value="*"/>
      </customHeaders>
    </httpProtocol>

     

    ה – header הראשון מאפשר לכל הדומיינים להפעיל AJAX, (ניתן גם להגדיר דומיינים מסויימים)

    הסיבה לשני הנוספים, היא מכיוון שבסיטואציות מסויימות הדפדפן יעשה Request מקדים (הנקרא Preflighted requests) אשר יוודא לפני הפעלת הבקשה עצמה האם מותר לו להריץ את הבקשה, הוא יעשה זאת בעזרת קריאת OPTIONS, וצריך לאשר גם אותה.

    עד כאן בקצרה על התהליך.

     

    ב – WCF שמתארח ב – Windows הקוד לעיל לא כל כך יעזור, ולכן כתבתי CustomBehavior שיאפשר זאת, הוקנפיג של ה – WCF נראה כך:

    Code Snippet
    <system.serviceModel>
      <services>
        <service name="ConsoleApplication1.Service">
          <endpoint address="http://localhost:12345/Service"
                    binding="webHttpBinding"
                    contract="ConsoleApplication1.IService"></endpoint>
        </service>
      </services>

      <behaviors>
        <serviceBehaviors>
          <behavior>
            <serviceDebug includeExceptionDetailInFaults="true"/>
          </behavior>
        </serviceBehaviors>
        <endpointBehaviors>
          <behavior>
            <webHttp faultExceptionEnabled="true" defaultOutgoingResponseFormat="Json"  />
            <crossDomain />
          </behavior>
        </endpointBehaviors>
      </behaviors>
      <extensions>
        <behaviorExtensions>
          <add name="crossDomain" type="ConsoleApplication1.CrossDomainExtensionElement, ConsoleApplication1" />
        </behaviorExtensions>
      </extensions>
    </system.serviceModel>

     

    • כדי לאפשר לעבוד ב – WEB ה – Binding צריך להיות מסוג wsHttpBindgin ודומיו (לא basic)
    • צריך להוסיף behavior מסוג webHttp, ומומלץ להגדיר שמה שחוזר הוא JSON.
    • לאחר מכן הוספתי crossDomain שהוא behaviorExtensions שמופיע בסוף ההגדרה מסוג המחלקה CrossDomainExtensionElement שמיד נראה אותה. (קצת ארוך…)

     

    Code Snippet
    public class CrossDomainExtensionElement : BehaviorExtensionElement
    {
        public override Type BehaviorType
        {
            get
            {
                return typeof(CrossDomainBehavior);
            }
        }

        protected override object CreateBehavior()
        {
            return new CrossDomainBehavior();
        }
    }

    public class CrossDomainBehavior : IEndpointBehavior
    {
        public void AddBindingParameters(ServiceEndpoint endpoint, BindingParameterCollection bindingParameters)
        {
        }

        public void ApplyClientBehavior(ServiceEndpoint endpoint, ClientRuntime clientRuntime)
        {
        }

        public void ApplyDispatchBehavior(ServiceEndpoint endpoint, EndpointDispatcher endpointDispatcher)
        {
            ChannelDispatcher channelDispatcher = endpointDispatcher.ChannelDispatcher;
            if (channelDispatcher != null)
            {
                foreach (EndpointDispatcher ed in channelDispatcher.Endpoints)
                {
                    ed.DispatchRuntime.MessageInspectors.Add(new CrossDomainMessageInspector());
                }
            }
        }

        public void Validate(ServiceEndpoint endpoint)
        {
        }
    }

    public class CrossDomainMessageInspector : IDispatchMessageInspector
    {
        private static IDictionary<string, string> headers = new Dictionary<string, string>
            {
            { "Access-Control-Allow-Origin", "*" },
            { "Access-Control-Allow-Methods", "*" },
            { "Access-Control-Allow-Headers", "content-type" }
            };

        public object AfterReceiveRequest(ref Message request, IClientChannel channel, InstanceContext instanceContext)
        {
            foreach (var item in headers)
            {
                WebOperationContext.Current.OutgoingResponse.Headers.Add(item.Key, item.Value);
            }

            return null;
        }

        public void BeforeSendReply(ref Message reply, object correlationState)
        {
        }
    }

     

    • המחלקה CrossDomainExtensionElement מגדירה שצריך להפעיל את CrossDomainBehavior
    • המחלקה CrossDomainBehavior מוסיפה Dispatchr לכל ה – endpoints את CrossDomainMessageInspector
    • מחלקת CrossDomainMessageInspector דואגת לכך שבכל בקשה מוסיפים את ה – headers הרלוונטיים.

     

    הבעייה היחידה עם זה שזה יעבוד רק בפניות GET, משום מה בפניות POST הוא מתעלם מהקוד הזה וזורק שגיאות, כדי לפתור את זה, השירות צריך להיראות כך:

    Code Snippet
    [ServiceContract]
    public interface IService
    {
        [OperationContract]
        [WebGet]
        int Add(int a, int b);

        [OperationContract]
        [WebGet]
        Person GetPerson();

        [OperationContract]
        [WebInvoke(Method = "*")]
        Person SetPerson(Person person);
    }

     

    עבור פניות שנאפשר POST נצטרך להגדיר את המאפיין Method כ – *.

     

    דוגמאות לפניות AJAX

    Code Snippet
    function clickme() {
        $.getJSON('http://localhost:12345/Service/add?a=1&b=2', function (res) {
            console.log(res);
        })
    }

    function clickme1() {
        $.getJSON('http://localhost:12345/Service/getperson', function (res) {
            console.log(res);
        })
    }

    function clickme2() {
        var obj = {
            Id: 5,
            Name: 'Hello',
            Date: '\/Date(231423432432)\/'
        };

        $.ajax({
            url: 'http://localhost:12345/Service/SetPerson',
            data: JSON.stringify(obj),
            method: 'post',
            contentType: 'application/json',
            success: function (res) {
                console.log(res);
            }
        });
    }

     

    יש לשים לב במתודה השלישית ש – WCF היא case sensitive והאובייקטים הנשלחים חייבים להתאים בשמות, בנוסף תאריכים צריך לשלוח בפורמט של מייקרוסופט.