从会话外部更新在线用户数据(无需重新验证)

Posted

技术标签:

【中文标题】从会话外部更新在线用户数据(无需重新验证)【英文标题】:Update online user-data from outside the session (without reauthentication) 【发布时间】:2020-10-14 16:10:38 【问题描述】:

有几种情况,我想更新用户/主体数据,以便在用户保持登录状态时反映更改(我不想强制重新认证)

从会话“内部”来看,这不是问题:

    @PostMapping("/updateInfo")
    fun updateMyData(
            @AuthenticationPrincipal user: AppUser,
            @Valid @RequestBody newInfo: UpdateDataRequest
    ): ResponseEntity<TestUserInfo> 
        val testInfo = TestUserInfo(user, newInfo)
        user.info = testInfo

        val updatedUser = users.save(user)
        return ResponseEntity.ok(updatedUser.info!!)
    

当我允许用户例如更改他们自己的数据时,我可以轻松访问和更改@AuthenticationPrincipal - 在连续的请求中我可以观察到数据已更新。

当我需要从会话“外部”更改用户数据时,情况就不同了。

用例

有两个用例:

一)。管理员更改用户数据 乙)。用户确认他的电子邮件地址

现在 a)。显然发生在另一个 http-session 中,其中主体是具有一些管理员权限的用户。 对于 b)。您可能会问,为什么这不会在会话中发生:我想要一个简单的一次性确认链接,即获取请求。我不能假设用户是通过设备上的会话登录的,确认链接已打开。对我来说,做一个单独的预身份验证提供程序或其他东西来让用户通过身份验证是不对的 - 然后会在浏览器上打开一个不再使用的不必要的会话。

所以在这两种情况下,当我通过 JPArepository 获取用户、更新数据并将其保存回来时,数据库中的更改是最新的 - 但登录用户不知道该更改,因为他们的用户数据存储在 http 会话中,不知道需要更新。

请注意,我没有使用 redis/spring-session 任何东西 - 这只是一个普通的 http 会话,所以据我了解,我不能使用 FindByIndexNameSessionRepository

我尝试过的

    在spring-security issue #3849 中,rwinch 建议覆盖SecurityContextRepository - 但是,没有关于如何准确执行此操作的更多信息 - 我试图了解界面但无法进一步了解。

    我试图通过对以下 SO 帖子的回复: How to reload authorities on user update with Spring Security(使用 redis 忽略答案。)

投票最多的answer by leo 无济于事,正如那里的 cmets 所述 Aure77 suggests 使用 SessionRegistry,我也尝试在 bealdung 之后使用它 - 但无济于事:我无法正确会话,当登录用户有活动会话时,getallprincipals() 始终为空。如果我有正确的会话,我什至不确定如何从那里继续,因为 Aure 只是建议使用强制重新身份验证的 expireNow() - 我想避免这种情况。 @ 987654326@ 类似的东西 - 从他的角度来看,我认为spring boot 默认使用线程本地securityContextRepository,这就是我没有主体的原因。他提出了一些我还没有理解的东西 - 答案也很老了(2012 年),我对尝试理解和应用它感到不太安全 TwiN suggests 使用 HandlerInterceptor。 Hasler Choo suggests 带有哈希集的修改版本,似乎更接近我的需要。如下所述 - 它有它的问题。

HandlerInterceptor 基于方法

这是迄今为止我可以成功实施的唯一解决方案 - 但它似乎不是很灵活。到目前为止,我的实施仅涵盖用户角色更改。 配置:

@Configuration
class WebMvcConfig : WebMvcConfigurer 
    @Autowired
    private lateinit var updateUserDataInterceptor : UpdateUserDataInterceptor

    override fun addInterceptors(registry: InterceptorRegistry) 
        registry.addInterceptor(updateUserDataInterceptor)
    

HandlerInterceptor:

@Component
class UpdateUserDataInterceptor(
        @Autowired
        private val users: AppUserRepository
) : HandlerInterceptor 
    private val usersToUpdate = ConcurrentHashMap.newKeySet<Long>()

    fun markUpdate(user: AppUser) = usersToUpdate.add(user.id)

    override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean 
        val auth = SecurityContextHolder.getContext().authentication
        (auth.principal as? AppUser)?.apply 
            synchronized(usersToUpdate) 
                if (id in usersToUpdate) 
                    role = users.findById(id).get().role
                    usersToUpdate.remove(id)
                
            
        

        return true
    

