避免方法

时间:2015-07-14 13:45:46

标签: java spring hibernate spring-security spring-boot

我正在编写一个spring boot应用程序(spring boot 1.2.2,spring 4.1.5),它本质上是一个REST API。我已经设置了spring安全性来自动加载用户,检查每个HTTP请求的授权等。用户存储在数据库中,我使用hibernate访问该数据库中的所有数据。

简而言之,问题是:我在PreFilter期间在每个HTTP请求上使用hibernate从我的数据库加载user个对象。我后来在RestController的params中收到了这个对象,但它不再附加到任何hibernate会话。因此,我无法访问用户对象中的任何惰性集合,而无需先从数据库中重新读取它。

问题是:有没有办法在PreFilter期间启动hibernate会话并保留它直到HTTP请求完成?

长版本,包含代码:首先,我设置了弹簧安全性,因此它将加载我的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期间启动hibernate会话并保留它直到HTTP请求完成?

1 个答案:

答案 0 :(得分:1)

经过一番考虑之后,我选择了user1675642提供的答案:我决定为每个HTTP请求使用单个hibernate会话(可能有嵌套会话)。我没有看到这种方法对于我的特定情况有任何问题,因为我总是为每个请求打开数据库连接,所以在堆栈跟踪上打开它几个方法并没有真正有任何区别(实际上它确实有所作为,因为我不必打开它两次)。

但是,我无法使用框架提供的OpenSessionInViewFilter。我得到一个“java.lang.IllegalArgumentException:ServletContext不能为null”异常。我无法弄清楚我的配置的哪个部分导致了这个问题,所以我继续编写了自己的实现:

首先,您必须声明以下过滤器:

@Component
public class OpenSessionFilter extends OncePerRequestFilter {
    @Autowired
    OpenSessionService sessionService;

    @Override
    protected void doFilterInternal(final HttpServletRequest request, final HttpServletResponse response, final FilterChain filterChain) throws ServletException, IOException {
        sessionService.doFilterInTransaction(request, response, filterChain);
    }
}

然后,您必须声明以下服务:

@Service
public class OpenSessionService {
    @Transactional
    public void doFilterInTransaction(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        filterChain.doFilter(request, response);
    }
}

然而,这回答了我的问题,但它并没有解决我的问题。问题是,Spring Security缓存身份验证委托人。这意味着身份验证过滤器只会偶尔从数据库加载用户。这意味着来自Spring Security的User对象仍然可以不附加到hibernate上下文(因为它是在之前的一个请求中创建的)。这迫使我在User对象周围创建一个包装器,在必要时手动将用户对象重新附加到新的hibernate会话。