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

Token access, reuse and auto refresh within the SDK

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

令牌在 Authentication class 上设置为静态字段,并且每次到期前都会更新。

然后 SDK 中的其他参与者联系 Authentication class,读取它的 static 字段令牌,在调用 REST 端点之前放入他们的 Authorization headers。 SDK 中的所有参与者在每次调用中始终重复使用相同的令牌,直到它过期并自动获取更新的令牌。

这就是行为,我仍在努力思考几个概念,但我相信边做边学。

这个 F# 库将从 C# 调用,它将首先传递凭据,然后实例化其他 classes/actors 并在传递参数时调用它们的方法每个单独的方法。那些其他参与者将使用这个存储的令牌。

要点 基本上在 Authentication 中有两个静态字段,并允许在刷新其中一个静态字段(即 Token)时访问其他参与者。

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# 代码将 C# 行为转换为 F#。我已经构建了身份验证 class 并且在实现中遇到了一些问题,尤其是在静态成员和随后访问它们方面。此外,我想遵循函数式编程的规则,并了解它是如何在 F# 的 Functional World 中完成的。请帮助我在 F# 代码中翻译此行为。

这个问题的惯用函数方法是首先尝试摆脱全局状态。

有几种方法可以解决这个问题,但我认为最好的方法是提供一个 AuthenticationContext,其中包含您的 C# 代码在全局状态下保存的数据,并使每个调用 migth 更新凭据,return 其结果 连同可能更新的授权上下文 .

基本上,给定一个使用令牌进行 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

您可以让该方法的实施首先检查凭据是否需要更新,如果需要,则在 returning 之前更新它们。

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

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.