【发布时间】:2015-10-03 04:48:12
【问题描述】:
我正在编写一个 spring boot 应用程序(spring boot 1.2.2、spring 4.1.5),它本质上是一个 REST API。我已经设置了 spring security 以在每个 HTTP 请求时自动加载用户、检查授权等。用户存储在一个数据库中,我使用hibernate来访问这个数据库中的所有数据。
简而言之,问题是:我在 PreFilter 期间对每个 HTTP 请求使用休眠模式从我的数据库中加载一个 user 对象。我后来在我的 RestController 的参数中收到了这个对象,但它不再附加到任何休眠会话。因此,如果不先从数据库中重新读取它,我就无法访问用户对象中的任何惰性集合。
问题是:有没有办法在 PreFilter 期间启动休眠会话并保持它直到 HTTP 请求完成?
长版,带代码: 首先,我设置了 spring 安全性,因此它会加载我的 User 对象作为授权的一部分:
安全配置:
@Configuration
@Component
@EnableWebMvcSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
UserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(new BCryptPasswordEncoder());
}
...
}
用户详情服务,从数据库中加载用户:
@Service
@Transactional
public class DomainUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
if (user == null)
throw new UsernameNotFoundException("User '" + username + "' not found");
return user;
}
}
我还想将 User 对象注入到控制器方法中,所以我这样做:
@PreAuthorize("hasAuthority('VIEW_ACCOUNT_INFO')")
@Transactional(readOnly = true)
@RequestMapping(value = "/user/api-keys/list", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public KeysResponse getApiKeys(
@AuthenticationPrincipal User user
) {
return new KeysResponse(user);
}
这很好用。但是,如果我尝试延迟加载与用户相关的集合,则会出现异常:
@Table(name = "users")
public class User implements UserDetails {
...
@Column
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
private Set<ApiKey> keys;
...
}
user.getKeys(); <-- Exception here
failed to lazily initialize a collection of role: com.sebbia.pushwit.core.domain.User.keys, could not initialize proxy - no Session
发生这种情况是因为在 PreFilter 期间加载用户对象后关闭了 hiberante 会话,现在 user 变量已分离。为了再次附加它,我必须从数据库中重新加载它:
@PreAuthorize("hasAuthority('MANAGE_ACCOUNT_INFO')")
@Transactional(readOnly = false)
@RequestMapping(value = "/user/api-keys/add", method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
public KeysResponse addApiKey(
@RequestParam(required = true) String name,
@RequestParam(required = true) Role role,
@AuthenticationPrincipal User user
) throws ApiKeyAlreadyExistsException {
// I have to re-attach user object to new session
Session session = (Session) entityManager.getDelegate();
user = (User) session.merge(user);
// Now I can change user or load any of it's collections
...
}
这可行,但这似乎容易出错且不必要。我实际上是从数据库中加载user 两次。考虑到我刚刚在之前的方法DomainUserDetailsService.loadUserByUsername 中加载了它,这似乎是一种浪费。
有没有办法在 PreFilter 期间启动休眠会话并保持它直到 HTTP 请求完成?
【问题讨论】:
-
第一次加载用户时为什么不考虑显式加载api密钥集合?或者另一种选择是在需要时仅为给定用户加载 api 密钥集合。无论如何,您不必重新加载用户实体。如果您需要更多解释,请告诉我。
-
@PavlaNováková 感谢您的建议,这是解决问题的好方法。但是,我仍然更喜欢允许我以通常方式访问变量的解决方案,例如 user.getKeys()。
-
使用我建议的解决方案,您当然可以作为 user.getKeys() 访问给定用户的 api 密钥集合 - 在第一种情况下,您可以初始化调用 user.getKeys().size 的集合() 例如在 loadUserByUsername 方法中,然后像往常一样访问集合 - user.getKeys() 没有得到 LazyInitializationException ,在第二种情况下(稍后加载显式密钥集合),方法逻辑将是:通过给定的用户 id 加载 api 密钥,然后只需调用 user.setKeys(loadedKeys) 并访问它们 user.getKeys()。在我看来,公开会议是不好的做法。
标签: java spring hibernate spring-security spring-boot