From 8f6d3c83aa9651e593b57b3d47cfd50a4ae73661 Mon Sep 17 00:00:00 2001 From: Kai Moritz Date: Fri, 29 Jan 2016 13:49:43 +0100 Subject: [PATCH] Switched from the manual implemented authentication-layer to Spring Security --- pom.xml | 9 +- .../de/juplo/yourshouter/SecurityContext.java | 66 ---------- .../SocialAuthenticationEntryPoint.java | 50 ++++++++ .../de/juplo/yourshouter/SocialConfig.java | 3 +- ...=> SpringSecurityContextUserIdSource.java} | 11 +- ....java => SpringSecuritySignInAdapter.java} | 28 ++-- .../yourshouter/UserCookieGenerator.java | 100 --------------- .../yourshouter/UserCookieInterceptor.java | 120 ------------------ .../de/juplo/yourshouter/WebMvcConfig.java | 21 --- .../juplo/yourshouter/WebSecurityConfig.java | 89 +++++++++++++ 10 files changed, 169 insertions(+), 328 deletions(-) delete mode 100644 src/main/java/de/juplo/yourshouter/SecurityContext.java create mode 100644 src/main/java/de/juplo/yourshouter/SocialAuthenticationEntryPoint.java rename src/main/java/de/juplo/yourshouter/{SecurityContextUserIdSource.java => SpringSecurityContextUserIdSource.java} (53%) rename src/main/java/de/juplo/yourshouter/{UserCookieSignInAdapter.java => SpringSecuritySignInAdapter.java} (64%) delete mode 100644 src/main/java/de/juplo/yourshouter/UserCookieGenerator.java delete mode 100644 src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java create mode 100644 src/main/java/de/juplo/yourshouter/WebSecurityConfig.java diff --git a/pom.xml b/pom.xml index 91aaa8b..5c70ea4 100644 --- a/pom.xml +++ b/pom.xml @@ -40,6 +40,10 @@ org.springframework.boot spring-boot-starter-thymeleaf + + org.springframework.boot + spring-boot-starter-security + org.springframework.social spring-social-facebook @@ -52,11 +56,6 @@ org.springframework.social spring-social-facebook-web - - org.springframework.security - spring-security-crypto - runtime - org.apache.httpcomponents diff --git a/src/main/java/de/juplo/yourshouter/SecurityContext.java b/src/main/java/de/juplo/yourshouter/SecurityContext.java deleted file mode 100644 index 37ca54c..0000000 --- a/src/main/java/de/juplo/yourshouter/SecurityContext.java +++ /dev/null @@ -1,66 +0,0 @@ -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/SocialAuthenticationEntryPoint.java b/src/main/java/de/juplo/yourshouter/SocialAuthenticationEntryPoint.java new file mode 100644 index 0000000..4c3671c --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/SocialAuthenticationEntryPoint.java @@ -0,0 +1,50 @@ +package de.juplo.yourshouter; + +import java.io.IOException; +import javax.servlet.ServletException; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.core.AuthenticationException; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Service; + + +/** + * Specialized implementation of {@link AuthenticationEntryPoint}, that + * redirects to the social sign-in-page, to let the user decide to sign in or + * not. + * + * @author Kai Moritz + */ +@Service +public class SocialAuthenticationEntryPoint implements AuthenticationEntryPoint +{ + private static final Logger LOG = + LoggerFactory.getLogger(SocialAuthenticationEntryPoint.class); + + + /** + * {@inheritDoc} + * + * To commence the sign-in through the Graph-API, we only have to redirect + * to our already implemented sign-in-page. + */ + @Override + public void commence( + HttpServletRequest request, + HttpServletResponse response, + AuthenticationException exception + ) + throws + IOException, + ServletException + { + LOG.info( + "redirecting unauthenticated request {} to /signin.html", + request.getRequestURI() + ); + response.sendRedirect("/signin.html"); + } +} diff --git a/src/main/java/de/juplo/yourshouter/SocialConfig.java b/src/main/java/de/juplo/yourshouter/SocialConfig.java index 4573fbb..7cc9a63 100644 --- a/src/main/java/de/juplo/yourshouter/SocialConfig.java +++ b/src/main/java/de/juplo/yourshouter/SocialConfig.java @@ -11,6 +11,7 @@ import org.springframework.context.annotation.ScopedProxyMode; import org.springframework.social.UserIdSource; import org.springframework.core.env.Environment; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.security.core.context.SecurityContext; import org.springframework.social.config.annotation.ConnectionFactoryConfigurer; import org.springframework.social.config.annotation.EnableSocial; import org.springframework.social.config.annotation.SocialConfigurerAdapter; @@ -101,7 +102,7 @@ public class SocialConfig extends SocialConfigurerAdapter @Override public UserIdSource getUserIdSource() { - return new SecurityContextUserIdSource(); + return new SpringSecurityContextUserIdSource(); } diff --git a/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java b/src/main/java/de/juplo/yourshouter/SpringSecurityContextUserIdSource.java similarity index 53% rename from src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java rename to src/main/java/de/juplo/yourshouter/SpringSecurityContextUserIdSource.java index 662da57..d774060 100644 --- a/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java +++ b/src/main/java/de/juplo/yourshouter/SpringSecurityContextUserIdSource.java @@ -1,5 +1,8 @@ package de.juplo.yourshouter; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.social.UserIdSource; import org.springframework.util.Assert; @@ -10,7 +13,7 @@ import org.springframework.util.Assert; * * @author Kai Moritz */ -public class SecurityContextUserIdSource implements UserIdSource +public class SpringSecurityContextUserIdSource implements UserIdSource { /** * Retrieves the ID of the current user from the {@link SecurityContext}. @@ -22,7 +25,9 @@ public class SecurityContextUserIdSource implements UserIdSource @Override public String getUserId() { - Assert.state(SecurityContext.userSignedIn(), "No user signed in!"); - return SecurityContext.getCurrentUser(); + SecurityContext context = SecurityContextHolder.getContext(); + Authentication authentication = context.getAuthentication(); + Assert.state(authentication != null, "No user signed in!"); + return authentication.getName(); } } diff --git a/src/main/java/de/juplo/yourshouter/UserCookieSignInAdapter.java b/src/main/java/de/juplo/yourshouter/SpringSecuritySignInAdapter.java similarity index 64% rename from src/main/java/de/juplo/yourshouter/UserCookieSignInAdapter.java rename to src/main/java/de/juplo/yourshouter/SpringSecuritySignInAdapter.java index 88cf156..05c978b 100644 --- a/src/main/java/de/juplo/yourshouter/UserCookieSignInAdapter.java +++ b/src/main/java/de/juplo/yourshouter/SpringSecuritySignInAdapter.java @@ -1,8 +1,11 @@ package de.juplo.yourshouter; -import javax.servlet.http.HttpServletResponse; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.social.connect.Connection; import org.springframework.social.connect.web.SignInAdapter; import org.springframework.stereotype.Service; @@ -12,22 +15,25 @@ import org.springframework.web.context.request.NativeWebRequest; /** * Simple implementation of {@link SignInAdapter}. * + * This implementation signes in the user by storing him in the + * {@link SecurityContext} provided by Spring-Security, using the user-ID as + * principal. + * * We configured Spring-Social to call this implementation, to sign in the * user, after he was authenticated by Facebook. * * @author Kai Moritz */ @Service -public class UserCookieSignInAdapter implements SignInAdapter +public class SpringSecuritySignInAdapter implements SignInAdapter { private final static Logger LOG = - LoggerFactory.getLogger(UserCookieSignInAdapter.class); - + LoggerFactory.getLogger(SpringSecuritySignInAdapter.class); /** - * Stores the user in the security-context to sign him in. - * Also remembers the user for subsequent calls by storing the ID in the - * cookie. + * Stores the user in the {@link SecurityContext} provided by Spring Security + * to sign him in. Spring Security will automatically persist the + * authentication in the user-session for subsequent requests. * * @param user * The user-ID. We configured Spring-Social to call @@ -42,7 +48,6 @@ public class UserCookieSignInAdapter implements SignInAdapter * redirected to the default-post-sign-in-URL (configured in * {@link ProviderSinInController}) after a successfull authentication. * - * @see {@link UserCookieSignInAdapter} * @see {@link ProviderSignInController#postSignInUrl} */ @Override @@ -57,10 +62,9 @@ public class UserCookieSignInAdapter implements SignInAdapter user, connection.getKey().getProviderId() ); - SecurityContext.setCurrentUser(user); - UserCookieGenerator - .INSTANCE - .addCookie(user, request.getNativeResponse(HttpServletResponse.class)); + + SecurityContextHolder.getContext().setAuthentication( + new UsernamePasswordAuthenticationToken(user, null, null)); // We return null to trigger a redirect to "/". return null; diff --git a/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java b/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java deleted file mode 100644 index 48d7078..0000000 --- a/src/main/java/de/juplo/yourshouter/UserCookieGenerator.java +++ /dev/null @@ -1,100 +0,0 @@ -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 deleted file mode 100644 index 1b00e09..0000000 --- a/src/main/java/de/juplo/yourshouter/UserCookieInterceptor.java +++ /dev/null @@ -1,120 +0,0 @@ -package de.juplo.yourshouter; - - -import java.io.IOException; -import java.util.Collections; -import java.util.regex.Pattern; -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 static Pattern PATTERN = Pattern.compile("^/signin|canvas"); - - - 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. - * @throws java.io.IOException - * if something wents wrong, while sending the redirect to the - * sign-in-page. - */ - @Override - public boolean preHandle( - HttpServletRequest request, - HttpServletResponse response, - Object handler - ) - throws - IOException - { - if (PATTERN.matcher(request.getServletPath()).find()) - return true; - - 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); - } - } - - response.sendRedirect("/signin.html"); - return false; - } - - /** - * 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 index 6676ae6..d520d24 100644 --- a/src/main/java/de/juplo/yourshouter/WebMvcConfig.java +++ b/src/main/java/de/juplo/yourshouter/WebMvcConfig.java @@ -2,12 +2,9 @@ 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.ViewControllerRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; @@ -21,24 +18,6 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter @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)); - } - /** * {@inheritDoc} */ diff --git a/src/main/java/de/juplo/yourshouter/WebSecurityConfig.java b/src/main/java/de/juplo/yourshouter/WebSecurityConfig.java new file mode 100644 index 0000000..738485e --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/WebSecurityConfig.java @@ -0,0 +1,89 @@ +package de.juplo.yourshouter; + +import javax.inject.Inject; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter; + + +@Configuration +@EnableWebSecurity +public class WebSecurityConfig extends WebSecurityConfigurerAdapter +{ + @Inject + AuthenticationEntryPoint authenticationEntryPoint; + + /** + * We have to disable the default-configuration, because some of it does + * not work along with the canvas-page: + *
    + *
  • + * The support for CSRF-tokens consideres the initial call of Facebook to + * the canvas-page of our app as invalid, because it is issued as a post + * and the CSRF-token is missing. + *
  • + *
  • + * In the default-configuration, the X-Frame-Options: DENY is + * set for every response. This prevents the browser from showing our + * response inside Facebook, becaus that is an iFrame and the header + * forbidds to display our content in a frame. + *
  • + *
+ */ + public WebSecurityConfig() + { + super(true); + } + + + /** + * @{@inheritDoc} + * + * Override the default-implementation to configure the authentication + * mechanism of Spring Security. + * + * We drop the support of CSRF-tokens, inject our specialized implementation + * of the {@link AuthenticationEntryPoint}-interface , disable the headers, + * that deny, to display our content insiede a frame and configure the + * pages, that should be accessible without authentication. + * We also drop support for a logout-page and the default-login-in-page. + */ + @Override + protected void configure(HttpSecurity http) throws Exception + { + http + .addFilter(new WebAsyncManagerIntegrationFilter()) + .exceptionHandling() + .authenticationEntryPoint(authenticationEntryPoint) + .and() + .headers() + .frameOptions().disable() + .and() + .sessionManagement().and() + .securityContext().and() + .requestCache().and() + .anonymous().and() + .servletApi().and() + .authorizeRequests() + .antMatchers("/signin.html", "/signin/*", "/canvas/*").permitAll() + .anyRequest().authenticated(); + } + + /** + * {@inheritDoc} + * + * Override the default-implementation, to configure Spring Security to use + * in-memory authentication. + */ + @Override + public void configure(AuthenticationManagerBuilder auth) + throws + Exception + { + auth.inMemoryAuthentication(); + } +} -- 2.20.1