如何防止 Spring Security 在会话中存储我的自定义 UserDetails 对象

Posted

技术标签:

【中文标题】如何防止 Spring Security 在会话中存储我的自定义 UserDetails 对象【英文标题】:How to prevent Spring Security storing my custom UserDetails object in the session 【发布时间】:2018-04-02 08:43:59 【问题描述】:

我已经为 Spring Security 配置了一个自定义的 UserDetailsS​​ervice 和 UserDetails 实现对象,它们从 JPA 存储中检索用户数据。但是,JPA 管理的 User 对象随后被存储在 HTTP 会话中。这会导致几个问题:

JPA 管理的对象应该在每个会话中存在一个,但是这个 User 对象被传递给用户发起的所有请求,当访问惰性属性时可能会导致错误,等等。我尝试过通过在从 UserDetailsS​​ervice 请求时尝试刷新用户对象来解决此问题,但这似乎并不完全可靠,并且不能防止在某些上下文中发生异常(例如,当会话从服务器上的序列化副本恢复时启动)。

更新数据库时,存储在会话中的 User 对象不会更改以反映更新,在某些情况下会导致返回过时的信息。

我想简单地将用户 ID 存储在会话中,然后根据每个请求将其解析为实际的用户对象。我该怎么做?

我的 UserDetailsS​​ervice 如下所示:

@Service
public class JPAUserDetailsService implements org.springframework.security.core.userdetails.UserDetailsService

    public static final Map<User.UserType,List<SimpleGrantedAuthority>> AUTHORITIES;

    static
    
        AUTHORITIES = new HashMap<> ();
        AUTHORITIES.put (User.UserType.RESTRICTED, Arrays.asList (
            new SimpleGrantedAuthority ("PUBLIC"),
            new SimpleGrantedAuthority ("USER_PROFILE")));
        AUTHORITIES.put (User.UserType.NORMAL, Arrays.asList (
            new SimpleGrantedAuthority ("PUBLIC"),
            new SimpleGrantedAuthority ("USER_PROFILE"),
            new SimpleGrantedAuthority ("REGISTERED")));
        AUTHORITIES.put (User.UserType.ADMIN, Arrays.asList (
            new SimpleGrantedAuthority ("PUBLIC"),
            new SimpleGrantedAuthority ("USER_PROFILE"),
            new SimpleGrantedAuthority ("REGISTERED"),
            new SimpleGrantedAuthority ("ADMIN")));
    

    public class UserWrapper implements UserDetails
    
        User user;

        public UserWrapper (User user)
        
            super ();
            this.user = user;
        

        @Override
        public Collection<? extends GrantedAuthority> getAuthorities ()
        
            return AUTHORITIES.get(user.getType ());
        

        @Override
        public String getPassword ()
        
            return user.getPassword ();
        

        @Override
        public String getUsername ()
        
            // the users canonical username (they may have multiple) is their most recent email address:
            return user.getEmailAddress ().stream ()
                    .sorted ((addr1, addr2) -> addr2.getValidFrom ().compareTo (addr1.getValidFrom ()))
                    .findFirst ()
                    .map (UserEmailAddress::getAddress)
                    .orElseGet (() -> user.getId ().toString ());
        

        @Override
        public boolean isAccountNonExpired ()
        
            return true;
        

        @Override
        public boolean isAccountNonLocked ()
        
            return ! user.isAccountLocked ();
        

        @Override
        public boolean isCredentialsNonExpired ()
        
            return true;
        

        @Override
        public boolean isEnabled ()
        
            return true;
        

        public User getUser ()
        
            User thisUser = user;
            if (!jpaContext.getEntityManagerByManagedType (User.class).contains (thisUser))
            
                // user object needs refreshing for the current session
                thisUser = users.get (thisUser.getId ()).orElseThrow (
                    () -> new ConcurrencyFailureException ("User deleted from database during operation"));
                user = thisUser;
            
            return thisUser;
        
    

    private JpaContext jpaContext;
    private UserRepository users;

    @Autowired
    public JPAUserDetailsService (UserRepository users, JpaContext jpaContext)
    
        this.users = users;
        this.jpaContext = jpaContext;       
    

    @Override
    public UserDetails loadUserByUsername (String username) throws UsernameNotFoundException
    
        return users.findByCurrentEmailAddress (username)
            .map (UserWrapper::new)
            .orElseThrow (() -> new UsernameNotFoundException (username));
    

    public UserDetails wrap (User u)
    
        return new UserWrapper(u);
    



