Skip to content
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@

import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.common.base.Strings;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import de.rwth.idsg.steve.service.WebUserService;
import de.rwth.idsg.steve.web.api.ApiControllerAdvice;
import lombok.RequiredArgsConstructor;
Expand All @@ -32,6 +34,7 @@
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.AuthenticationEntryPoint;
Expand All @@ -42,6 +45,9 @@
import jakarta.servlet.http.HttpServletResponse;

import java.io.IOException;
import java.util.Collections;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

/**
* @author Sevket Goekay <[email protected]>
Expand All @@ -52,10 +58,18 @@
@RequiredArgsConstructor
public class ApiAuthenticationManager implements AuthenticationManager, AuthenticationEntryPoint {

// Because Guava's cache does not accept a null value
private static final UserDetails DUMMY_USER = new User("#", "#", Collections.emptyList());

private final WebUserService webUserService;
private final PasswordEncoder passwordEncoder;
private final ObjectMapper jacksonObjectMapper;

private final Cache<String, UserDetails> userCache = CacheBuilder.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES) // TTL
.maximumSize(100)
.build();

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = (String) authentication.getPrincipal();
Expand All @@ -65,7 +79,7 @@ public Authentication authenticate(Authentication authentication) throws Authent
throw new BadCredentialsException("Required parameters missing");
}

UserDetails userDetails = webUserService.loadUserByUsernameForApi(username);
UserDetails userDetails = getFromCacheOrDatabase(username);
if (!areValuesSet(userDetails)) {
throw new DisabledException("The user does not exist, exists but is disabled or has API access disabled.");
}
Expand Down Expand Up @@ -99,8 +113,19 @@ public void commence(HttpServletRequest request,
response.getWriter().print(jacksonObjectMapper.writeValueAsString(apiResponse));
}

private UserDetails getFromCacheOrDatabase(String username) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do you set the cache logic here instead of webUserService.loadUserByUsernameForApi?

I don't know if it is a choice to not use it but Spring has its own way of doing caches: https://spring.io/guides/gs/caching
And you can still use guava under to wood if you want it: https://docs.spring.io/spring-framework/docs/4.2.x/javadoc-api/org/springframework/cache/guava/GuavaCacheManager.html

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@goekay Just for my technical knowledge because I didn't use them before: why do you use guava directly and not hidden behind spring cache?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

a combination of multiple reasons actually (disclaimer: i have been using both of them for a long time)

  • stylistic preference if it is just about a localized cache usage, instead of something big or application-wide
  • tighter control i can have with guava. this does not matter when using GuavaCacheManager, since the same can be done with that... but then, if you control guava like this, why introduce spring magic? which brings me to my next point.
  • absence of multiple spring layers, abstractions, which can lead to weird misbehaviour and gotchas
  • to be consistent with the codebase: steve has this direct usage of Guava at some other places, there is no usage of Spring cache

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the answer! 👍

why introduce spring magic?

That could be true for almost every spring import ;)

try {
return userCache.get(username, () -> {
UserDetails user = webUserService.loadUserByUsernameForApi(username);
return (user == null) ? DUMMY_USER : user;
});
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}

private static boolean areValuesSet(UserDetails userDetails) {
if (userDetails == null) {
if (userDetails == null || userDetails == DUMMY_USER) {
return false;
}
if (!userDetails.isEnabled()) {
Expand Down