Implemented a simple UserIdSource, that stores the user in a cookie part-03
authorKai Moritz <kai@juplo.de>
Mon, 25 Jan 2016 01:01:28 +0000 (02:01 +0100)
committerKai Moritz <kai@juplo.de>
Sat, 30 Jan 2016 14:01:07 +0000 (15:01 +0100)
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.

src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java [deleted file]
src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java [new file with mode: 0644]
src/main/java/de/juplo/yourshouter/SecurityContext.java [new file with mode: 0644]
src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java [new file with mode: 0644]
src/main/java/de/juplo/yourshouter/SocialConfig.java
src/main/java/de/juplo/yourshouter/UserCookieGenerator.java [new file with mode: 0644]
src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java [new file with mode: 0644]
src/main/java/de/juplo/yourshouter/WebMvcConfig.java [new file with mode: 0644]

diff --git a/src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java b/src/main/java/de/juplo/yourshouter/AnonymousUserIdSource.java
deleted file mode 100644 (file)
index c09f400..0000000
+++ /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 <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";
-  }
-}
diff --git a/src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java b/src/main/java/de/juplo/yourshouter/ProviderUserIdConnectionSignUp.java
new file mode 100644 (file)
index 0000000..fa4613d
--- /dev/null
@@ -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 (file)
index 0000000..37ca54c
--- /dev/null
@@ -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<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();
+  }
+}
diff --git a/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java b/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java
new file mode 100644 (file)
index 0000000..9fae323
--- /dev/null
@@ -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 <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;
+  }
+}
index 5308709..2abcd42 100644 (file)
@@ -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 (file)
index 0000000..48d7078
--- /dev/null
@@ -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 <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;
+  }
+}
diff --git a/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java b/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java
new file mode 100644 (file)
index 0000000..6a6cba6
--- /dev/null
@@ -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 <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();
+  }
+}
diff --git a/src/main/java/de/juplo/yourshouter/WebMvcConfig.java b/src/main/java/de/juplo/yourshouter/WebMvcConfig.java
new file mode 100644 (file)
index 0000000..e5c6d6f
--- /dev/null
@@ -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));
+  }
+}