From: Kai Moritz Date: Fri, 29 Jan 2016 12:49:43 +0000 (+0100) Subject: Switched from the manual implemented authentication-layer to Spring Security X-Git-Url: http://juplo.de/gitweb/?p=examples%2Ffacebook-app;a=commitdiff_plain;h=8f6d3c83aa9651e593b57b3d47cfd50a4ae73661 Switched from the manual implemented authentication-layer to Spring Security --- 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/SecurityContextUserIdSource.java b/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java deleted file mode 100644 index 662da57..0000000 --- a/src/main/java/de/juplo/yourshouter/SecurityContextUserIdSource.java +++ /dev/null @@ -1,28 +0,0 @@ -package de.juplo.yourshouter; - -import org.springframework.social.UserIdSource; -import org.springframework.util.Assert; - - -/** - * Implementation of {@link UserIdSource}, that retrieves the ID of the current - * user from the {@link SecurityContext}. - * - * @author Kai Moritz - */ -public class SecurityContextUserIdSource implements UserIdSource -{ - /** - * Retrieves the ID of the current user from the {@link SecurityContext}. - * If no ID is found, an exception is thrown. - * - * @return The ID of the current user - * @throws IllegalStateException, if no current user is found. - */ - @Override - public String getUserId() - { - Assert.state(SecurityContext.userSignedIn(), "No user signed in!"); - return SecurityContext.getCurrentUser(); - } -} 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/SpringSecurityContextUserIdSource.java b/src/main/java/de/juplo/yourshouter/SpringSecurityContextUserIdSource.java new file mode 100644 index 0000000..d774060 --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/SpringSecurityContextUserIdSource.java @@ -0,0 +1,33 @@ +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; + + +/** + * Implementation of {@link UserIdSource}, that retrieves the ID of the current + * user from the {@link SecurityContext}. + * + * @author Kai Moritz + */ +public class SpringSecurityContextUserIdSource implements UserIdSource +{ + /** + * Retrieves the ID of the current user from the {@link SecurityContext}. + * If no ID is found, an exception is thrown. + * + * @return The ID of the current user + * @throws IllegalStateException, if no current user is found. + */ + @Override + public String getUserId() + { + 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/SpringSecuritySignInAdapter.java b/src/main/java/de/juplo/yourshouter/SpringSecuritySignInAdapter.java new file mode 100644 index 0000000..05c978b --- /dev/null +++ b/src/main/java/de/juplo/yourshouter/SpringSecuritySignInAdapter.java @@ -0,0 +1,72 @@ +package de.juplo.yourshouter; + + +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; +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 SpringSecuritySignInAdapter implements SignInAdapter +{ + private final static Logger LOG = + LoggerFactory.getLogger(SpringSecuritySignInAdapter.class); + + /** + * 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 + * {@link UserCookieSignInAdapter} to extract a user-ID from the + * connection. + * @param connection + * The connection. In our case a connection to Facebook. + * @param request + * The actual request. We need it, to store the cookie. + * @return + * We return null, to indicate, that the user should be + * redirected to the default-post-sign-in-URL (configured in + * {@link ProviderSinInController}) after a successfull authentication. + * + * @see {@link ProviderSignInController#postSignInUrl} + */ + @Override + public String signIn( + String user, + Connection connection, + NativeWebRequest request + ) + { + LOG.info( + "signing in user {} (connected via {})", + user, + connection.getKey().getProviderId() + ); + + 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/UserCookieSignInAdapter.java b/src/main/java/de/juplo/yourshouter/UserCookieSignInAdapter.java deleted file mode 100644 index 88cf156..0000000 --- a/src/main/java/de/juplo/yourshouter/UserCookieSignInAdapter.java +++ /dev/null @@ -1,68 +0,0 @@ -package de.juplo.yourshouter; - -import javax.servlet.http.HttpServletResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.social.connect.Connection; -import org.springframework.social.connect.web.SignInAdapter; -import org.springframework.stereotype.Service; -import org.springframework.web.context.request.NativeWebRequest; - - -/** - * Simple implementation of {@link SignInAdapter}. - * - * 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 -{ - private final static Logger LOG = - LoggerFactory.getLogger(UserCookieSignInAdapter.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. - * - * @param user - * The user-ID. We configured Spring-Social to call - * {@link UserCookieSignInAdapter} to extract a user-ID from the - * connection. - * @param connection - * The connection. In our case a connection to Facebook. - * @param request - * The actual request. We need it, to store the cookie. - * @return - * We return null, to indicate, that the user should be - * redirected to the default-post-sign-in-URL (configured in - * {@link ProviderSinInController}) after a successfull authentication. - * - * @see {@link UserCookieSignInAdapter} - * @see {@link ProviderSignInController#postSignInUrl} - */ - @Override - public String signIn( - String user, - Connection connection, - NativeWebRequest request - ) - { - LOG.info( - "signing in user {} (connected via {})", - user, - connection.getKey().getProviderId() - ); - SecurityContext.setCurrentUser(user); - UserCookieGenerator - .INSTANCE - .addCookie(user, request.getNativeResponse(HttpServletResponse.class)); - - // We return null to trigger a redirect to "/". - return null; - } -} 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(); + } +}