JAAS 登录模块中的用户主体 - 作为单独主体或 Bean 属性的额外属性?

User Principal in JAAS Login Module - extra attributes as separate Principal or as Bean properties?

我已经实现了一个 JAAS LoginModule,它与 Spring 4 和 Struts 2.3 配合得很好。同样的 LoginModule 也通过 Tomcat 8.0/8.5 中的 ServletFilter 调用,以验证和授权对 Spring 框架之外的 Servlet 的请求。

LoginModule 使用 java.security.acl.Group 的简单实现,并使用 java.security.Principal 的两个简单实现将用户和角色分开。 "simple" 我的意思是满足接口的最小实现。

"User" 实现将 name 属性 映射到唯一的用户名(实际上是电子邮件地址)。由于电子邮件地址是唯一的但可以更改,因此帐户数据库包含一个唯一的帐户标识符 (GUID),用于分配组、角色和记录服务请求(同时也使我们的用户匿名)。在我的模型中,AccountIdentifier 有自己的 class。本质上,我有两个唯一的帐户标识符,但由于需要将电子邮件地址提供给 LoginModule 进行身份验证,它最终成为用户主体的基础。

帐户标识符当前未传播到 LoginModule 中的 Subject,但现在我需要它来记录服务请求。

我看到了通过 Subject 提供帐户标识符的两种方式,但我不确定哪一种是 JAAS 的最佳实践:

  1. 扩展我当前的 "User" Principal 实现以包括在 LoginModule.commit().
  2. 期间设置的 "accountIdentifier" 属性
  3. AccountIdentifier 作为单独的 Principal 实施,并在 LoginModule.commit() 期间添加到 Subject

第一个选项最简单,但这似乎也违背了将个人身份信息与帐户分离的目的(这是我需要做的事情,以满足即将到来的欧洲 GDPR 要求)。

我是否应该将 "User" 主体(包含电子邮件地址的主体)添加到主题中?

JAAS 和 Servlet 规范在身份验证和用户原则方面存在一些不兼容性。因此,Spring 使用与 Tomcat 不同的 JAAS 集成方法。

此答案记录了一种以兼顾 Tomcat 和 Spring 的方式实现 JAAS 登录模块的综合方法。


为清楚起见,从问题中复制了实施用户原则的两个选项:

  1. 扩展我当前的 "User" Principal 实现以包括在 LoginModule.commit() 期间设置的 "accountIdentifier" 属性。
  2. AccountIdentifier 作为单独的 Principal 实施,并在 LoginModule.commit() 期间添加到 Subject

选项 1 具有将不同形式的个人身份信息连接在一起的不幸副作用,这在某些环境中可能违反欧洲 GDPR 法规(会话可能被序列化到磁盘上,并且此信息将随之而来)。

选项 2 分离出个人身份信息,但必须以克服 Servlet 规范和 Tomcat 的 JAAS 实现中的几个限制的方式实现。

下面详细描述了这些限制,粗体部分总结了要点。


JAASRealm 要求 Subject 的后备集合保留 Principal 的顺序。

Tomcat 8.5 JAAS Realm 文档指出:

Using JAASRealm gives the developer the ability to combine practically any conceivable security realm with Tomcat's CMA.

但接着说:

Although not specified in JAAS, you should create separate classes to distinguish between users and roles, extending javax.security.Principal so that Tomcat can tell which Principals returned from your login module are users and which are roles (see org.apache.catalina.realm.JAASRealm). Regardless, the first Principal returned is always treated as the user Principal.

请注意,上面的 Tomcat 文档使用了短语 "the user Principal"。尽管 JAAS API 建议将用户和角色实现为不同的 classes 扩展 javax.security.Principal, this is not compatible with the Servlet Specification because HttpServletRequest.getUserPrincipal() 只允许 returned:

Returns a javax.security.Principal object containing the name of the current authenticated user. If the user has not been authenticated, the method returns null.

严格阅读上面的文档说它应该包含"...当前认证用户的名称",但是为了满足我最初的目标,我我将其解释为 “...已验证主题的任何名称或标识符”。这更接近于 com.sun.security.auth.UserPrincipal 文档(即 "A user principal identified by a username or account name")。

