1

I'm writing a spring boot application (spring boot 1.2.2, spring 4.1.5), that is essentially a REST API. I've setup spring security to automatically load user, check authorization etc. upon every HTTP request. Users are stored in a database, I use hibernate to access all the data inside this database.

In short, the problem is: I load a user object from my database using hibernate on every HTTP request during PreFilter. I later receive this very object in my RestController's params, but it's no longer attached to any hibernate session. Because of this, I cannot access any lazy collections inside user object without re-reading it from the database first.

The question is: is there a way to start hibernate session during PreFilter and keep it until HTTP request is complete?

Long version, with code: First of all, I setup spring security so it'll load my User object as a part of authorization:
Security configuration:

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

    ...
}

User details service, which loads users from database:

@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;
    }
}

I also want to inject User object into controller methods, so I do it like this:

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

This works fine. However, if I try to lazy load a collection related to user, I get an exception:

@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

This happens because hiberante session was closed after the user object was loaded during PreFilter, and now the user variable is detached. In order to attach it again, I have to re-load it from the database:

@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
    ...
}

This works, but this seems error prone and unnecessary. I'm essentially loading user from the database twice. This seems like a waste considering that I've just loaded it in the previous method, DomainUserDetailsService.loadUserByUsername.

Is there a way to start hibernate session during PreFilter and keep it until HTTP request is complete?

Alexey
  • 6,550
  • 3
  • 44
  • 63
  • 1
    Why don't you consider explicitly loading api key collection when loading a user first time? Or another option would be to load just api key collection for given user when it is needed. Anyway you don't have to reload user entity. If you need some more explanation, let me know. – Pavla Nováková Jul 14 '15 at 14:00
  • @PavlaNováková Thank you for your suggestion, this is a decent solution to the problem. However, I'll still prefer a solution that allows me to access variables in a usual fashion, like user.getKeys(). – Alexey Jul 14 '15 at 14:13
  • With the solution I've suggested you can of course access collection of api keys for given user as user.getKeys() - in first case you can initialize the collection calling user.getKeys().size() within loadUserByUsername method for example and then access the collection as usual - user.getKeys() without getting LazyInitializationException and in the second case (explicit key collection loading later) the method logic would be: load api keys by given user id and then simply call user.setKeys(loadedKeys) and access them user.getKeys(). Open session in view is bad practice in my opinion. – Pavla Nováková Jul 14 '15 at 14:30

2 Answers2

2

There's the session per request pattern. Have a look here for a description. If you're using spring there's a filter implementation for this.

Be aware that its use is contoversial: Why is Hibernate Open Session in View considered a bad practice?

Community
  • 1
  • 1
user1675642
  • 717
  • 1
  • 5
  • 13
1

After some consideration, I went with the answer provided by user1675642: I've decided to use single hibernate session (with possible nested sessions) for every HTTP request. I don't see any problems with this approach for my particular case, because I always open database connection for every request, so opening it few methods up the stack trace doesn't really make any difference (actually it does make a difference because I don't have to open it twice).

However, I couldn't utilize the framework-provided OpenSessionInViewFilter. I was getting an "java.lang.IllegalArgumentException: ServletContext must not be null" exception. I wasn't able to figure out what part of my configuration was causing this problem, so I went ahead and wrote my own implementation:

First, you have to declare the following filter:

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

Then, you have to declare the following service:

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

However, this answers my question but it doesn't solve my problem. The thing is, Spring Security caches Authentication Principals. This means that authentication filter will only load user from database once in a while. This means, that the User object coming from Spring Security can still be unattached to hibernate context (because it was created during one of the previous requests). This forces me to create a wrapper around the User object that will manually re-attach user object to a new hibernate session when necessary.

Alexey
  • 6,550
  • 3
  • 44
  • 63
  • Thank you for these Filters, which helped solve our requirements together with the RefreshingUserDetailsSecurityContextRepository from https://stackoverflow.com/a/26697333. Our Application is very user centric, so most Controller-Methods receive a User-Object as @AuthenticationPrincipal. With the Help of the RefreshingUserDetailsSecurityContextRepository it is refreshed on every page load, which allows for realtime permission changes. Together with these Filters the Entity is correctly bound to the correct Jpa-Session and can directly be processed within the Controllers or their views. – Peter Körner Jul 23 '18 at 11:00