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.
+++ /dev/null
-package de.juplo.yourshouter;
-
-import org.springframework.social.UserIdSource;
-
-
-/**
- * Simple implementation of {@link UserIdSource}, that always returns the
- * string <code>anonymous</code> 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";
- }
-}
--- /dev/null
+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();
+ }
+}
--- /dev/null
+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<String> CURRENT_USER = new ThreadLocal<>();
+
+
+ /**
+ * Fetches the ID of the current user from the thread-local.
+ *
+ * @return
+ * The ID of the current user, or <code>null</code> 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
+ * <code>true</code>, if a user is signed in, <code>false</code> 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();
+ }
+}
--- /dev/null
+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 <code>anonymous</code>,
+ * 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;
+ }
+}
{
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();
}
--- /dev/null
+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 <code>user</code> as the
+ * cookie-name.
+ */
+ private UserCookieGenerator()
+ {
+ generator.setCookieName("user");
+ }
+
+
+ /**
+ * Creates a cookie with the name <code>user</code>, 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 <code>user</code> 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 <code>user</code>.
+ *
+ * @param request
+ * The {@link HttpServletRequest} to read the cookie-value from.
+ * @return
+ * The value of the cookie with the name <code>user</code>, or
+ * <code>null</code>, 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;
+ }
+}
--- /dev/null
+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 <code>true</code>, 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();
+ }
+}
--- /dev/null
+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));
+ }
+}