提问者:小点点

为什么缓存访问令牌在 oauth2 中被认为是不好的?


我正在按照这篇文章撤销用户访问权限:

http://bitoftech . net/2014/07/16/enable-oauth-refresh-tokens-angular js-app-using-ASP-net-we b-API-2-owin/

现在考虑一下,在验证用户之后,我已经发布了一个访问令牌,使用期限为30分钟,如上面的文章所示,刷新令牌为1天,但是如果管理员在10分钟内删除该用户,还有20分钟,那该怎么办,所以在这种情况下,我需要撤销对该用户的访问。

为此,我需要从刷新令牌表中删除该用户条目,以禁止进一步的访问令牌请求,但由于访问令牌到期时间仍有20分钟,因此用户将能够访问受保护的资源,这是完全错误的。

所以我正在考虑实现缓存机制以在服务器上缓存访问令牌并保存在数据库中。因此,当该用户被撤销时,我可以简单地从缓存和数据库中删除该用户条目,以阻止该用户访问受保护的资源。

但下面的2个答案表明,oauth2不是这样设计的:

撤销 OAuthBearer身份验证的访问令牌

OAuth2-刷新令牌的不必要复杂性

所以我的问题是:

1) 为什么缓存访问令牌不被认为比刷新令牌机制更好,也是一种糟糕的方法?

我的第二个问题是基于@Hans Z的以下回答,他在回答中说:

这必然涉及资源服务器(RS)咨询授权服务器(AS ),这是一个巨大的开销。

2) 如果撤销用户的访问权限,为什么 RS 会咨询 AS,因为 AS 仅用于根据本文对用户进行身份验证和生成访问令牌?

3)文章中只有2个项目:

> < li>Authentication.api -验证用户并生成访问令牌 < li>

资源服务器-借助< code>[Authorize]属性验证accesstoken

在上述情况下,哪个是授权服务器?

更新:我已经决定使用刷新令牌来撤销用户访问权限,以防用户被删除,并且当用户注销时,我将从刷新令牌表中刷新令牌,因为您要求我们在用户单击注销后立即注销用户。

但这里的问题是我有 250 个与用户关联的角色,所以如果我将角色放在 accesstoken 中,那么访问令牌的大小将非常大,我们无法从标头传递如此巨大的访问令牌,但我无法查询角色以验证用户对endpoint的访问每次调用该endpoint时。

所以这是我面临的另一个问题。


共3个答案

匿名用户

这里似乎有两个不同的问题:关于访问令牌和关于大角色列表。

访问令牌

OAuth2被设计成能够处理高负载,这需要一些权衡。特别是这就是为什么OAuth2一方面明确区分“资源服务器”和“授权服务器”角色,另一方面明确区分“访问令牌”和“刷新令牌”的原因。如果对于每个请求,您都必须检查用户授权,这意味着您的授权服务器应该能够处理系统中的所有请求。对于高负载系统,这是不可行的。

OAuth2允许您在性能和安全性之间进行以下权衡:授权服务器生成一个访问令牌,资源服务器可以在不访问授权服务器的情况下对其进行验证(在授权服务器的生命周期内完全或至少不超过一次)。这实际上是授权信息的缓存。因此,通过这种方式,您可以大大减少授权服务器的负载。与缓存一样,缺点也是一样的:授权信息可能会停止。通过改变访问令牌的生命周期,您可以调整性能与安全平衡。

如果您使用微服务架构,其中每个服务都有自己的存储并且不访问彼此的存储,这种方法也可能会有所帮助。

但是,如果您没有太多负载并且只有单个资源服务器,而不是使用不同技术实现的大量不同服务,则没有什么可以阻止您对每个请求进行实际的全面验证。即是的,您可以将访问令牌存储在数据库中,在每次访问资源服务器时对其进行验证,并在删除用户时删除所有访问令牌等。但正如@Evk注意到的那样,如果这是您的场景 - OAuth2 对您来说是一个超调。

角色大列表

AFAIU OAuth2没有为用户角色提供明确的特性。有一个“作用域”特性也可以用于角色,它的典型实现会产生250个角色的太长的字符串。OAuth2仍然没有为访问令牌明确指定任何特定的格式,所以您可以创建一个自定义令牌,将角色信息作为位掩码保存。使用base-64编码,您可以在一个字符中包含6个角色(64 = 2^6).所以250-300个角色可以用40-50个字符来管理。

JWT公司

由于您可能无论如何都需要一些自定义令牌,因此您可能会对JSON Web令牌(又名JWT)感兴趣。简而言之,JWT 允许您指定自定义附加有效负载(专用声明)并将您的角色位掩码放在那里。

如果您真的不需要任何 OAuth2 高级功能(例如作用域),您实际上可以单独使用 JWT,而无需完整的 OAuth2 内容。尽管 JWT 令牌应该仅通过内容进行验证,但您仍然可以将它们存储在本地数据库中,并对数据库进行其他验证(就像您将对访问刷新令牌所做的那样)。

更新2017年12月1日

如果您要使用OWIN OAuth基础结构,您可以通过< code > oauthbeareratuthenticationoptions 和< code > OAuthAuthorizationServerOptions 中的< code>AccessTokenFormat自定义令牌格式。您也可以覆盖< code>RefreshTokenFormat。

下面是一个草图,展示了如何将角色声明“压缩”为单个位掩码:

  1. 定义列出您拥有的所有角色的自定义角色枚举
[Flags]
public enum CustomRoles
{
    Role1,
    Role2,
    Role3,

