24

我们在应用程序中使用 Hibernate/JPA、Spring、Spring Data 和 Spring Security。我有一个User使用 JPA 映射的标准实体。此外,我有一个UserRepository

public interface UserRepository extends CrudRepository<User, Long> {
    List<User> findByUsername(String username);
}

它遵循 Spring Data 命名查询方法的约定。我有一个实体

@Entity
public class Foo extends AbstractAuditable<User, Long> {
    private String name;
}

我想使用 Spring Data 审计支持。(如此所述。)因此,我创建了AuditorService如下:

@Service
public class AuditorService implements AuditorAware<User> {

    private UserRepository userRepository;

    @Override
    public User getCurrentAuditor() {
        String username = SecurityContextHolder.getContext().getAuthentication().getName();
        List<User> users = userRepository.findByUsername(username);
        if (users.size() > 0) {
            return users.get(0);
        } else {
            throw new IllegalArgumentException();
        }
    }

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
}

当我创建一个方法

@Transactional
public void createFoo() {
    Foo bar = new Foo(); 
    fooRepository.save(foo);
}

一切都正确连接并且FooRepository是 Spring Data CrudRepository。然后StackOverflowError抛出一个,因为调用findByUsername似乎触发休眠以将数据刷新到数据库,这触发AuditingEntityListener谁调用AuditorService#getCurrentAuditor再次触发刷新等等。

如何避免这种递归?是否有加载User实体的“规范方式”?或者有没有办法防止 Hibernate/JPA 刷新?

4

4 回答 4

21

解决方案是不在实现中获取User记录AuditorAware。这会触发所描述的循环,因为选择查询会触发刷新(这种情况是因为 Hibernate/JPA 想要在执行选择之前将数据写入数据库以提交事务),从而触发对AuditorAware#getCurrentAuditor.

解决方案是将User记录存储在UserDetails提供给 Spring Security 中。因此,我创建了自己的实现:

public class UserAwareUserDetails implements UserDetails {

    private final User user;
    private final Collection<? extends GrantedAuthority> grantedAuthorities;

    public UserAwareUserDetails(User user) {
        this(user, new ArrayList<GrantedAuthority>());
    }

    public UserAwareUserDetails(User user, Collection<? extends GrantedAuthority> grantedAuthorities) {
        this.user = user;
        this.grantedAuthorities = grantedAuthorities;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return user.getSaltedPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }

    public User getUser() {
        return user;
    }
}

此外,我更改UserDetailsService了加载User和创建UserAwareUserDetails. 现在可以User通过以下方式访问实例SercurityContextHolder

@Override
public User getCurrentAuditor() {
    return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser();
}
于 2013-01-09T10:00:34.373 回答
21

我遇到了同样的问题,我所做的只是将findByUsername(username)方法上的传播更改为Propagation.REQUIRES_NEW,我怀疑这是事务的问题,所以我改用新事务,这对我来说效果很好。我希望这会有所帮助。

@Repository
public interface UserRepository extends JpaRepository<User, String> {

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    List<User> findByUsername(String username);
}
于 2015-08-22T12:06:48.367 回答
3

看起来您将 User 实体用于两个不同的事情:

  • 验证
  • 审计

我认为为审计目的准备一个特殊的 AuditableUser 会更好(它将具有与原始用户相同的用户名字段)。考虑以下情况:您想从数据库中删除一些用户。如果您的所有审计对象都链接到用户,那么它们将 a)松散作者 b)也可能被级联删除(取决于链接的实现方式)。不确定你是否想要它。因此,通过使用特殊的 AuditableUser,您将拥有:

  • 没有递归
  • 能够从系统中删除某些用户并保留有关它的所有审核信息
于 2013-01-09T10:12:35.430 回答
3

老实说,您实际上并不需要另一个实体。例如,我有类似的问题,我通过以下方式解决了它:

public class SpringSecurityAuditorAware implements AuditorAware<SUser>, ApplicationListener<ContextRefreshedEvent> {
    private static final Logger LOGGER = getLogger(SpringSecurityAuditorAware.class);
    @Autowired
    SUserRepository repository;
    private SUser systemUser;

    @Override
    public SUser getCurrentAuditor() {
        final Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        SUser principal;
        if (authentication == null || !authentication.isAuthenticated()) {
            principal = systemUser;
        } else {
            principal = (SUser) authentication.getPrincipal();
        }
        LOGGER.info(String.format("Current auditor is >>> %s", principal));
        return principal;
    }

    @Override
    public void onApplicationEvent(final ContextRefreshedEvent event) {
        if (this.systemUser == null) {
            LOGGER.info("%s >>> loading system user");
            systemUser = this.repository.findOne(QSUser.sUser.credentials.login.eq("SYSTEM"));
        }
    }
}

其中 SUser 是我用于审计和安全的类。我的用例可能与您的不同,之后我的方法将被删除,但可以这样解决。

于 2013-07-21T00:23:19.157 回答