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

Posted

技术标签:

【中文标题】为啥缓存访问令牌在 oauth2 中被认为是不好的?【英文标题】:Why caching access token is consider bad in oauth2?为什么缓存访问令牌在 oauth2 中被认为是不好的? 【发布时间】:2018-05-04 21:49:20 【问题描述】:

我正在关注这篇文章以撤销用户访问权限:

http://bitoftech.net/2014/07/16/enable-oauth-refresh-tokens-angularjs-app-using-asp-net-web-api-2-owin/

现在考虑在验证用户后,我已经发布了一个寿命为 30 分钟的访问令牌,如上一篇文章所示,刷新令牌为 1 天,但如果管理员在 10 分钟内删除该用户,还剩 20 分钟,那么现在在这种情况下我需要撤消该用户的访问权限。

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

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

但是下面这 2 个答案是说这不是 oauth2 的设计方式:

Revoke access token of OAuthBearerAuthentication

OAuth2 - unnecessary complexity with refresh token

所以我的问题是:

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

我的第二个问题是基于 @Hans Z. 给出的以下答案,他在其中说:

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

2) 在撤销用户访问权限的情况下,为什么 RS 会咨询 AS,因为 AS 仅用于根据 Article 对用户进行身份验证并生成访问令牌?

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

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

资源服务器 - 借助[Authorize] 属性验证访问令牌

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

更新:我决定使用刷新令牌来撤销用户访问,以防用户被删除,并且当用户注销时,由于您要求我们要注销,我将从刷新令牌表中刷新令牌用户点击注销后立即用户。

但这里的问题是 我有 250 个角色与用户相关联,所以如果我将角色放入 accesstoken 中,那么 accesstoken 的大小将非常大,我们无法从 header 传递如此巨大的 accesstoken,但我无法查询每次调用端点时验证端点的用户访问权限的角色。

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

【问题讨论】:

缓存令牌在哪里?可能有数千台服务器正在使用由一台授权服务器颁发的令牌。 @Evk :我已经更新了我的问题,所以基本上在服务器上缓存访问令牌。我将在服务器上为任何客户端应用程序的每个用户缓存访问令牌。 是的,我在服务器上不明白,但在哪个服务器上?可以有很多使用令牌的资源服务,每个服务都在不同的物理服务器上。然后是身份验证服务,它也在不同的物理服务器上。 @Evk 我猜缓存将在 Authentication.api 和 resourceserver.api 项目之间共享,因为 accesstoken 是在 Authentication.api 项目中生成的,并且 accesstoken 将从资源 api server.so 缓存中验证在这 2 个项目之间共享。这有意义吗? 拥有这样的缓存与要求资源服务器调用授权服务器来验证令牌相同(因为如果这是分布式缓存 - 需要网络调用才能从缓存中获取您的令牌),这就是我们的目的试图避免。我们有令牌,它包含授权用户请求所需的所有信息,而无需以任何方式咨询授权服务器。没有这个(联系授权服务器)就不可能使访问令牌无效 - 这就是存在刷新令牌的原因。在刷新无效后让访问令牌存活一段时间被认为是好的。 【参考方案1】:

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

访问令牌

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

OAuth2 允许您在性能和安全性之间进行以下权衡:授权服务器生成一个访问令牌,无需访问授权服务器即可由资源服务器验证(在授权服务器的整个生命周期中至少一次或至少一次)。这有效地缓存了授权信息。因此,通过这种方式,您可以大大减少授权服务器上的负载。缺点与缓存相同:授权信息可能会停止。通过改变访问令牌的生命周期,您可以调整性能与安全平衡。

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

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

角色列表

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

JWT

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

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


2017 年 12 月 1 日更新

如果您想使用 OWIN OAuth 基础架构,您可以通过 AccessTokenFormatOAuthBearerAuthenticationOptionsOAuthAuthorizationServerOptions 中自定义提供自定义格式化程序的令牌格式。你也可以覆盖RefreshTokenFormat

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

    定义您的 CustomRoles 枚举,列出您拥有的所有角色