我宁愿不只是更新角色,而是替换整个原则-但主体是身份验证对象中的final。 因此,每当 a 需要更新角色以外的其他内容时,都必须在此处特别提及。

剩下的问题:

除了HandlerInterceptor,还有其他解决方案吗? 是否有基于HandlerInterceptor 的解决方案,允许我完全更新主体对象

【问题讨论】:

@KavithakaranKanapathippillai 我添加了指向各个答案的直接链接 - 2 以下的所有选项都是对同一个 SO 帖子的答案。 感谢您的回答 - 我相信下一个请求应该没问题,我明天再检查一下。 【参考方案1】:

我不考虑单实例应用程序

1.三个因素在起作用

您希望以多快的速度反映更改(current session and current request vs current session and next request vs next session) 您是否必须尽量减少使用分布式内存或缓存对响应时间的影响? 您想以响应时间为代价来降低成本(不能使用分布式内存)吗?

现在您可以从第一个因素中选择一个选项。但是对于第二个和第三个因素,你优化一个因素的代价是另一个因素。或者您尝试找到一个平衡点,例如尝试将受影响的用户列表保存在内存中,然后为受影响的用户访问数据库。

(不幸的是,您将受影响的用户列表保存在 UpdateUserDataInterceptor 中的优化将无法正常工作,除非它是单实例应用程序)

2。现在,根据我对您问题的理解,我正在对发挥作用的三个因素做出以下回答。

当前会话下一个请求 降低成本(无分布式内存) 数据库调用会影响性能

(我稍后会更新我对其他可能路径和这些路径可能实现的想法)

3.所选路径的实现选项 - next-request-with-db-calls-and-no-distributed-memory

请求过滤器链中任何能够调用数据库的组件都可以通过更新 SecurityContext 来实现。如果您在 SecurityContextRepository 中执行此操作,您将尽早执行此操作,您甚至可能有机会使用更新的原则恢复 SecurityContext,而不是更新已经创建的 SecurityContext。但是任何其他过滤器或拦截器也可以通过更新 SecurityContext 来实现这一点。

4.详细了解每个实施

SecurityContextRepository 选项:

看着HttpSessionSecurityContextRepository,似乎直接扩展它。

public class HttpSessionSecurityContextRepository 
            implements SecurityContextRepository 
    .....
    public SecurityContext loadContext(HttpRequestResponseHolder reqRespHolder) 
        HttpServletRequest request = reqRespHolder.getRequest();
        HttpServletResponse response = reqRespHolder.getResponse();
        HttpSession httpSession = request.getSession(false);

        SecurityContext context = readSecurityContextFromSession(httpSession);
        ........

        //retrieve the user details from db
        //and update the principal. 
        .......
        return context;
    


SecurityContextHolderStrategy 选项

ThreadLocalSecurityContextHolderStrategy,看起来也很直白

final class ThreadLocalSecurityContextHolderStrategy 
            implements SecurityContextHolderStrategy 
    

    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();
    ....


    public void setContext(SecurityContext context) 

        // you can intercept this call here, manipulate the SecurityContext and set it 

        Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
        contextHolder.set(context);
    

    .....

另一个过滤器或 HandlerInterceptor //TODO 将更新

注意:

您提到principal 在身份验证对象中是最终的,您想替换它。您可以通过创建UserDetails 的可变包装器、扩展您当前的UserDetailsService 并返回该包装器来实现此目的。然后你就可以更新主体了,
YourWrapper principalWrapper =(YourWrapper) securityContext
                       .getAuthentication().getPrincipal();
principalWrapper.setPrincipal(updated);

【讨论】:

以上是关于从会话外部更新在线用户数据(无需重新验证)的主要内容,如果未能解决你的问题,请参考以下文章

更新数据时在下一个身份验证会话中更新用户值

来自外部源的 Django 用户和身份验证

Laravel 为外部用户登录成功设置会话或令牌 - 无用户仅数据库 Api 用户

django 在任何地方都提供相同的会话

春季安全删除用户 - 会话仍处于活动状态

动态数据更新,无需刷新或重新加载