2013-01-08 74 views
11

我們在我們的應用程序中使用Hibernate/JPA,Spring,Spring Data和Spring Security。我有一個使用JPA映射的標準User實體。另外,我有一個UserRepository如何用Spring Data JPA和Spring Security實現AuditorAware?

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

隨後命名查詢方法春節數據約定。我有一個實體

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

我想使用Spring Data審計支持。 (正如descripe 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調用似乎觸發休眠刷新數據到數據庫觸發AuditingEntityListener誰調用AuditorService#getCurrentAuditor再次觸發刷新等等。

如何避免這種遞歸?有沒有一種「規範的方式」來加載User實體?或者有沒有辦法阻止Hibernate/JPA刷新?

回答

11

解決方案不是獲取AuditorAware實施中的User記錄。這會觸發描述的循環,因爲select查詢會觸發刷新(這是因爲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。現在,可以通過SercurityContextHolder訪問User實例:

@Override 
public User getCurrentAuditor() { 
    return ((UserAwareUserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()).getUser(); 
} 
3

它看起來像您使用中,用戶實體的兩個不同的東西:

  • 認證
  • 審計

我認爲爲審計目的準備一個特殊的AuditableUser會更好(它將與原始用戶具有相同的用戶名字段)。 請考慮以下情況:您想要從數據庫中刪除某個用戶。如果所有的審計對象都鏈接到用戶,那麼他們將a)鬆散的作者b)也可能被級聯刪除(取決於鏈接是如何實現的)。不確定你想要它。 因此,通過使用特殊的AuditableUser你將有:

  • 沒有遞歸從系統中刪除一些用戶
  • 能力,並保留所有關於它的審計信息
+0

爲什麼要刪除一個用戶呢?爲什麼不標記爲刪除或某事。 – aycanadal

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是這兩者我使用的審計,以及對安全性的類。 我有可能不同於你的用例,我的方法會被刪除後,但它可以像這樣解決。

3

我得到了同樣的問題,我所做的只是將findByUsername(username)方法的傳播更改爲Propagation.REQUIRES_NEW,我懷疑這是交易問題,所以我更改爲使用新的交易,並且對我來說運行良好。我希望這可以幫助。

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

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