Reputation: 953
I have 2 types of users, Schools and Volunteers, Each have their own UserDetails implementation, and UserDetailsService implementations, also each have different http configuration:
This one is for schools
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/school/**")
.userDetailsService(service)
.formLogin().loginPage("/school/login").permitAll().usernameParameter("username")
.passwordParameter("password").failureUrl("/school/login?error=true")
.and()
.authorizeRequests()
.antMatchers("/school/register").permitAll()
.antMatchers("/school/confirm**").permitAll()
.antMatchers("/school/**").hasRole(School.class.getSimpleName())
.and()
.rememberMe().tokenValiditySeconds(2592000).rememberMeParameter("remember-me")
.userDetailsService(service).key("S").rememberMeCookieName("S")
.and()
.logout().logoutSuccessUrl("/").logoutUrl("/school/logout");
}
And this one is for volunteers
protected void configure(HttpSecurity http) throws Exception {
http
.antMatcher("/volunteer/**")
.userDetailsService(service)
.formLogin().loginPage("/volunteer/login").permitAll().usernameParameter("username")
.passwordParameter("password").failureUrl("/volunteer/login?error=true")
.and()
.authorizeRequests()
.antMatchers("/volunteer/register").permitAll()
.antMatchers("/volunteer/confirm**").permitAll()
.antMatchers("/volunteer/**").hasRole(Volunteer.class.getSimpleName())
.and()
.rememberMe().tokenValiditySeconds(2592000).rememberMeParameter("remember-me")
.userDetailsService(service).key("V").rememberMeCookieName("V")
.and()
.logout().logoutSuccessUrl("/").logoutUrl("/volunteer/logout");
}
In both service
is their user details service implementation
They are configured in two nested classes that extends WebSecurityConfigurerAdapter
.
So my problem is that when i log in as school, and then go to (for example) http://localhost/volunteer
, it shows 403 forbidden instead of login form.
I turned on debug and saw that it just found authenticated user in security context and use it, and while it has no required role it fails with 403, then if i explicitly go to login page for volunteers and login, it then couldn't login as school with the same 403. Here is logs:
This and etc. for login as school:
17:11:50.628 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/volunteer/**'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/**'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 1 of 15 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 2 of 15 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.context.HttpSessionSecurityContextRepository: HttpSession returned null object for SPRING_SECURITY_CONTEXT
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.context.HttpSessionSecurityContextRepository: No SecurityContext was available from the HttpSession: org.apache.catalina.session.StandardSessionFacade@1dd229eb. A new one will be created.
17:11:50.629 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 3 of 15 in additional filter chain; firing Filter: 'HeaderWriterFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.header.writers.HstsHeaderWriter: Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@19d9486b
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 4 of 15 in additional filter chain; firing Filter: 'CharacterEncodingFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 5 of 15 in additional filter chain; firing Filter: 'CsrfFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 6 of 15 in additional filter chain; firing Filter: 'LogoutFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/logout'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.FilterChainProxy: /school/login at position 7 of 15 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/school/login'; against '/school/login'
17:11:50.630 [http-nio-8081-exec-9] DEBUG security.web.authentication.UsernamePasswordAuthenticationFilter: Request is to process authentication
And then when i go to volunteer section:
17:15:03.860 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/**'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 1 of 15 in additional filter chain; firing Filter: 'WebAsyncManagerIntegrationFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 2 of 15 in additional filter chain; firing Filter: 'SecurityContextPersistenceFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.context.HttpSessionSecurityContextRepository: Obtained a valid SecurityContext from SPRING_SECURITY_CONTEXT: 'org.springframework.security.core.context.SecurityContextImpl@b35de31c: Authentication: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 3 of 15 in additional filter chain; firing Filter: 'HeaderWriterFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.header.writers.HstsHeaderWriter: Not injecting HSTS header since it did not match the requestMatcher org.springframework.security.web.header.writers.HstsHeaderWriter$SecureRequestMatcher@439cf926
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 4 of 15 in additional filter chain; firing Filter: 'CharacterEncodingFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 5 of 15 in additional filter chain; firing Filter: 'CsrfFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 6 of 15 in additional filter chain; firing Filter: 'LogoutFilter'
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Request 'GET /volunteer' doesn't match 'POST /volunteer/logout
17:15:03.861 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 7 of 15 in additional filter chain; firing Filter: 'UsernamePasswordAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Request 'GET /volunteer' doesn't match 'POST /volunteer/login
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 8 of 15 in additional filter chain; firing Filter: 'BasicAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 9 of 15 in additional filter chain; firing Filter: 'RequestCacheAwareFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 10 of 15 in additional filter chain; firing Filter: 'SecurityContextHolderAwareRequestFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 11 of 15 in additional filter chain; firing Filter: 'RememberMeAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.authentication.rememberme.RememberMeAuthenticationFilter: SecurityContextHolder not populated with remember-me token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 12 of 15 in additional filter chain; firing Filter: 'AnonymousAuthenticationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.authentication.AnonymousAuthenticationFilter: SecurityContextHolder not populated with anonymous token, as it already contained: 'org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 13 of 15 in additional filter chain; firing Filter: 'SessionManagementFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 14 of 15 in additional filter chain; firing Filter: 'ExceptionTranslationFilter'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.FilterChainProxy: /volunteer at position 15 of 15 in additional filter chain; firing Filter: 'FilterSecurityInterceptor'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/register'
17:15:03.864 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/confirm**'
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.util.matcher.AntPathRequestMatcher: Checking match of request : '/volunteer'; against '/volunteer/**'
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.access.intercept.FilterSecurityInterceptor: Secure object: FilterInvocation: URL: /volunteer; Attributes: [hasRole('ROLE_Volunteer')]
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.web.access.intercept.FilterSecurityInterceptor: Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@b35de31c: Principal: com.primaryrun.model.school.SecureSchool@16d; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@166c8: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: EC164B454F5406F933CCB3C6CFEF2E1D; Granted Authorities: ROLE_School
17:15:03.865 [http-nio-8081-exec-5] DEBUG security.access.vote.AffirmativeBased: Voter: org.springframework.security.web.access.expression.WebExpressionVoter@3466a837, returned: -1
17:15:03.887 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: No properties file found for [classpath:localization/messages_en_US] - neither plain properties nor XML
17:15:03.888 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: Re-caching properties for filename [classpath:localization/messages_en] - file hasn't been modified
17:15:03.889 [http-nio-8081-exec-5] DEBUG context.support.ReloadableResourceBundleMessageSource: No properties file found for [classpath:localization/messages] - neither plain properties nor XML
17:15:03.890 [http-nio-8081-exec-5] DEBUG security.web.access.ExceptionTranslationFilter: Access is denied (user is not anonymous); delegating to AccessDeniedHandler
Is it possible in spring security to be logged in as different users at the same time?
If so, how to achieve this? Do I have to somehow create new security context, authentication provider or some other stuff?
Why does it use Schools' security context if i am at the volunteers side?
Upvotes: 0
Views: 1751
Reputation: 953
Solved by adding a filter to spring security filter chain
http.addFilterBefore(new MultipleLoginFilter(), SecurityContextPersistenceFilter.class);
That store/extract security context in session under different name. Used annotation to describe dimension via reflection:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SecurityDimension {
String name();
}
That annotate user details implementations. Also configured logout to invalidate only the user that pressed logout:
http.clearAuthentication(true)
.invalidateHttpSession(false).logoutSuccessHandler(new SimpleLoginInvalidator())
Where SimpleLoginInvalidator
is:
public class SimpleLoginInvalidator implements LogoutSuccessHandler {
private static final Logger logger = LoggerFactory.getLogger(SimpleLoginInvalidator.class);
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
HttpSession session = request.getSession();
try {
String dimensionName = authentication.getPrincipal().getClass().getAnnotation(SecurityDimension.class).name();
session.removeAttribute(dimensionName + "_" + SPRING_SECURITY_CONTEXT_KEY);
logger.debug("Cleared security context for dimension: " + dimensionName);
} catch (NullPointerException npe) {
logger.debug("Could not clear custom security context attribute", npe);
}
}
}
Hope it helps someone, still looking for more 'out of the box' answer
MultipleLoginFilter :
public class MultipleLoginFilter extends OncePerRequestFilter {
private static final Pattern pattern = Pattern.compile("(?<!/)/([^/?#]+)");
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
logger.debug("Performing multiple login management");
HttpSession session = request.getSession(false);
String path = request.getRequestURI().replace(request.getContextPath(), "");
String desiredDimension = getDesiredDimension(path);
logger.debug("Requesting security context for security dimension: " + desiredDimension);
SecurityContext context = (SecurityContext) session.getAttribute(SPRING_SECURITY_CONTEXT_KEY);
if (context == null) {
logger.debug("Current context is empty, trying to find and associate the one stored session...");
populateSecurityContextByDimension(session, desiredDimension);
} else {
logger.debug("Extracted non-null current security context: " + context);
String dimensionName = context.getAuthentication().getPrincipal().getClass().getAnnotation(SecurityDimension.class).name();
if (!path.matches("/" + dimensionName + "(/.+)?")) {
logger.debug("Extracted security context is not associated with desired security dimension(" + desiredDimension + "). Storing in session...");
session.setAttribute(dimensionName + "_" + SPRING_SECURITY_CONTEXT_KEY, context);
populateSecurityContextByDimension(session, desiredDimension);
} else {
logger.debug("Current security context is associated with desired security dimension, proceed as default.");
}
}
filterChain.doFilter(request, response);
}
private String getDesiredDimension(String requestUrl) {
Matcher m = pattern.matcher(requestUrl);
String desiredDimension = "";
if (m.find() && m.groupCount() == 1) {
desiredDimension = m.group(1);
} else {
logger.error("Could not identify desired security dimension from url: " + requestUrl);
}
return desiredDimension;
}
private void populateSecurityContextByDimension(HttpSession session, String desiredDimension) {
logger.debug("Extracting stored security context associated with desired dimension(" + desiredDimension + ")...");
SecurityContext localContext = (SecurityContext) session.getAttribute(desiredDimension + "_" + SPRING_SECURITY_CONTEXT_KEY);
session.setAttribute(SPRING_SECURITY_CONTEXT_KEY, localContext);
if(localContext != null) {
logger.debug("Storing extracted context as current, context: " + localContext);
} else {
logger.debug("No security context for dimension(" + desiredDimension + ") in session, assigning null.");
}
}
}
Upvotes: 1