如何使用Spring Data JPA和Spring Security实现AuditorAware?

时间:2013-01-08 20:42:36

标签: spring hibernate jpa spring-security spring-data

我们在应用程序中使用Hibernate / JPA,Spring,Spring Data和Spring Security。我有一个使用JPA映射的标准User实体。此外,我有一个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审核支持。 (如描述here。)因此我创建了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数据CrudRepository。然后抛出StackOverflowError,因为对findByUsername的调用似乎触发了hibernate将数据刷新到数据库,触发AuditingEntityListener调用AuditorService#getCurrentAuditor的数据再次触发刷新和等等。

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

4 个答案:

答案 0 :(得分:12)

解决方案不是在User实现中获取AuditorAware记录。这会触发所描述的循环,因为select查询会触发flush(这是因为Hibernate / JPA希望在执行select之前将数据写入数据库以提交事务),这会触发对AuditorAware#getCurrentAuditor的调用。

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

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();
}

答案 1 :(得分:9)

我遇到了同样的问题,我所做的只是将findByUsername(username)方法上的传播更改为Propagation.REQUIRES_NEW,我怀疑这是交易的问题,所以我改为使用新的交易这对我很有用。我希望这可以提供帮助。

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

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    List<User> findByUsername(String username);
}

答案 2 :(得分:3)

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

  • 验证
  • 审计

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

  • 没有递归
  • 从系统中删除某些用户并保留所有审核信息的能力

答案 3 :(得分: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既是我用于审计的类,也是安全性的类。 我的用例可能与你的用例不同,之后我的方法会被删除,但可以这样解决。