From 02f599692669d48f9865764fda994ad61d203ffb Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Mon, 25 Jan 2016 02:01:28 +0100 Subject: [PATCH] Implemented a simple UserIdSource, that stores the user in a cookie This concept was borrowed from the official example "Spring Social Canvas". The idea to store the internal user-id in a cookie and later load the data of the user according to the cookie is inherent insecure and must not be used in a production environment. One simply can use Spring-Security instead - we will show how to switch in a later example. This implementation was choosen only for educational purposes, because it clarifys the design of Spring Social. --- .../yourshouter/AnonymousUserIdSource.java | 21 ---- .../ProviderUserIdConnectionSignUp.java | 42 +++++++ .../de/juplo/yourshouter/SecurityContext.java | 66 +++++++++++ .../SecurityContextUserIdSource.java | 42 +++++++ .../de/juplo/yourshouter/SocialConfig.java | 11 +- .../yourshouter/UserCookieGenerator.java | 100 ++++++++++++++++ .../yourshouter/UserCookieInterceptor.java | 107 ++++++++++++++++++ .../de/juplo/yourshouter/WebMvcConfig.java | 40 +++++++ 8 files changed, 404 insertions(+), 25 deletions(-) delete mode 100644 src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java create mode 100644 src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java create mode 100644 src/main/java/de/juplo/yourshouter/SecurityContext.java create mode 100644 src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java create mode 100644 src/main/java/de/juplo/yourshouter/UserCookieGenerator.java create mode 100644 src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java create mode 100644 src/main/java/de/juplo/yourshouter/WebMvcConfig.java diff --git a/src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java b/src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java deleted file mode 100644 index c09f400..0000000 --- a/src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java +++ /dev/null @@ -1,21 +0,0 @@ -package de.juplo.yourshouter; - -import org.springframework.social.UserIdSource; - - -/** - * Simple implementation of {@link UserIdSource}, that always returns the - * string anonymous as user-ID, like the UserIdSource, that is - * automatically configured by Spring-Boot, if Spring-Security is not - * present. - * - * @author Kai Moritz - */ -public class AnonymousUserIdSource implements UserIdSource -{ - @Override - public String getUserId() - { - return "anonymous"; - } -} diff --git a/src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java b/src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java new file mode 100644 index 0000000..fa4613d --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java @@ -0,0 +1,42 @@ +package de.juplo.yourshouter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.social.connect.Connection; +import org.springframework.social.connect.ConnectionKey; +import org.springframework.social.connect.ConnectionSignUp; +import org.springframework.stereotype.Service; + + +/** + * Extracts the local user-ID from the data given by the provider. + * + * @author Kai Moritz + */ +@Service +public class ProviderUserIdConnectionSignUp implements ConnectionSignUp +{ + private final Logger LOG = + LoggerFactory.getLogger(ProviderUserIdConnectionSignUp.class); + + + /** + * This implementation simply reuse the ID, that was provided by the provider. + * + * @param connection + * The {@link Connection} for the unknown user. + * @return + * The user-ID, that was provided by the provider. + */ + @Override + public String execute(Connection connection) + { + ConnectionKey key = connection.getKey(); + LOG.info( + "signing up user {} from provider {}", + key.getProviderUserId(), + key.getProviderId() + ); + return key.getProviderUserId(); + } +} diff --git a/src/main/java/de/juplo/yourshouter/SecurityContext.java b/src/main/java/de/juplo/yourshouter/SecurityContext.java new file mode 100644 index 0000000..37ca54c --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/SecurityContext.java @@ -0,0 +1,66 @@ +package de.juplo.yourshouter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + + +/** + * Simple SecurityContext that stores the currently signed-in connection in a + * thread local. + * + * @author Kai Moritz + */ +public final class SecurityContext +{ + private final static Logger LOG = LoggerFactory.getLogger(SecurityContext.class); + private final static ThreadLocal CURRENT_USER = new ThreadLocal<>(); + + + /** + * Fetches the ID of the current user from the thread-local. + * + * @return + * The ID of the current user, or null if no user is known. + */ + public static String getCurrentUser() + { + String user = CURRENT_USER.get(); + LOG.debug("current user: {}", user); + return user; + } + + /** + * Stores the given ID as the ID of the current user in the thread-local. + * + * @param user + * The ID to store as the ID of the current user. + */ + public static void setCurrentUser(String user) + { + LOG.debug("setting current user: {}", user); + CURRENT_USER.set(user); + } + + /** + * Checks, if a user is signed in. That is, if the ID of a user is stored in + * the thread-local. + * + * @return + * true, if a user is signed in, false otherwise. + */ + public static boolean userSignedIn() + { + boolean signedIn = CURRENT_USER.get() != null; + LOG.debug("user signed in: {}", signedIn); + return signedIn; + } + + /** + * Removes the ID of the current user from the thread-local. + */ + public static void remove() + { + LOG.debug("removing current user"); + CURRENT_USER.remove(); + } +} diff --git a/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java b/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java new file mode 100644 index 0000000..9fae323 --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java @@ -0,0 +1,42 @@ +package de.juplo.yourshouter; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.social.UserIdSource; + + +/** + * Implementation of {@link UserIdSource}, that retrieves the ID of the current + * user from the {@link SecurityContext}. + * + * @author Kai Moritz + */ +public class SecurityContextUserIdSource implements UserIdSource +{ + private final static Logger LOG = + LoggerFactory.getLogger(SecurityContextUserIdSource.class); + + + /** + * Retrieves the ID of the current user from the {@link SecurityContext}. + * + * @return + * The ID of the current user, or the special ID anonymous, + * if no current user is present. + */ + @Override + public String getUserId() + { + String user = SecurityContext.getCurrentUser(); + if (user != null) + { + LOG.debug("found user \"{}\" in the security-context", user); + } + else + { + LOG.info("found no user in the security-context, using \"anonymous\""); + user = "anonymous"; + } + return user; + } +} diff --git a/src/main/java/de/juplo/yourshouter/SocialConfig.java b/src/main/java/de/juplo/yourshouter/SocialConfig.java index 5308709..2abcd42 100644 --- a/src/main/java/de/juplo/yourshouter/SocialConfig.java +++ b/src/main/java/de/juplo/yourshouter/SocialConfig.java @@ -70,22 +70,25 @@ public class SocialConfig extends SocialConfigurerAdapter { InMemoryUsersConnectionRepository repository = new InMemoryUsersConnectionRepository(connectionFactoryLocator); + repository.setConnectionSignUp(new ProviderUserIdConnectionSignUp()); return repository; } /** - * Configure a {@link UserIdSource}, that is equivalent to the one, that is - * created by Spring-Boot. + * Configure our new implementation of {@link UserIdSource}, that retrieves + * the current user from the {@link SecurityContext}. * * @return * An instance of {@link AnonymousUserIdSource}. * - * @see {@link AnonymousUserIdSource} + * @see {@link SecurityContextUserIdSource} + * @see {@link SecurityContext} + * @see {@link UserCookieInterceptor} */ @Override public UserIdSource getUserIdSource() { - return new AnonymousUserIdSource(); + return new SecurityContextUserIdSource(); } diff --git a/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java b/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java new file mode 100644 index 0000000..48d7078 --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java @@ -0,0 +1,100 @@ +package de.juplo.yourshouter; + + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.web.util.CookieGenerator; +import org.thymeleaf.util.StringUtils; + + +/** + * Utility class for managing the cookie that remembers the user. + * + * @author Kai Moritz + */ +final class UserCookieGenerator +{ + private final static Logger LOG = + LoggerFactory.getLogger(UserCookieGenerator.class); + + public final static UserCookieGenerator INSTANCE = new UserCookieGenerator(); + + + private final CookieGenerator generator = new CookieGenerator(); + + + /** + * Constructs an instance of this class, using user as the + * cookie-name. + */ + private UserCookieGenerator() + { + generator.setCookieName("user"); + } + + + /** + * Creates a cookie with the name user, that stores the ID of + * the user for subsequent calls. + * + * @param user + * The ID of the current user + * @param response + * The {@link HttpServletResponse} to store the cookie in. + */ + public void addCookie(String user, HttpServletResponse response) + { + LOG.debug("adding cookie {}={}", generator.getCookieName(), user); + generator.addCookie(response, user); + } + + /** + * Removes the cookie with the name user by storing an empty + * string as its value. + * + * @param response + * The {@link HttpServletResponse} to remove the cookie from. + */ + public void removeCookie(HttpServletResponse response) + { + LOG.debug("removing cookie {}", generator.getCookieName()); + generator.addCookie(response, ""); + } + + /** + * Reads the current value of the cookie with the name user. + * + * @param request + * The {@link HttpServletRequest} to read the cookie-value from. + * @return + * The value of the cookie with the name user, or + * null, if no cookie by that name can be found or the value + * of the cookie is an empty string. + */ + public String readCookieValue(HttpServletRequest request) + { + String name = generator.getCookieName(); + Cookie[] cookies = request.getCookies(); + if (cookies != null) + { + for (Cookie cookie : cookies) + { + if (cookie.getName().equals(name)) + { + String value = cookie.getValue(); + if (!StringUtils.isEmptyOrWhitespace(value)) + { + LOG.debug("found cookie {}={}", name, value); + return value; + } + } + } + } + LOG.debug("cookie \"{}\" not found!", name); + return null; + } +} diff --git a/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java b/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java new file mode 100644 index 0000000..6a6cba6 --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java @@ -0,0 +1,107 @@ +package de.juplo.yourshouter; + + +import java.util.Collections; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import org.springframework.social.connect.UsersConnectionRepository; +import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; + + +/** + * Intercepts all requests to handle the user-cookie. + * + * @author Kai Moritz + */ +public final class UserCookieInterceptor extends HandlerInterceptorAdapter +{ + private final static Logger LOG = + LoggerFactory.getLogger(UserCookieInterceptor.class); + + + private final UsersConnectionRepository repository; + + + /** + * Creates an instance of this class, that uses the given instance of + * {@link UsersConnectionRepository}. + * + * @param repository + * The instance of {@link UsersConnectionRepository} to use. + */ + public UserCookieInterceptor(UsersConnectionRepository repository) + { + this.repository = repository; + } + + + /** + * Before a request is handled, the current user is loaded from the cookie, + * if the cookie is present and the user is known. If the user is not known, + * the cookie is removed. + * + * @param request + * The {@link HttpServletRequest} that is intercepted. + * @param response + * The {@link HttpServletResponse} that is intercepted. + * @param handler + * The handler, that handles the intercepted request. + * @return + * Always true, to indicate, that the intercepted request + * should be handled normally. + */ + @Override + public boolean preHandle( + HttpServletRequest request, + HttpServletResponse response, + Object handler + ) + { + String user = UserCookieGenerator.INSTANCE.readCookieValue(request); + if (user != null) + { + if (!repository + .findUserIdsConnectedTo("facebook", Collections.singleton(user)) + .isEmpty() + ) + { + LOG.info("loading user {} from cookie", user); + SecurityContext.setCurrentUser(user); + return true; + } + else + { + LOG.warn("user {} is not known!", user); + UserCookieGenerator.INSTANCE.removeCookie(response); + } + } + return true; + } + + /** + * After a request, the user is removed from the security-context. + * + * @param request + * The {@link HttpServletRequest} that is intercepted. + * @param response + * The {@link HttpServletResponse} that is intercepted. + * @param handler + * The handler, that handles the intercepted request. + * @param exception + * If an exception was thrown during the handling of this request, it is + * handed in through this parameter. + */ + @Override + public void afterCompletion( + HttpServletRequest request, + HttpServletResponse response, + Object handler, + Exception exception + ) + { + SecurityContext.remove(); + } +} diff --git a/src/main/java/de/juplo/yourshouter/WebMvcConfig.java b/src/main/java/de/juplo/yourshouter/WebMvcConfig.java new file mode 100644 index 0000000..e5c6d6f --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/WebMvcConfig.java @@ -0,0 +1,40 @@ +package de.juplo.yourshouter; + + + +import javax.inject.Inject; + +import org.springframework.context.annotation.Configuration; +import org.springframework.social.connect.UsersConnectionRepository; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; +import org.springframework.web.servlet.config.annotation.InterceptorRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; + + +/** + * Spring MVC Configuration. + * + * @author Kai Moritz + */ +@Configuration +@EnableWebMvc +public class WebMvcConfig extends WebMvcConfigurerAdapter +{ + @Inject + private UsersConnectionRepository usersConnectionRepository; + + + /** + * Configure the {@link UserCookieInterceptor} to intercept all requests. + * + * @param registry + * The {@link InterceptorRegistry} to use. + * + * @see {@link UserCookieInterceptor} + */ + @Override + public void addInterceptors(InterceptorRegistry registry) + { + registry.addInterceptor(new UserCookieInterceptor(usersConnectionRepository)); + } +} -- 2.20.1