更新

写下 Spring 用于处理身份验证以响应下面的评论的过程,我想我应该将我的笔记记录到这里,因为它可能会帮助其他人弄清楚我缺少什么。我对我试图修改的过程的理解是这样的:

当有人登录我的站点时,登录表单提交被 Spring Security 过滤器拦截,并调用我的 UserDetailsService 实现(一个常规的、单例范围的 Spring bean)来获取对应于的 UserDetails 对象输入的用户名。检查密码,如果它匹配登录过滤器,则创建UsernamePassworkAuthenticationToken 并在当前请求的安全上下文中调用SecurityContext.setAuthentication。 HTTP 安全上下文实现最终将令牌存储在 HTTP 会话对象中。

对此的明显解决方案不起作用。更改UserDetails 对象以存储User 对象的ID 字段并按需获取它会失败,因为HTTP 会话要求放置在其中的所有对象都是Serializable(实际上可以在不通知的情况下任意序列化和反序列化它们,例如在服务器重新配置期间或负载平衡系统中两个节点之间的迁移期间),并且获取User 的能力取决于对Spring 配置的UserRepository 实例的引用,我没有合理的方法来获取静态上下文:我只能在可以控制可用实例数据的情况下创建一个有效的UserDetails,这样才能确保正确的UserRepository可用。

这表明有一个可能的替代解决方案,这可能比找到一种方法让 Spring Security 按需重新创建 UserDetails 对象更通用:有没有一种方法可以在 HTTP 会话中存储对象,这样当它们由容器反序列化,与 Spring 服务 bean 的关联(例如我的 UserRepository 实例)可以在反序列化期间重新连接吗?

【问题讨论】:

“我想简单地将用户 ID 存储在会话中,然后根据每个请求将其解析为实际的用户对象”,这就是所谓的请求范围,所以我想想如果你将@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS) 添加到你的服务中,它就可以解决问题。 @zakariaamine - 这不会为每个请求创建一个 UserDetailsS​​ervice 实例,而不是一个 UserDetails 对象吗? 会的。 UserDetails 不是 bean,所以不会受到影响。 ...但我需要更改其行为的是 UserDetails,而不是 UserDetailsS​​ervice。 抱歉,我没有在 UserDetails 上看到 @Autowired,是的,您也可以在 UserDetails 上使用它 【参考方案1】:

这是一个非常有趣的问题。我也偶然发现了同样的问题。

您可以使用自定义实现 spring 的UserDetails 来存储任何信息。这可能是您自己的自定义“用户实体”。这也可能只是一个 ID 或例如 JsonWebToken (JWT)。

超时数据:我会说这不是一个大问题。为每个请求重新加载 UserDetails。由于请求通常会在几秒钟内得到非常快速的响应,因此这通常不是问题。

JPA-managed-object:是的,您自己的 UserEntity 的多个实例可能会在不同的会话中飞来飞去。尤其是当您的系统处于高负载下时。但从 JPA/Hibernate 的角度来看,它们都是分离的实体。所以这里不用担心。

==> 除了

当您的 UserEntity 中有 LAZY 加载的属性时。这一个问题。您无法加载它们。至少我还没有找到方法。

【讨论】:

以上是关于如何防止 Spring Security 在会话中存储我的自定义 UserDetails 对象的主要内容,如果未能解决你的问题,请参考以下文章

如何在自定义 Spring Security 登录成功处理程序中注入会话 bean

如何在 Spring Security 中关闭 HTTP 会话超时?

如何在 Spring Security 3.1 中进行没有身份验证和授权的并发会话控制

在同一场战争中防止春季会话覆盖

如何在 Spring Security 中的子域之间共享会话

如何在 Spring MVC-Spring Security 应用程序中处理 GWT RPC 调用的会话过期异常