    MaxRole // fake, for convenience
}
    public static string EncodeRoles(IEnumerable<string> roles)
    {
        byte[] bitMask = new byte[(int)CustomRoles.MaxRole];
        foreach (var role in roles)
        {
            CustomRoles roleIndex = (CustomRoles)Enum.Parse(typeof(CustomRoles), role);
            var byteIndex = ((int)roleIndex) / 8;
            var bitIndex = ((int)roleIndex) % 8;
            bitMask[byteIndex] |= (byte)(1 << bitIndex);
        }
        return Convert.ToBase64String(bitMask);
    }

    public static IEnumerable<string> DecodeRoles(string encoded)
    {
        byte[] bitMask = Convert.FromBase64String(encoded);

        var values = Enum.GetValues(typeof(CustomRoles)).Cast<CustomRoles>().Where(r => r != CustomRoles.MaxRole);

        var roles = new List<string>();
        foreach (var roleIndex in values)
        {
            var byteIndex = ((int)roleIndex) / 8;
            var bitIndex = ((int)roleIndex) % 8;
            if ((byteIndex < bitMask.Length) && (0 != (bitMask[byteIndex] & (1 << bitIndex))))
            {
                roles.Add(Enum.GetName(typeof(CustomRoles), roleIndex));
            }
        }

        return roles;
    }
public class CustomTicketSerializer : IDataSerializer<AuthenticationTicket>
{

    public const string RoleBitMaskType = "RoleBitMask";
    private readonly IDataSerializer<AuthenticationTicket> _standardSerializers = DataSerializers.Ticket;

    public static SecureDataFormat<AuthenticationTicket> CreateCustomTicketFormat(IAppBuilder app)
    {
        var tokenProtector = app.CreateDataProtector(typeof(OAuthAuthorizationServerMiddleware).Namespace, "Access_Token", "v1");
        var customTokenFormat = new SecureDataFormat<AuthenticationTicket>(new CustomTicketSerializer(), tokenProtector, TextEncodings.Base64Url);
        return customTokenFormat;
    }

    public byte[] Serialize(AuthenticationTicket ticket)
    {
        var identity = ticket.Identity;
        var otherClaims = identity.Claims.Where(c => c.Type != identity.RoleClaimType);
        var roleClaims = identity.Claims.Where(c => c.Type == identity.RoleClaimType);
        var encodedRoleClaim = new Claim(RoleBitMaskType, EncodeRoles(roleClaims.Select(rc => rc.Value)));
        var modifiedClaims = otherClaims.Concat(new Claim[] { encodedRoleClaim });
        ClaimsIdentity modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
        var modifiedTicket = new AuthenticationTicket(modifiedIdentity, ticket.Properties);
        return _standardSerializers.Serialize(modifiedTicket);
    }

    public AuthenticationTicket Deserialize(byte[] data)
    {
        var ticket = _standardSerializers.Deserialize(data);
        var identity = ticket.Identity;
        var otherClaims = identity.Claims.Where(c => c.Type != RoleBitMaskType);
        var encodedRoleClaim = identity.Claims.SingleOrDefault(c => c.Type == RoleBitMaskType);
        if (encodedRoleClaim == null)
            return ticket;

        var roleClaims = DecodeRoles(encodedRoleClaim.Value).Select(r => new Claim(identity.RoleClaimType, r));
        var modifiedClaims = otherClaims.Concat(roleClaims);
        var modifiedIdentity = new ClaimsIdentity(modifiedClaims, identity.AuthenticationType, identity.NameClaimType, identity.RoleClaimType);
        return new AuthenticationTicket(modifiedIdentity, ticket.Properties);
    }
}
var customTicketFormat = CustomTicketSerializer.CreateCustomTicketFormat(app);
OAuthBearerOptions.AccessTokenFormat = customTicketFormat;
OAuthServerOptions.AccessTokenFormat = customTicketFormat;

在<code>OAuthAuthorizationServerProvider</code>中添加<code>ClaimTypes。分配给用户的每个角色的角色到ClaimsIdentity

在控制器中使用标准的< code>AuthorizeAttribute,例如

[Authorize(Roles = "Role1")]
[Route("")]
public IHttpActionResult Get()

为了方便和一些安全,您可以子类 AuthorizeAttribute 类以接受 CustomRoles 枚举而不是字符串作为角色配置。

匿名用户

我希望我回答对了你的问题,并能提供一些答案:

1) 如果您开发的AS要求在每次用户登录时进行验证,则可以将其兑现。

2)我认为@Hans Z.的意思是由AS撤销用户。当RS撤销用户时,它不会改变他们仍然是AS识别的用户的事实。但是当AS撤销用户时,它会阻止他们使用他们的身份。

3) 本文可能假设授权是由 RS 完成的,AS 只负责告诉您谁是用户,RS 将据此决定授权。

匿名用户

刷新令牌方法的主要优点是减少数据库查询的数量,访问令牌具有声明和签名,因此无需查询数据库即可信任令牌。

缓存访问令牌将起作用,但随后您必须查询每个请求的缓存。

您必须在访问权限更改的n分钟延迟和检查访问令牌有效性的查询数量之间进行权衡

随着复杂性的增加,您几乎可以实现这两个目标。在这种情况下,您必须将缓存存储在服务器RAM中,并且只存储已撤销的令牌以保持列表较小。当您有多个服务器实例时,您将不得不在您的RS和AS之间保持已撤销令牌的缓存同步,这会带来复杂性。

基本上,当访问令牌被撤销时,AS必须通知所有RS将该访问令牌添加到撤销的令牌缓存中。

每当有资源请求时,RS会检查令牌是否被撤销,如果没有撤销,RS会服务器资源。这样,每个请求都有开销,但由于缓存在内存中,因此开销大大减少,撤销令牌的数量将比有效令牌的数量少得多。