Bla-Bla-Car Server.
На ціей сторінці я розповім про мій сервер відомого французького сервісу BlaBlaCar. Для починаючих програмістів цей код може бути цікавим у декількох аспектах.
Отже почнемо з першого моменту - найпростішої технології аутентифікації, що можлива у ASP.NET взагалі без бази. Та побіжно подивимося взагалі на структуру цього проєкту.
Як ви можете бачити на скрині, усі публічно доступні сторінки доступні у головному каталозі проєкту. За винятком двох сторінок, що знаходяться у папці Internal. Саме ці дві сторінки потребують аутентифікації. Зроблено це за допомогою такого простого конфігу:
1: <?xml version="1.0"?>
2:
3: <configuration>
4: <appSettings>
5: <add key="AU_TestMode" value="false"/>
6: <add key="AU_URL" value="https://api.blablacar.com/oauth/v2/access_token?grant_type=client_credentials"/>
7: <add key="ClientID" value="111111111111111111111111111111111111111111111111111111111111111111"/>
8: <add key="ClientSecret" value="22222222222222222222222222222222222222222222222222222222222222"/>
9: <add key="API_URL" value="https://api.blablacar.com/api/v2/trips"/>
10: </appSettings>
11: <system.web>
12: <compilation debug="true" strict="false" explicit="true" targetFramework="4.0" />
13: <customErrors mode="Off"/>
14:
15: <authentication mode="Forms" >
16: <forms loginUrl="~/Login.aspx" timeout="2880" name=".ASPXFORMSAUTH" defaultUrl="~/Default.aspx" >
17: <credentials passwordFormat="Clear">
18: <user name="User1" password="SecretPassword" />
19: </credentials>
20: </forms>
21: </authentication>
22:
23: <authorization>
24: <allow users="*"/>
25: <deny users="?" />
26: </authorization>
27:
28: </system.web>
29:
30: <location path="Internal">
31: <system.web>
32: <authorization>
33: <allow users="User1" />
34: <deny users="*, ?" />
35: </authorization>
36: </system.web>
37: </location>
38:
39: <system.webServer>
40: <modules runAllManagedModulesForAllRequests="true"/>
41: </system.webServer>
42:
43: </configuration>
І дуже простої сторінці Login:
1: <%@ Page Title="" Language="vb" AutoEventWireup="false" MasterPageFile="~/Site.Master" CodeBehind="Login.aspx.vb" Inherits="BlaBlaCarTest.Login" %>
2:
3: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
4:
5: <h2>на эту страничку требуется вход</h2>
6: <br />
7: <asp:TextBox ID="Login1" runat="server" ></asp:TextBox> Логин
8: <br />
9: <asp:TextBox ID="Pass1" runat="server" TextMode="Password" ></asp:TextBox> Пароль
10: <br /><br />
11: <asp:Button ID="Button1" runat="server" Text="Войти" Width="100px" />
12:
13: </asp:Content>
1: Public Class Login
2: Inherits System.Web.UI.Page
3:
4: Protected Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click
5: If FormsAuthentication.Authenticate(Login1.Text, Pass1.Text) Then
6: FormsAuthentication.RedirectFromLoginPage(Login1.Text, False)
7: End If
8: End Sub
9: End Class
Таким простим чином ці дві сторінки виявляються захищеними від публічного огляду. Власне захист прописан у стрічках 15-37 конфігу.
Теперь подивимося на механізм оновлення кешу. Починається все з того, що прі старті веб-вузла запускається процес CacheTimer.ReStart
1: Public Class Global_asax
2: Inherits System.Web.HttpApplication
3:
4: Sub Application_Start(ByVal sender As Object, ByVal e As EventArgs)
5: Dim CacheTimer As New SetKeyToCache
6: CacheTimer.ReStart()
7: End Sub
8:
9: ...
1: Public Class SetKeyToCache
2:
3: Public Sub ReStart()
4: GetTicket()
5: End Sub
6:
7:
8: Public Sub GetTicket()
9: Dim expires_in As Integer
10: Dim access_token As String = ""
11: Dim ExperiTime As System.TimeSpan
12: Dim Err_MSG As String = ""
13: Dim ENCODED_CLIENTID_SECRET As String = ""
14: '
15: 'тестирвоание разніх вариантов ответов сервиса
16: If CBool(System.Configuration.ConfigurationManager.AppSettings("AU_TestMode")) Then
17: 'ENCODED_CLIENTID_SECRET = "{'expires_in':86400,'access_token':'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE0Mzc5OTgwNTcsImNsaWVudCI6IjFhODRhN2JlZmM5OTUyNGExOTM4MTg3YTZkZWEzMjE5Y2MyYTYxMWEwMWIwMWEwOGU4NmQwMjY1YWM4NTY4NTIiLCJzY29wZSI6IiIsInVzZXIiOm51bGx9.r3Y6I0_0VXKWKqjTK_1FOFLf9fY0x03PNs3Mb248zFE'}"
18: ENCODED_CLIENTID_SECRET = "{'error':{'code':'x','message':'This access token is no longer available !','developper-message':'access_token.violation','support-link':'https:\/\/api.blablacar.ru\/kontakt'}}"
19: Else
20: ENCODED_CLIENTID_SECRET = (New GetKeyFromPhpRest).GetSecretToken
21: End If
22: '
23: If Not ENCODED_CLIENTID_SECRET.StartsWith("{") Then
24: 'если ответ вообще не похож на JSON - єто явная ошибка
25: Err_MSG = ENCODED_CLIENTID_SECRET
26: ENCODED_CLIENTID_SECRET = ""
27:
28: ElseIf ENCODED_CLIENTID_SECRET.StartsWith("{""error") Or ENCODED_CLIENTID_SECRET.StartsWith("{'error") Then
29: 'если из токена прилетел Error
30: Try
31: 'и если Error нормально парсится
32: Dim JSS = New Script.Serialization.JavaScriptSerializer()
33: Dim JSS_Obj = JSS.DeserializeObject(ENCODED_CLIENTID_SECRET)
34: Err_MSG = JSS.DeserializeObject(ENCODED_CLIENTID_SECRET)("error")("message")
35: ENCODED_CLIENTID_SECRET = ""
36: Catch ex As Exception
37: Err_MSG = ex.Message & ":" & ENCODED_CLIENTID_SECRET
38: ENCODED_CLIENTID_SECRET = ""
39: End Try
40: Else
41: Try
42: 'если нет Error и expires_in и access_token нормально парсятся
43: Dim JSS = New Script.Serialization.JavaScriptSerializer()
44: Dim JSS_Obj = JSS.DeserializeObject(ENCODED_CLIENTID_SECRET)
45:
46: expires_in = JSS_Obj("expires_in")
47: access_token = JSS_Obj("access_token")
48: ExperiTime = New System.TimeSpan(0, 0, expires_in - 10)
49: Err_MSG = ""
50: Catch ex As Exception
51: Err_MSG = ex.Message & ":" & ENCODED_CLIENTID_SECRET
52: ENCODED_CLIENTID_SECRET = ""
53: End Try
54: End If
55: '
56: If ENCODED_CLIENTID_SECRET = "" Or Err_MSG <> "" Then
57: 'хорошего токена нет, есть ошибка
58: System.Web.HttpRuntime.Cache.Add("SECRET_TOKEN_ERROR", Err_MSG, Nothing, System.Web.Caching.Cache.NoAbsoluteExpiration, New System.TimeSpan(0, 0, 30), CacheItemPriority.Normal, AddressOf ReStart)
59: System.Web.HttpRuntime.Cache.Add("SECRET_TOKEN", "", Nothing, System.Web.Caching.Cache.NoAbsoluteExpiration, New System.TimeSpan(10, 0, 0), CacheItemPriority.Normal, AddressOf ReStart)
60: Else
61: 'есть хороший токен
62: System.Web.HttpRuntime.Cache.Add("SECRET_TOKEN_ERROR", "", Nothing, System.Web.Caching.Cache.NoAbsoluteExpiration, New System.TimeSpan(0, 0, 30), CacheItemPriority.Normal, AddressOf ReStart)
63: System.Web.HttpRuntime.Cache.Add("SECRET_TOKEN", access_token, Nothing, System.Web.Caching.Cache.NoAbsoluteExpiration, ExperiTime, CacheItemPriority.Normal, AddressOf ReStart)
64: End If
65: End Sub
66:
67:
68: End Class
У цьому коді важливо зрозуміти його рекурсивну структуру. Тобто коли кеш застаріває, сам класс 58-63 викликае сам себе. Це не зовсім коректний метод, бо глубина стека виклику буде наростати, і коли наросте до максимуму - мабуть 32 рекурсивних виклика - прога або сайт упаде і рестартує спочатку. Але рециклінг сайта IIS робить сам по собі раз у день, і при досить великому часу життя токена глубина наростання стека не доходить до 32-х. На практиці від цього сервісу приходить срок життя токену майже на день i до перезапуску веб-вузлу через надмірне наростання стеку рекурсії викликів - не доходить.
Взагалі це найбільше спрощення у цьому сервері - розрахунок на рециклінг IIS до того, як переповніться стек рекурсії. Більш корректно було б самому обробити переповнення стеку і знов запустити рестарт оновлення кешу.
Останній код цього класу, мабуть коментарів не потребує - (New GetKeyFromPhpRest).GetSecretToken - віддає чи токен, чи помилку. Це аналізується і формується кеш з токеном чи помилкою. У цьому класі є і тестовий режим, щоб не звертатися до сервісу за токеном занадто часто прі налагодженні цього сайту.
Теперь подивимось далі на ключовий класс GetKeyFromPhpRest, який відає SecurityToken від серверу авторизація.
1: Public Class GetKeyFromPhpRest
2:
3: Dim BlaBlaCarAU_URL As String = System.Configuration.ConfigurationManager.AppSettings("AU_URL") '"https://api.blablacar.com/oauth/v2/access_token?grant_type=client_credentials"
4: Dim ClientIDString As String = System.Configuration.ConfigurationManager.AppSettings("ClientID") ' "1111111111111111111111111111111111111111111111111111111111111111"
5: Dim ClientSecretString As String = System.Configuration.ConfigurationManager.AppSettings("ClientSecret") '"2222222222222222222222222222222222222222222222222222222222222222"
6:
7: Public Function GetSecretToken() As String
8: Try
9: Dim AU As String = ClientIDString & ":" & ClientSecretString
10:
11: Dim byteArray As Byte()
12: byteArray = System.Text.Encoding.UTF8.GetBytes(AU)
13:
14: Dim POST_String As String = Convert.ToBase64String(byteArray)
15:
16: '========== System.NotSupportedException The URI prefix is not recognized.
17: Dim request As Net.HttpWebRequest = Net.HttpWebRequest.Create(BlaBlaCarAU_URL)
18: request.Method = "POST"
19: request.ContentType = "application/octet-stream"
20: request.Headers.Add("Authorization", "Basic " & POST_String)
21: request.ContentLength = 0
22: '========== System.Net.WebExceptionStatus.Timeout Unable to connect to the remote server
23: Dim POST_Stream As IO.Stream = request.GetRequestStream()
24: POST_Stream.Close()
25: 'ждем
26: '========== System.Net.WebException.Timeout
27: '========== System.Net.WebException = "The remote server returned an error: (404) Not Found."
28: '========== The remote server returned an error: (403) Forbidden.
29: Dim response As Net.HttpWebResponse = request.GetResponse()
30: Dim GET_Stream As IO.Stream = response.GetResponseStream()
31: Dim reader As IO.StreamReader
32: reader = New IO.StreamReader(GET_Stream, System.Text.UTF8Encoding.GetEncoding("windows-1251"))
33: Dim ENCODED_CLIENTID_SECRET As String = reader.ReadToEnd
34:
35: reader.Close()
36: GET_Stream.Close()
37: response.Close()
38:
39: Return ENCODED_CLIENTID_SECRET
40: Catch ex As Exception
41: Return ex.Message
42: End Try
43: End Function
44:
45: End Class
У цьому класі багато цікавих моментів. По перше, цікава технологія REST, у якій ключ передається до серверу не у тілі реквесту а у хеадері.
У відповідь видається або JSON, який має SecurityToken та період валідності токену, або помилка.
Клас, що ми розглядали попереду - аналізує цю відповідь і закладає у кеш або токен доступу, або помилку для діагностики.
Ще одна цікава особливість цього класу, що на бейсіке потрібно зробити точно таке перетворювання строк, яке роблять звичайні PHP-функціі string base64_encode ( string $str ). Але звідки мені знати, як працює PHP?
Допоміг мені зрозуміти, як працює PHP - ось цей сервіс http://www.tools4noobs.com/online_php_functions/base64_encode/ - а ключове перекодування цього класу, еквівалентне PHP-функціі - я зробив у стрічках 11-14 вище.
Ну а далі все занадто просто і описувати вже особливо нічого. Я зробив ось такий хандлер BlaBlaCarInfo.ashx
1: Imports System.Web
2: Imports System.Web.Services
3:
4: Public Class BlaBlaCarInfo
5: Implements System.Web.IHttpHandler
6:
7: Sub ProcessRequest(ByVal context As HttpContext) Implements IHttpHandler.ProcessRequest
8: Dim fn As String = ""
9: Dim tn As String = ""
10: Dim db As String = ""
11: Dim de As String = ""
12: 'перекодировка из unicode escape string в строки NET делается автоматически
13: fn = context.Request.Params("fn")
14: tn = context.Request.Params("tn")
15: db = context.Request.Params("db")
16: de = context.Request.Params("de")
17: Try
18: If System.Web.HttpRuntime.Cache("SECRET_TOKEN") Is Nothing Then
19: Dim CacheTimer As New SetKeyToCache
20: CacheTimer.ReStart()
21: End If
22: 'сделали запрос - ошибки быть не должно - Nothing или ""
23: If System.Web.HttpRuntime.Cache("SECRET_TOKEN_ERROR") Is Nothing Then
24: System.Web.HttpRuntime.Cache("SECRET_TOKEN_ERROR") = ""
25: End If
26: 'если ошибки нет - обработка продолжается, иначе ошибка
27: If System.Web.HttpRuntime.Cache("SECRET_TOKEN_ERROR") = "" Then
28: '
29: Dim Info As New GetInfoFromPhpRest
30: Dim HTML As String = Info.GetFullInfo(System.Web.HttpRuntime.Cache("SECRET_TOKEN"), fn, tn, db, de)
31:
32: context.Response.ContentType = "application/json"
33: context.Response.Write(HTML)
34: Else
35: context.Response.ContentType = "text/plain"
36: context.Response.Write(System.Web.HttpRuntime.Cache("SECRET_TOKEN_ERROR"))
37: End If
38: Catch ex As Exception
39: context.Response.ContentType = "text/plain"
40: context.Response.Write(ex.Message)
41: End Try
42: End Sub
43:
44: ReadOnly Property IsReusable() As Boolean Implements IHttpHandler.IsReusable
45: Get
46: Return False
47: End Get
48: End Property
49:
50: End Class
До якого звертається код jQuery на головній формі Default.aspx
1: <%@ Page Title="" Language="vb" AutoEventWireup="false" MasterPageFile="~/Site.Master"
2: CodeBehind="Default.aspx.vb" Inherits="BlaBlaCarTest._Default_" %>
3:
4: <asp:Content ID="Content2" ContentPlaceHolderID="MainContent" runat="server">
5: <h2>
6: AJAX client</h2>
7: <br />
8: <asp:TextBox ID="fn" runat="server" ClientIDMode="Static" Text="Москва"></asp:TextBox>
9: откуда
10: <br />
11: <asp:TextBox ID="tn" runat="server" ClientIDMode="Static" Text="Архангельск"></asp:TextBox>
12: куда
13: <br />
14: <asp:TextBox ID="db" runat="server" ClientIDMode="Static" Text="13-08-2015"></asp:TextBox>
15: от
16: <br />
17: <asp:TextBox ID="de" runat="server" ClientIDMode="Static" Text="30-08-2015"></asp:TextBox>
18: до<br />
19: <br />
20: <script type="text/javascript">
21:
22: var callback = function parse(data) {
23: $('<div>всего предложений - ' + data.pager.total + '</div>').insertAfter('#res1');
24: var i = 1;
25: $(data.trips).each(
26: function () {
27: var one = this;
28: i = i + 1;
29: $('<div id="res' + i + '">' + one.price.string_value + ' : ' + one.arrival_place.address + ' : ' + one.links._front + '</div>').insertAfter('#res1');
30: });
31: };
32:
33: function ajax_request(parseresult) {
34: $.ajax({
35: url: 'BlaBlaCarInfo.ashx',
36: method: "GET",
37: data: {
38: fn: fn.value,
39: tn: tn.value,
40: db: db.value,
41: de: de.value
42: },
43: dataType: 'json',
44: success: parseresult,
45: error: function (data) {
46: alert(data);
47: }
48: });
49: };
50:
51: </script>
52: <input type="button" onclick="ajax_request(callback)" />
53: <br />
54: <div id="res1">
55: </div>
56: </asp:Content>
Цей код на jQuery робить не тільки реквест до хандлеру BlaBlaCarInfo.ashx, але й парсить відповідь серверу (яку ви бачите нище), яку транзитом хандлер відправляє у канал браузеру.
Слово транзитом тут підкреслено, бо у мене були сумніви - що важливіше меня у цьому рішенні - швидкість чи гнучкість. Можна б було, наприклад, распарсити відповідь серверу прямо у хандлері BlaBlaCarKey.ashx за допомогою JSON-парсера бейсіка, як я це зробив у стрічках 43-44 класу SetKeyToCache - і відправити у браузер лише потрібні дані. Але хто знає, що потрібно буде змінити в цьому коді з часом. Взагалі, як ви розумієте, клієнт тут умовний. Тому я відправив усе і розпарсів відповідь серверу напрямки у браузері (у стрічці 29 jQuery).
|