由于 Tomcat 的 JAASRealm 和 Servlet 规范的 HttpServletRequest 中的上述限制,如果要将帐户标识符传播到请求,则显然很重要a ServletFilter(只能访问当前会话、请求和响应),它必须包含在第一个 Principal 中(因此原始问题中的选项 1 将满足此要求,或者仅选项 2如果它首先出现并且我不需要原始用户名)。我相信我真正需要的只是帐户标识符,所以我现在坚持使用第二个选项,我将 "EmailAddressPrincipal" 交给 MyLoginModule,然后通过 Subject(即 MyLogin.commit() 添加 "AccountIdentifierPrincipal" 作为第一个主体)。

JAASRealm 文档实际上在 Principals 的精确顺序上略有矛盾,这取决于您正在阅读的部分:

As this Realm iterates over the Principals returned by Subject.getPrincipals(), it will identify the first Principal that matches the "user classes" list as the Principal for this user

对比

Regardless, the first Principal returned is always treated as the user Principal.


Servlet API 不保证由 Subject 编辑的 Principles return 的顺序。

本质上,如果我要创建一个模仿 JAASRealm 正在做的事情的 ServletFilter,身份验证将如下所示(特别注意迭代器):

final LoginContext loginContext = new LoginContext(MyLoginModule.JAAS_REALM, new DefaultCallbackHandler(username, password));
loginContext.login();
final Subject subject = loginContext.getSubject();
request.getSession().setAttribute("AUTH_USER_PRINCIPAL", subject.getPrincipals(AccountIdentifierPrincipal.class).iterator().next());
request.getSession().setAttribute("AUTH_ROLE_PRINCIPALS", subject.getPrincipals(MyRolePrincipal.class));

不幸的是,这与 javax.security.auth.Subject, which mandates that a java.util.Set is used as the backing collection for Principals. Additionally, the Set.iterator() 文档状态的构造函数直接冲突:

The elements are returned in no particular order (unless this set is an instance of some class that provides a guarantee).

我们对 Subject 的最早访问是在 LoginModule.initialize() 方法中,不幸的是,它在 LoginContext 内部某处被调用(我认为)。这意味着我们无法控制用作 Principals 后备集合的 Set 的确切子 class,因此无法控制它们的顺序。当它到达 ServletFilter 时,它是一个 SynchronizedSet,所以甚至不清楚原来的 class 是什么,或者是否发生了重新排序。

这一切都表明,为了让 JAASRealm 按预期工作,只能提供一个用户主体。在那个中间层的任何地方都没有明确建立 Subject Principals.

顺序的接口

结论

使用JAASRealm时,在commit期间,只应将声明的用户类型中的一个Principal添加到Subject

使用 JAASRealm 时,避免使用多个用户 class 名称。

违反以上两条规则可能会导致未定义的and/or不一致行为。


解决方案:对 Spring 使用 AuthorityGranter,对非框架 Tomcat servlet

使用 ServletFilter

为了选项 2,我避免使用 JAASRealm,因为根据上述所有文档,它没有忠实地遵守 JAAS。这让我回到纯粹的 ServletFilter 方法。

javax.security.auth.Subject 包含授权所需的一切:多个用户主体、角色和 ACL 组。不幸的是,这个 class 只能部分序列化,这意味着我不能将 class 包装为 Principal 和 return 它。

为了满足 Spring 的 DefaultJaasAuthenticationProvider, implement an AuthorityGranterPrincipal 映射到角色名称 - 这提供了对映射执行方式的完全控制。

由于 AuthorityGranter 在 Spring 框架之外不可用,我还实现了一个 ServletFilter,它使用类似的方法为我的非 Spring 映射角色网络应用程序。暂时,我使用 HttpServletRequestWrapper 从会话属性(在身份验证期间存储在会话中)读取主体和角色并覆盖 getUserPrincipal and isUserInRole。最后,我将重新访问 JAASRealm 以查看它是否包含处理此部分的任何功能,但我还没有完成。