SDK中的令牌访问,重用和自动刷新

时间:2018-03-09 03:41:05

标签: c# f# c#-to-f#

来自C#和其他语言,以及F#的新手,并尝试移植我使用OO语言构建的SDK库。 SDK负责首先检索访问令牌,在静态字段上设置,然后设置特定间隔以在令牌到期之前连续刷新令牌。

令牌在Authentication类上设置为静态字段,并在每次到期之前更新。

然后,SDK中的其他actor会联系到Authentication类,读取它的static字段标记,在调用REST端点之前放置在Authorization headers中。 SDK中的所有参与者在每次调用时都会重复使用相同的令牌,直到它过期并且自动获取更新的一个。

这就是行为,我仍然试图围绕几个概念,但我相信在做这个时学习。

这个F#库将从C#调用,在那里它将通过Credentials开始,然后随后实例化其他类/ actor并在为每个单独的方法传递params时调用它们的方法。那些其他演员将使用这个存储的令牌。

要点基本上在Authentication中有两个静态字段,并在刷新其中一个静态字段(即令牌)时启用对其他actor的访问。

public class Authentication 
{
     public static Token token;
     public static Credentials credentials;
     public static Token RequestToken(Credentials credentials)
     {
           Authentication.credentials = credentials // cache for subsequent use
           // http REST call to access token based on credentials/api keys etc.
           // Authentication.token = someAccessTokenObject; // cache result

     }

     public static Token AddTokenObserver(Credentials credentials) 
     {
            this.RequestToken(credentials);
            // set interval, like call RequestToken every 24 hrs
     }
}

public class Class1 
{
     public someReturnObject RequestService1(someParams) {
           // accesses Authentication.credentials
           // accesses Authentication.token
           // places in the authorization headers 
           // and calls the web service
     }
      // + several other methods that repeats the same pattern
}

public class Class2
{
     public someReturnObject RequestService2(someParams) {
           // accesses Authentication.credentials
           // accesses Authentication.token
           // places in the authorization headers 
           // and calls the web service
     }
     // + several other methods that repeats the same pattern
}

使用SDK

// initialize SDK by passing credentials and enable auto refresh
Authentication.AddTokenObserver(someCredentials) // set token observer

Class1 c1 = new Class1();
c1.RequestService1(someObject1); // uses credentials & token from Authentication

Class c2 = new Class2();
c2.RequestService2(someObject2); // uses credentials & token from Authentication

我的F#尝试

type Credentials = {mutable clientId: string; mutable clientSecret: string;}
type Token = {mutable access_token: string; mutable refresh_token: string}

type Authentication =
    static member token = {access_token = ""; refresh_token = ""};
    static member credentials = {clientId = ""; clientSecret = "";}            
    new() = {}

    member this.RequestToken(credentials) =
        let data : byte[] = System.Text.Encoding.ASCII.GetBytes("");

        let host = "https://example.com";
        let url = sprintf "%s&client_id=%s&client_secret=%s" host credentials.clientId credentials.clientSecret

        let request = WebRequest.Create(url) :?> HttpWebRequest
        request.Method <- "POST"
        request.ContentType <- "application/x-www-form-urlencoded"
        request.Accept <- "application/json;charset=UTF-8"
        request.ContentLength <- (int64)data.Length

        use requestStream = request.GetRequestStream() 
        requestStream.Write(data, 0, (data.Length))
        requestStream.Flush()
        requestStream.Close()

        let response = request.GetResponse() :?> HttpWebResponse

        use reader = new StreamReader(response.GetResponseStream())
        let output = reader.ReadToEnd()

        printf "%A" response.StatusCode // if response.StatusCode = HttpStatusCode.OK throws an error

        Authentication.credentials.clientId <- credentials.clientId

        let t = JsonConvert.DeserializeObject<Token>(output)                            
        Authentication.token.access_token <- t.access_token
        Authentication.token.token_type <- t.token_type

        reader.Close()
        response.Close()
        request.Abort()

F#测试

    [<TestMethod>]    
    member this.TestCredentials() = 
        let credentials = {
            clientId = "some client id"; 
            clientSecret = "some client secret"; 
        }
        let authenticaiton = new Authentication()

        try
            authenticaiton.RequestToken(credentials) 
            printfn "%s, %s" credentials.clientId Authentication.credentials.clientId // Authentication.credentials.clientId is empty string
            Assert.IsTrue(credentials.clientId = Authentication.credentials.clientId) // fails
        with
            | :? WebException -> printfn "error";

问题

在上述单元测试中

Authentication.credentials.clientId is empty string
Assert fails

在调用令牌服务后,我无法在单元测试中访问静态成员。我如何一起接近这一点是有问题的。

我需要帮助在一些F#代码的帮助下翻译F#中的C#行为。我已经构建了Authentication类,并且在实现中存在一些问题,特别是在静态成员周围并随后访问它们。另外,我想遵循函数式编程的规则,并学习如何在F#中的Functional World中完成它。请帮我翻译F#代码中的这种行为。

1 个答案:

答案 0 :(得分:0)

解决这个问题的惯用功能方法是首先尝试摆脱全局状态。

有几种方法,但我认为最有效的方法是提供一个AuthenticationContext,其中包含C#代码保持全局状态的数据,并且每次调用 migth 续订凭据,将结果与可能更新的授权上下文一起返回

基本上,给定一种使用令牌进行API调用的方法

type MakeApiCall<'Result> = Token -> 'Result

我们想要创建这样的东西:

type AuthenticatedCall<'Result> = AuthenticationContext -> 'Result * AuthenticationContext

然后,您还可以让上下文跟踪是否需要续订(例如,通过存储上次续订时的时间戳,存储到期日期或其他内容),并提供两个功能

type NeedsRenewal = AuthenticationContext -> bool
type Renew = AuthenticationContext -> AuthenticationContext

现在,如果您获得带有功能的凭证

type GetAccessToken = AuthenticationContext -> Token * AuthenticationContext

您可以通过检查凭据是否需要续订来启动该方法的实施,如果是,则在返回之前更新它们。

因此,示例实现可能如下所示:

type AuthenticationContext = {
    credentials : Credentials
    token : Token
    expiryDate : DateTimeOffset
}

let needsRenewal context =
    context.expiryDate > DateTimeOffset.UtcNow.AddMinutes(-5) // add some safety margin

let renew context =
    let token = getNewToken context.Credentials
    let expiryDate = DateTimeOffset.UtcNow.AddDays(1)
    { context with token = token, expiryDate = expiryDate }

let getAccessToken context =
    let context' =
        if needsRenewal context
        then renew context
        else context

    return context'.token, context'

let makeAuthenticatedCall context makeApicall =
    let token, context' = getAccessToken context

    let result = makeApiCall token

    result, context'

现在,如果您每次拨打API电话,都可以访问上次通话中的AuthenticationContext,那么基础设施将负责为您续订令牌。

你会很快注意到这只会将问题推到跟踪身份验证上下文,并且你必须经常传递它。例如,如果要进行两次连续的API调用,则执行以下操作:

let context = getInitialContext ()

let resultA, context' = makeFirstCall context
let resultB, context'' = makeSecondCall context'

如果我们能够构建能够跟踪我们上下文的内容,那么我们不必传递它吗?

事实证明有一个functional pattern for this situation