问题
We try to substitute an existing Spring Security Basic Login for a REST-API in an Open Source Application to achieve a custom login with a token. I read this blogpost about the topic: http://javattitude.com/2014/06/07/spring-security-custom-token-based-rest-authentication/
When the request has no header named "Cookie", I get correcty a 401 - unauthorized response (expected behaviour). When the request has a valid token, I get an infinite loop causing a java.lang.StackOverflowError:
Exception in thread "http-bio-8080-exec-45" java.lang.StackOverflowError
at org.apache.tomcat.util.http.NamesEnumerator.<init>(MimeHeaders.java:402)
at org.apache.tomcat.util.http.MimeHeaders.names(MimeHeaders.java:228)
at org.apache.catalina.connector.Request.getHeaderNames(Request.java:2108)
at org.apache.catalina.connector.RequestFacade.getHeaderNames(RequestFacade.java:726)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at javax.servlet.http.HttpServletRequestWrapper.getHeaderNames(HttpServletRequestWrapper.java:103)
at org.activiti.rest.security.CustomTokenAuthenticationFilter.attemptAuthentication(CustomTokenAuthenticationFilter.java:43)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:211)
at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:65)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:166)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:749)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:487)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:412)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:339)
at org.springframework.security.web.firewall.RequestWrapper$FirewalledRequestAwareRequestDispatcher.forward(RequestWrapper.java:132)
at org.activiti.rest.security.TokenSimpleUrlAuthenticationSuccessHandler.onAuthenticationSuccess(TokenSimpleUrlAuthenticationSuccessHandler.java:30)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:298)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.authentication.logout.LogoutFilter.doFilter(LogoutFilter.java:110)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:65)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:101)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:342)
at org.springframework.security.web.FilterChainProxy.doFilterInternal(FilterChainProxy.java:192)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:166)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:344)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:261)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)
at org.apache.catalina.core.ApplicationDispatcher.invoke(ApplicationDispatcher.java:749)
at org.apache.catalina.core.ApplicationDispatcher.processRequest(ApplicationDispatcher.java:487)
at org.apache.catalina.core.ApplicationDispatcher.doForward(ApplicationDispatcher.java:412)
at org.apache.catalina.core.ApplicationDispatcher.forward(ApplicationDispatcher.java:339)
at org.springframework.security.web.firewall.RequestWrapper$FirewalledRequestAwareRequestDispatcher.forward(RequestWrapper.java:132)
at org.activiti.rest.security.TokenSimpleUrlAuthenticationSuccessHandler.onAuthenticationSuccess(TokenSimpleUrlAuthenticationSuccessHandler.java:30)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:331)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.successfulAuthentication(AbstractAuthenticationProcessingFilter.java:298)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:235)
at org.activiti.rest.security.CustomTokenAuthenticationFilter.doFilter(CustomTokenAuthenticationFilter.java:86)
My Spring Security Configuration looks like this:
@Configuration
@EnableWebSecurity
@EnableWebMvcSecurity
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public AuthenticationProvider authenticationProvider() {
return new BasicAuthenticationProvider();
}
@Autowired
AuthenticationProvider basicAuthenticationProvider;
@Bean
public CustomTokenAuthenticationFilter customTokenAuthenticationFilter(){
System.out.println("+++ create new CustomTokenAuthenticationFilter for path=/**");
return new CustomTokenAuthenticationFilter("/**");
};
@Autowired
CustomTokenAuthenticationFilter customTokenAuthenticationFilter;
@Override
protected void configure(HttpSecurity http) throws Exception {
System.out.println("init of http security START");
http
.authenticationProvider(authenticationProvider())
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()
.and()//.authenticationProvider(basicAuthenticationProvider);
.addFilterBefore(customTokenAuthenticationFilter, BasicAuthenticationFilter.class)
.httpBasic();
//.and().addFilter(filter);
System.out.println("init of http security DONE");
}
}
I already tried to change the URL-Mapping from /** to /activiti-rest/** but then, the Basic Authentication kicks in again.
This is my custom Token Authentication filter:
public class CustomTokenAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
private static final Logger logger = LoggerFactory.getLogger(CustomTokenAuthenticationFilter.class);
public CustomTokenAuthenticationFilter(String defaultFilterProcessesUrl) {
super(defaultFilterProcessesUrl);
super.setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher(defaultFilterProcessesUrl));
setAuthenticationManager(new NoOpAuthenticationManager());
setAuthenticationSuccessHandler(new TokenSimpleUrlAuthenticationSuccessHandler());
}
public final String HEADER_SECURITY_TOKEN = "Cookie";//"LdapToken";
/**
* Attempt to authenticate request - basically just pass over to another method to authenticate request headers
*/
@Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
Enumeration<String> headerNames = request.getHeaderNames();
int i = 0;
while (headerNames.hasMoreElements()){
String key = (String) headerNames.nextElement();
String value = request.getHeader(key);
System.out.println("+++ key["+i+"]" +key);
System.out.println("+++ val["+i+"]" +value);
i++;
}
String token = request.getHeader(HEADER_SECURITY_TOKEN);
logger.info("token found:"+token);
System.out.println("+++ token found:"+token);
AbstractAuthenticationToken userAuthenticationToken = authUserByToken(token);
if(userAuthenticationToken == null) throw new AuthenticationServiceException(MessageFormat.format("Error | {0}", "Bad Token"));
System.out.println("+++ userAuthenticationToken:"+userAuthenticationToken.toString());
return userAuthenticationToken;
}
/**
* authenticate the user based on token
* @return
*/
private AbstractAuthenticationToken authUserByToken(String token) {
if(token==null) {
System.out.println("+++ i shouldn't be null +++");
return null;
}
AbstractAuthenticationToken authToken = new JWTAuthenticationToken(token);
try {
return authToken;
} catch (Exception e) {
System.out.println(e);
logger.error("Authenticate user by token error: ", e);
}
return authToken;
}
@Override
public void doFilter(ServletRequest req, ServletResponse res,
FilterChain chain) throws IOException, ServletException {
System.out.println("++++++++++++++++++++++++++++++ doFilter ");
super.doFilter(req, res, chain);
}
}
And my Custom Success handler. I think that this causes the infinite loop, but I cannot figure out, why:
public class TokenSimpleUrlAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Override
protected String determineTargetUrl(HttpServletRequest request,
HttpServletResponse response) {
System.out.println("+++ yuhuuu determineTargetUrl+++");
String context = request.getContextPath();
String fullURL = request.getRequestURI();
String url = fullURL.substring(fullURL.indexOf(context)+context.length());
return url;
}
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response, Authentication authentication)
throws IOException, ServletException {
System.out.println("+++ yuhuuu onAuthenticationSuccess+++");
String url = determineTargetUrl(request,response);
request.getRequestDispatcher(url).forward(request, response);
}
}
All other classes (NoOpAuthenticationManager and RestAuthenticationEntryPoint) are exactly like in this blogpost.
Would be great I someone could give me a hint what could cause this infinite loop. As I said, it only occurs when the Request has a valid token.
Thanks and best regards Ben
回答1:
your coding approach is valid. However, I can provide you with a slightly different but working approach. Before I start to explain the solution, here is the code:
WebSecurityConfig.java
@Configuration
@EnableWebMvcSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().
antMatchers("/restapi").hasRole("USER")
.and().addFilterBefore(new SsoTokenAuthenticationFilter(authenticationManager()), BasicAuthenticationFilter.class).httpBasic()
.and().authorizeRequests().antMatchers("/**").permitAll().anyRequest().authenticated();
}
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
// The order is important! During runtime Spring Security tries to find Provider-Implementations that
// match the UsernamePasswordAuthenticationToken (which will be created later..). We must make sure
// that daoAuthenticationProvider matches first. Why? Hard to explain, I figured it out with the debugger.
auth.authenticationProvider(daoAuthenticationProvider());
auth.authenticationProvider(tokenAuthenticationProvider());
}
@Bean
public AuthenticationProvider tokenAuthenticationProvider() {
return new SsoTokenAuthenticationProvider();
}
@Bean
public AuthenticationProvider daoAuthenticationProvider() {
// DaoAuthenticationProvider requires a userDetailsService object to be attached.
// So we build one. This replaces the AuthenticationConfiguration, which is commented out below
// Build the userDetailsService
User userThatMustMatch = new User("michael", "password", AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
Collection<UserDetails> users = new ArrayList<>();
users.add(userThatMustMatch);
InMemoryUserDetailsManager userDetailsService = new InMemoryUserDetailsManager(users);
// Create the DaoAuthenticationProvider that will handle all HTTP BASIC AUTH requests
DaoAuthenticationProvider daoAuthProvider = new DaoAuthenticationProvider();
daoAuthProvider.setUserDetailsService(userDetailsService);
return daoAuthProvider;
}
SsoTokenAuthenticationFilter.java
public class SsoTokenAuthenticationFilter extends GenericFilterBean {
public final String HEADER_SECURITY_COOKIE = "LdapToken";
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest,?> ssoTokenAuthenticationDetailsSource = new SsoTokenWebAuthenticationDetailsSource();
public SsoTokenAuthenticationFilter(AuthenticationManager authenticationManager) {
this.authenticationManager = authenticationManager;
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// check if SSO token is available. If not, pass down to next filter in chain
try {
Cookie[] cookies = httpRequest.getCookies();
if (cookies == null){
chain.doFilter(request, response);
return;
}
Cookie ssoCookie = null;
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("ssoToken"))
ssoCookie = cookies[i];
}
if (ssoCookie == null){
chain.doFilter(request, response);
return;
}
// SSO token found, now authenticate and afterwards pass down to next filter in chain
authenticateWithSsoToken(httpRequest);
logger.debug("now the AuthenticationFilter passes down to next filter in chain");
chain.doFilter(request, response);
} catch (InternalAuthenticationServiceException internalAuthenticationServiceException) {
SecurityContextHolder.clearContext();
logger.error("Internal authentication service exception", internalAuthenticationServiceException);
httpResponse.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
} catch (AuthenticationException authenticationException) {
SecurityContextHolder.clearContext();
logger.debug("No or invalid SSO token");
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage());
}
}
private void authenticateWithSsoToken(HttpServletRequest request) throws IOException {
System.out.println("+++ authenticateWithSSOToken +++");
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(null, null, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
authRequest.setDetails(ssoTokenAuthenticationDetailsSource.buildDetails(request));
// Delegate authentication to SsoTokenAuthenticationProvider, he will call the SsoTokenAuthenticationProvider <-- because of the configuration in WebSecurityConfig.java
Authentication authResult = authenticationManager.authenticate(authRequest);
}}
SsoTokenAuthenticationProvider.java
public class SsoTokenAuthenticationProvider implements AuthenticationProvider {
public SsoTokenAuthenticationProvider() {
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SsoTokenWebAuthenticationDetails ssoTokenWebAuthenticationDetails = null;
WebAuthenticationDetails webWebAuthenticationDetails = (WebAuthenticationDetails)authentication.getDetails();
if (! (webWebAuthenticationDetails instanceof SsoTokenWebAuthenticationDetails)){
// ++++++++++++++++++++++++
// BASIC authentication....
// ++++++++++++++++++++++++
UsernamePasswordAuthenticationToken emptyToken = new UsernamePasswordAuthenticationToken(null, null);
emptyToken.setDetails(null);
return emptyToken; //return null works, too.
}
// ++++++++++++++++++++++++
// LDAP authentication....
// ++++++++++++++++++++++++
ssoTokenWebAuthenticationDetails = (SsoTokenWebAuthenticationDetails)webWebAuthenticationDetails;
Cookie ssoTokenCookie = ssoTokenWebAuthenticationDetails.getSsoTokenCookie();
// check if SSO cookie is available
if (ssoTokenCookie == null){
return new UsernamePasswordAuthenticationToken(null, null); //do basic auth.
}
String username = ssoTokenCookie.getValue();
// Do your SSO token authentication here
if (! username.equals("michael"))
return new UsernamePasswordAuthenticationToken(null, null); //do basic auth.
// Create new Authentication object. Name and password can be null (but you can set the values of course).
// Be careful with your role names!
// In WebSecurityConfig the role "USER" is automatically prefixed with String "ROLE_", so it is "ROLE_USER" in the end.
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(null, null, AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_USER,ROLE_RESTUSER"));
authRequest.setDetails(ssoTokenWebAuthenticationDetails);
// Don't let spring decide.. you already have made the right decisions. Tell spring you have an authenticated user.
// vielleicht ist dieses obere Kommentar auch bullshit... ich lese das morgen noch mal nach...
SecurityContextHolder.getContext().setAuthentication(authentication);
return authentication;
}
@Override
public boolean supports(Class<?> authentication) {
return authentication.equals(UsernamePasswordAuthenticationToken.class);
}
}
SsoTokenWebAuthenticationDetailsSource.java
public class SsoTokenWebAuthenticationDetailsSource extends
WebAuthenticationDetailsSource {
@Override
public WebAuthenticationDetails buildDetails(HttpServletRequest context) {
return new SsoTokenWebAuthenticationDetails(context);
}
}
SsoTokenWebAuthenticationDetails.java
public class SsoTokenWebAuthenticationDetails extends WebAuthenticationDetails {
private static final long serialVersionUID = 1234567890L;
private Cookie ssoTokenCookie;
public SsoTokenWebAuthenticationDetails(HttpServletRequest request) {
super(request);
// Fetch cookie from request
Cookie[] cookies = request.getCookies();
Cookie ssoTokenCookie = null;
for (int i = 0; i < cookies.length; i++) {
if (cookies[i].getName().equals("SSOToken"))
ssoTokenCookie= cookies[i];
}
this.setSsoTokenCookie(ssoTokenCookie);
}
public Cookie getSsoTokenCookie() {
return ssoTokenCookie;
}
public void setSsoTokenCookie(Cookie ssoTokenCookie) {
this.ssoTokenCookie = ssoTokenCookie;
}
}
I describe the solution in a view words:
- The Config class secures any
/restapicontroller with roleROLE_USER. The authentication can be done using httpBasic authentication, but before you can try basic auth. you must try to authenticate the user by a ssoTokenCookie (if available). Therefore, you set theSsoTokenAuthenticationFilteras filter before basic auth. is applied. - Inside the filter, you check if a ssoTokenCookie is available in request.
- If yes, you delegate the authentication to the standard spring
AuthenticationManager. TheAuthenticationManagerknows your ownSsoTokenAuthenticationProviderimplementation and delegates the authentication to it. Here, it is important to have the cookie information available. This can be done by use of a customizedWebAuthenticationDetails. - if no, you pass down the work to the next filter in chain. It's no surprise, the standard
BasicAuthenticationFilterwill be called. Because you told Spring to use the standarddaoAuthenticationProviderinWebSecurityConfig.java, Spring can authenticate the user when the proper credentials will be entered in the basic auth. dialog
- If yes, you delegate the authentication to the standard spring
来源:https://stackoverflow.com/questions/28368254/infinite-loop-in-custom-spring-security-application