如何防止Spring Security在会话

时间:2017-10-21 08:50:58

标签: spring session spring-security

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

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

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

我想在会话中简单地存储用户ID,然后在每个请求的基础上将其解析为实际的User对象。我怎么能这样做?

我的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用于处理身份验证的过程以响应下面的评论,我想我应该在这里记下这是如何工作的,因为它可能有助于其他人弄清楚我错过了什么。我对我试图修改的过程的理解是:

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

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

这表明有一种可能的替代解决方案,这可能比找到一种方法让Spring Security按需重新创建UserDetails对象更通用:是否有一种方法可以在HTTP会话中存储对象,以便何时它们是由容器反序列化的,在反序列化过程中可以重新连接到我的UserRepository实例之类的Spring服务bean的关联吗?

1 个答案:

答案 0 :(得分:0)

这是一个很有趣的问题。我也遇到了同样的问题。

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

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

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

==> 除外 <==

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