Spring Session Redis和Spring Security如何更新用户会话?

7
我正在使用Spring Boot、Spring Security和Spring Session(Redis)构建一个Spring REST Web应用程序。我正在使用Spring Cloud和Zuul代理遵循网关模式来构建云应用程序。在这个模式中,我使用Spring Session来管理Redis中的HttpSesssion,并使用它来授权我的资源服务器上的请求。当执行更改会话权限的操作时,我希望更新该对象,以便用户无需注销即可反映更新。有人有解决方案吗?
1个回答

8

要更新权限,您需要在两个地方修改身份验证对象。一个是安全上下文中,另一个是请求上下文中。您的主体对象将是org.springframework.security.core.userdetails.User或扩展该类(如果您已覆盖了UserDetailsService)。这适用于修改当前用户。

    Authentication newAuth = new UsernamePasswordAuthenticationToken({YourPrincipalObject},null,List<? extends GrantedAuthority>)

    SecurityContextHolder.getContext().setAuthentication(newAuth);
    RequestContextHolder.currentRequestAttributes().setAttribute("SPRING_SECURITY_CONTEXT", newAuth, RequestAttributes.SCOPE_GLOBAL_SESSION);

使用Spring Session更新任何已登录用户的会话需要自定义过滤器。该过滤器存储了一组由某些进程修改的会话。当新会话需要被修改时,消息系统会更新该值。当请求具有匹配的会话键时,过滤器会在数据库中查找用户以获取更新。然后,它会更新会话上的“SPRING_SECURITY_CONTEXT”属性,并更新SecurityContextHolder中的身份验证。 用户无需注销。在指定过滤器的顺序时,重要的是它在SpringSessionRepositoryFilter之后执行。该对象具有@Order -2147483598,因此我只需要将我的过滤器加1来确保它是下一个要执行的过滤器。
工作流程如下:
  1. Modify User A Authority
  2. Send Message To Filter
  3. Add User A Session Keys to Set (In the filter)
  4. Next time User A passed through the filter, update their session

    @Component
    @Order(UpdateAuthFilter.ORDER_AFTER_SPRING_SESSION)
    public class UpdateAuthFilter extends OncePerRequestFilter
    {
    public static final int ORDER_AFTER_SPRING_SESSION = -2147483597;
    
    private Logger log = LoggerFactory.getLogger(this.getClass());
    
    private Set<String> permissionsToUpdate = new HashSet<>();
    
    @Autowired
    private UserJPARepository userJPARepository;
    
    private void modifySessionSet(String sessionKey, boolean add)
    {
        if (add) {
            permissionsToUpdate.add(sessionKey);
        } else {
            permissionsToUpdate.remove(sessionKey);
        }
    }
    
    public void addUserSessionsToSet(UpdateUserSessionMessage updateUserSessionMessage)
    {
        log.info("UPDATE_USER_SESSION - {} - received", updateUserSessionMessage.getUuid().toString());
        updateUserSessionMessage.getSessionKeys().forEach(sessionKey -> modifySessionSet(sessionKey, true));
        //clear keys for sessions not in redis
        log.info("UPDATE_USER_SESSION - {} - success", updateUserSessionMessage.getUuid().toString());
    }
    
    @Override
    public void destroy()
    {
    
    }
    
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
    {
        HttpSession session = httpServletRequest.getSession();
    
    if (session != null)
    {
        String sessionId = session.getId();
        if (permissionsToUpdate.contains(sessionId))
        {
            try
            {
                SecurityContextImpl securityContextImpl = (SecurityContextImpl) session.getAttribute("SPRING_SECURITY_CONTEXT");
                if (securityContextImpl != null)
                {
                    Authentication auth = securityContextImpl.getAuthentication();
                    Optional<User> user = auth != null
                                          ? userJPARepository.findByUsername(auth.getName())
                                          : Optional.empty();
    
                    if (user.isPresent())
                    {
                        user.get().getAccessControls().forEach(ac -> ac.setUsers(null));
    
                        MyCustomUser myCustomUser = new MyCustomUser (user.get().getUsername(),
                                                                     user.get().getPassword(),
                                                                     user.get().getAccessControls(),
                                                                     user.get().getOrganization().getId());
    
                        final Authentication newAuth = new UsernamePasswordAuthenticationToken(myCustomUser ,
                                                                                               null,
                                                                                               user.get().getAccessControls());
                        SecurityContextHolder.getContext().setAuthentication(newAuth);
                        session.setAttribute("SPRING_SECURITY_CONTEXT", newAuth);
                    }
                    else
                    {
                        //invalidate the session if the user could not be found
                        session.invalidate();
                    }
                }
                else
                {
                    //invalidate the session if the user could not be found
                    session.invalidate();
                }
            }
            finally
            {
                modifySessionSet(sessionId, false);
            }
        }
    }
    
    filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
    

这个答案对我帮助很大,谢谢!不过需要注意的是:在修改身份验证后,您需要将会话属性设置为SecurityContextImpl的实例,而不是Authentication。 - Lev
感谢@Ceekay,这救了我的一命!我同意@Lev的观点,SecurityContext实现似乎是正确的选择。此外,如果操作针对当前登录的用户,则我认为RequestAttributes.SCOPE_SESSION更合适。 - demaniak
另外需要补充的是,在调用 SecurityContextHolder.getContext().setAuthentication(newAuth) 后,您可以直接使用从 SecurityContextHolder.getContext() 获取的上下文,来更新 RequestContext - demaniak

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接