[Flags]
public enum CustomRoles

    Role1,
    Role2,
    Role3,

    MaxRole // fake, for convenience

    创建EncodeRolesDecodeRoles 方法以在IEnumerable<string> 角色格式和基于上面定义的CustomRoles 的base64 编码位掩码之间进行转换,例如:
    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;
    
    SecureDataFormat&lt;AuthenticationTicket&gt; 的自定义实现中使用这些方法。为简单起见,我将大部分工作委托给标准 OWIN 组件,只实现我的 CustomTicketSerializer,它创建另一个 AuthenticationTicket 并使用标准 DataSerializers.Ticket。这显然不是最有效的方法,但它显示了您可以做什么:
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);
    

    在您的 Startup.cs 中配置 OWIN 以使用您的自定义格式,例如:
var customTicketFormat = CustomTicketSerializer.CreateCustomTicketFormat(app);
OAuthBearerOptions.AccessTokenFormat = customTicketFormat;
OAuthServerOptions.AccessTokenFormat = customTicketFormat;

    在您的OAuthAuthorizationServerProvider 中,将ClaimTypes.Role 添加到分配给用户的每个角色的ClaimsIdentity

    在您的控制器中使用标准AuthorizeAttribute,例如

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

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

【讨论】:

赞成您为帮助我所做的努力,非常感谢您的回答,但我无法理解角色部分。您说我需要创建自定义令牌,但是 owin 将如何将该令牌验证为因为据我所知,令牌验证是基于机器密钥完成的,所以如果我创建自己的自定义令牌,那么 owin 将如何验证该令牌? @Learning-Overthinker-Confused,我添加了一些代码来展示如何将位掩码“侵入”到标准 OWIN OAuth 基础架构中。 我有 1 个与身份验证和授权有关的问题,即是使用身份服务器 4 还是使用 Taiseer Joudeh 的作者代码。您能建议我吗?【参考方案2】:

我希望我能正确回答您的问题并提供一些答案:

1) 如果你开发了你的 AS 要求每次用户登录时验证它,你可以兑现它。

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

3) 文章大概假设授权是由RS完成的,AS只负责告诉你谁是用户,RS据此决定授权。

【讨论】:

【参考方案3】:

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

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

这是您必须在访问权限更改延迟 n 分钟与检查访问令牌有效性的查询次数之间做出选择的权衡

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

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

只要有资源请求,RS 就会检查令牌是否被撤销,如果没有被撤销,RS 就会为资源提供服务。这样,每个请求都会产生开销,但由于缓存在内存中,并且与有效令牌的数量相比,被撤销的令牌数量会非常少,因此开销会大大减少。

【讨论】:

所以你是说管理缓存只是有点复杂? 是的,主要是查询和维护缓存的复杂性和性能【参考方案4】:

“授权服务器可以是与资源服务器相同的服务器,也可以是单独的实体。” [RFC 6749,p。 6]

话虽如此,如果是这种情况,您可以缓存,但资源服务器应该可以理解令牌,并且不需要缓存。如果这是您实现的细节,那么缓存是可能的,但不是必需的。

【讨论】:

以上是关于为啥缓存访问令牌在 oauth2 中被认为是不好的?的主要内容,如果未能解决你的问题,请参考以下文章

使用 Spring Oauth2 缓存访问令牌

oAuth2.0:为啥需要“授权码”,然后才需要令牌?

Oauth2/Openid 连接。如何撤销未知的访问/刷新令牌

移动应用程序的 OAuth2 访问令牌是不是必须过期?

为啥我在使用 PL/SQL 的 Oauth 2 访问令牌代码中出现此错误?

为啥带有 Netflix Zuul 反向代理的 Keycloak OAUTH2 不传递令牌