spring-boot - 从 session 外部更新在线用户数据(无需重新验证)

标签 spring-boot kotlin spring-security

有几种情况,我想更新用户/主体数据 以便反射(reflect)更改 当用户保持登录状态 (我不想强制重新认证)
从 session “内部”来看,这不是问题:

    @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 - 在连续的请求中,我可以观察到数据已更新。
当我需要从 session “外部”更改用户数据时,这是不同的。
用例
有两个用例:
一种)。管理员更改用户数据
乙)。用户确认他的电子邮件地址
现在一个)。显然发生在另一个 http-session 中,其中主体是具有一些管理员权限的用户。
对于 b)。您可能会问,为什么这不会在 session 中发生:我想要一个简单的一次性确认链接,即获取请求。我不能假设用户是通过设备上的 session 登录的,确认链接已打开。对我来说,做一个单独的预身份验证提供程序或其他东西来获得用户身份验证是不对的——然后会在浏览器上打开一个不再使用的不必要的 session 。
因此,在这两种情况下,当我通过 JPArepository 获取用户、更新数据并将其保存回来时,数据库中的更改都是最新的 - 但登录用户不知道该更改,因为他们的用户数据存储在http session 中,不知道需要更新。
请注意,我没有使用任何 redis/spring-session - 这只是一个普通的 http session ,所以据我了解,我不能使用 FindByIndexNameSessionRepository .
我试过的
  • spring-security issue #3849 rwinch 建议覆盖 SecurityContextRepository - 但是,没有关于如何准确执行此操作的更多信息 - 我试图了解界面,但无法进一步了解。
  • 我试图通过对以下 SO 帖子的回复:
    How to reload authorities on user update with Spring Security (使用redis忽略答案。)
  • 点赞最多的answer by leo没有帮助,正如评论中提到的
  • Aure77 suggests使用 SessionRegistry ,我也尝试在 bealdung 之后使用它- 但无济于事:我无法正确 session ,当登录用户有事件 session 时,getallprincipals() 始终为空。如果我有正确的 session ,我什至不确定如何从那里继续,因为 Aure 只是建议使用 expireNow()这会强制重新身份验证 - 我想避免这种情况。
  • alexkasko suggests类似的东西-从他看来,也许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)
        }
    }
    
    处理程序拦截器:
    @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基于解决方案,允许我完全更新主体对象
  • 最佳答案

    I am not considering single instance applications


    1. 三个因素在起作用
  • 您希望以多快的速度反射(reflect)更改(current session and current request vs current session and next request vs next session)
  • 您是否必须尽量减少使用分布式内存或缓存对响应时间的影响?
  • 你想以牺牲响应时间为代价来降低成本(不能使用分布式内存)吗?

  • 现在您可以从第一个因素中选择一个选项。但是对于第二个和第三个因素,您会以另一个因素的代价来优化一个因素。或者您尝试找到一个平衡点,例如尝试将受影响的用户列表保存在内存中,然后为受影响的用户访问数据库。
    (不幸的是,您将受影响的用户列表保存在 UpdateUserDataInterceptor 中的优化不会工作,除非它是单实例应用程序,否则它不会存储在分布式内存中)
    2.现在根据我对您问题的理解,我正在对发挥作用的三个因素做出以下回答。
  • 当前 session 下一个请求
  • 降低成本(无分布式内存)
  • 数据库调用会影响性能

  • (稍后我将更新我对其他可能路径和这些路径可能实现的想法)
    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);
    

    关于spring-boot - 从 session 外部更新在线用户数据(无需重新验证),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/62554398/

    相关文章:

    android - 在启动器应用程序上向上和向下滑动手势

    java - 有没有办法在 Controller 级别 @PreAuthorize 注释中访问 PathVariable?

    java - 使用 'webapp-runner' 运行 Spring boot 应用程序后,它停止在一行 "INFO: Starting ProtocolHandler ["http-nio-808 0"]"

    Android 每 x 分钟运行一次 Kotlin Coroutine

    hibernate - 如何使用现代 Spring Boot + Data JPA 和 Hibernate 设置生成 ddl 创建脚本?

    Android - Fragment 中的 GlSurfaceView 同时运行两次

    java - 使用 Spring Security 保护 Spring Cloud 功能

    spring - Spring Security:REST API需要哪个过滤器

    java - 使用 Spring Kafka 在单个事务中写入两个 Kafka 主题

    java - CrudRepository OperationNotSupported