我们在应用程序中使用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刷新?
答案 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既是我用于审计的类,也是安全性的类。 我的用例可能与你的用例不同,之后我的方法会被删除,但可以这样解决。