SpringSecurity Context 中 获取 和 更改 当前用户信息的问题

SpringSecurity Context 获取和更改用户信息的问题

SecurityContext 异步线程中获取用户信息

今天在做项目时遇到了一个问题,我需要获取当前用户的 ID。之前,前端并没有存储用户信息,我一直是在后端的 service 中通过 SecurityContext 来获取用户信息,这个方法之前一直有效。然而,今天在另一个 service 中调用时却无法获取到用户信息。

经过详细排查,发现 SecurityContext 的内容是与请求线程(如 HTTP 请求)绑定的。但我当前的 service 是用于处理 MQTT 消息,这属于异步线程。因此,在异步线程中无法从 SecurityContext 获取用户信息,只能另寻解决方案。

 /**
     * 处理接收到的设备数据,根据数据类型进行不同的处理。energy数据存储到数据库,其他数据通过WebSocket发送到前端展示。
     * @param data 数据内容
     */
    private void handleIncomingData(String data) {
        	......
            /*
                * 通过设备ID找到用户ID, 不能通过securityContext获取当前用户ID,因为这里是异步处理消息,不在请求线程中。
                * 通过securityContext获取当前用户ID的方法只能在请求线程中使用,通常是与HTTP请求相关的操作才能获取到。
                * 这里是MQTT消息处理,不在请求线程中,所以要通过其他方式获取当前用户ID。
                * 还因为这里是存入数据库,因为也不能依赖物理设备的用户ID,设备不一定是存用户ID, 降低耦合性。
               TODO: 这里是否可以改进?
             */
            long userId = deviceMapper.findDeviceById(deviceId).getUserId();
            if (userId<=0) {//如果userId<=0,说明没有找到对应的设备
                logger.error("Failed to get user id by device id: {}", deviceId);
                throw new RuntimeException("Failed to get user id by device id: " + deviceId);
            }

            Energy energy = new Energy();
            energy.setDeviceId(deviceId);
            energy.setEnergy(totalEnergy);
            energy.setRecordDate(recordDate);
            energy.setUserId(userId);
			......
    }

SecurityContext 线程降级问题

在项目中遇到 SecurityContext 线程降级的问题,具体场景是用户修改个人资料(如邮箱)后,我希望 SecurityContext 中的用户信息能及时更新。然而,在修改邮箱后,用户需要跳转到邮箱验证码验证页面,该页面通过 security 配置中的 permitAll() 允许匿名访问。

这个配置导致一个问题:虽然用户已经登录认证,但在访问 /verify-code 页面时,SecurityContext 中的身份会被降级为匿名用户,进而导致更新后的信息没有在 SecurityContext 中及时反映。

我了解到可以通过 setAuthentication() 手动更新 SecurityContext,但尝试后依然无法解决问题,更新后的用户信息仍旧无法及时同步到 SecurityContext 中~

   @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // disable csrf
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/login","/register","/verify-code","/forgot-password","/change-password").permitAll()// permit request without authentication
                        .requestMatchers("/ws/**").permitAll()// permit websocket request without authentication
                        .anyRequest().authenticated()
                )
                .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)
                .logout(AbstractHttpConfigurer::disable);// disable logout otherwise it will conflict with our custom logout

        return http.build();
    }

9.22 更新

线程降级的问题目前已经解决了,主要涉及两个问题:

  1. 方法一: 刷新Token。
  2. 方法二: 在JwtFilter修改过滤逻辑。

方法一:
由于我是基于Token的机制,所以所有请求发过来的时候都会首先经过JwtFilter,但是我在Filter里面通过解析Token来设置用户的信息的。由于之前在用户更新了profile后没有及时更新新的Token返回给前端,因此导致了虽然我context的内容在用户信息更改后重新配置了,但是JwtFilter每次在请求进来的时候都是在旧的Token里面解析的这个就是问题的关键。

所以这里的解决方案就是每次用户更新profile后重新根据新的用户信息生成新的Token,然后返回给前端。 但是这样做其实不是特别的好,首先来回Token的消耗,还有就是设计也不是很好。

方法二:
我个人采用的是方法二,主要是好像比较符合现在主流的设计?

第二个方法就是,我们在用户登陆生成Token的时候不在Token里面放入太多的用户信息,就放入必要的信息就好,比如就只放用户id。

然后在更改我们的JwtFilter在获取用户的时候,地一次首现根据Token里面解析出来的ID去从数据库中获取用户最新的身份信息。这样的效率会很低因为每次API请求可能都设计数据库的查询,因此这是就要引入Cache了。以后再来的请求先找Cache再找数据库。最后,要记得更新用户信息后要记得清楚这个用户的Cache,下次就可以缓存最新的了。

这里不多讲了,cache的准备在sping专题里单独写一篇。大家也可以去看我GitHub得代码。欢迎指出问题!

修改用户信息的代码片段:

    protected ResponseEntity<Result> completeProfileUpdate(User user) {
        userMapper.updateUser(user);
        redisTemplate.delete(user.getEmail());
        redisTemplate.delete("temp_user:" + user.getEmail() + ":updateProfile");

        // 每当用户更新profile,移除所有的用户缓存,以便下次查询时重新加载数据
        evictAllUserCaches(user.getId());

        // 创建新的 LoginUser 对象,使用更新Spring Security上下文中的 Authentication 对象
        List<String> permissions = cachedUserService.getPermissionsByUserId(user.getId());
        List<String> roles = cachedUserService.getRolesByUserId(user.getId());
        LoginUser updatedLoginUser = new LoginUser(user, permissions, roles);
        // 创建新的 Authentication 对象
        Authentication newAuth = new UsernamePasswordAuthenticationToken(
                updatedLoginUser, null, updatedLoginUser.getAuthorities()
        );
        // 在每次更新用户信息后,都需要更新一下 SecurityContextHolder 中的 Authentication 对象
        SecurityContextHolder.getContext().setAuthentication(newAuth);

        return ResponseEntity.ok(Result.success("Profile updated successfully"));
    } 
   

JwtFilter 里的代码片段:

   /**
     * parse the token and get the user information to create a LoginUser object
     * @param token token to parse
     * @return LoginUser object
     * @throws UsernameNotFoundException if the user is not found
     * @Description: get the newest user information from the database if the user is not in the cache!
     */
    private LoginUser getLoginUser(String token) {
        // Parse the token to get the user information
        Map<String, Object> claims = jwtUtil.parseToken(token);

        // get the user information from the database can make sure the user is up to date
        Long userId = Long.parseLong(claims.get("userId").toString());
        // if the user is not in the cache, get the user from the database
        User user = cachedUserService.getUserById(userId);
        if (user != null) {
            user.setPassword(null);// 不将密码传递保存在redis中
        }else {
            throw new UsernameNotFoundException("User not found");
        }
        List<String> roles = cachedUserService.getRolesByUserId(userId);
        List<String> permissions = cachedUserService.getPermissionsByUserId(userId);

        return new LoginUser(user, permissions, roles);
    }

你可能感兴趣的:(Spring,java,spring,springsecurity,SecurityContext)