How do I implement a custom FilterSecurityInterceptor using grails 1.3.2 and the plugin spring-security-core 1?
I'm writing a grails 1.3.2 application and implementing security with spring-security-core 1.0. For reasons outside the scope of this question, I'm implementing a custom FilterSecurityInterceptor in addition to the out of the box interceptors. I've started with a blog entry on the topic and attempted to adjust it for Spring Security 3 without much success.
Loosely following the blog (since it is based on an older version of Spring Security), I've created the following classes:
- A org.springframework.security.authentication.AbstractAuthenticationToken subclass to hold my credentials.
- A org.springframework.security.authentication.AuthenticationProvider subclass to implement the authenticate and supports methods for populating an Authentication instance with data from my UserDetailsService.
- A org.springframework.security.web.access.intercept.FilterSecurityInterceptor subclass to implement doFilter and afterPropertiesSet methods.
- Some configuration of beans and the spring-security-core plugin to recognize my AuthenticationProvider and insert my filter into the filter chain.
My AbstractAuthenticationToken is pretty simple:
class InterchangeAuthenticationToken extends AbstractAuthenticationToken {
String credentials
Integer name
Integer principal
String getCredentials() { //necessary or I get compilation error
return credentials
}
Integer getPrincipal() { //necessary or I get compilation error
return principal
}
}
My AuthenticationProvider is pretty simple:
class InterchangeAuthenticationProvider implements org.springframework.security.authentication.AuthenticationProvider {
Authentication authenticate(Authentication customAuth) {
def authUser = AuthUser.get(customAuth.principal)
if (authUser) {
customAuth.setAuthorities(authUser.getAuthorities())
customAuth.setAuthenticated(true)
return customAuth
} else {
return null
}
}
boolean supports(Class authentication) {
return InterchangeAuthenticationToken.class.isAssignableFrom(authentication)
}
}
I've implemented a trivial FilterSecurityInterceptor. Eventually this will do something interesting:
class InterchangeFilterSecurityInterceptor extends FilterSecurityInterceptor implements InitializingBean {
def authenticationManager
def interchangeAuthenticationProvider
def securityMetadataSource
void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) {
if (SecurityContextHolder.getContext().getAuthentication() == null) {
def myAuth = new InterchangeAuthenticationToken()
myAuth.setName(1680892)
myAuth.setCredentials('SDYLWUYa:nobody::27858cff')
myAuth.setPrincipal(1680892)
myAuth = authenticationManager.authenticate(myAuth);
if (myAuth) {
println "Successfully Authenticated ${userId} in object ${myAuth}"
// Store to SecurityContextHolder
SecurityContextHolder.getContext().setAuthentication(myAuth);
}
}
chain.doFilter(request, response)
}
void afterPropertiesSet() {
def providers = authenticationManager.providers
providers.add(interchangeAuthenticationProvider)
authenticationManager.providers = providers
}
}
Finally I configure some beans:
beans = {
interchangeAuthenticationProvider(com.bc.commerce.core.InterchangeAuthenticationProvider) {
}
interchangeFilterSecurityInterceptor(com.bc.commerce.core.InterchangeFilterSecurityInterceptor) {
authenticati开发者_Python百科onManager = ref('authenticationManager')
interchangeAuthenticationProvider = ref('interchangeAuthenticationProvider')
securityMetadataSource = ref('objectDefinitionSource')
}
}
And do some configuration of the plugin:
grails.plugins.springsecurity.dao.hideUserNotFoundExceptions = true //not setting this causes exception
grails.plugins.springsecurity.providerNames = [
'interchangeAuthenticationProvider',
'daoAuthenticationProvider',
'anonymousAuthenticationProvider',
'rememberMeAuthenticationProvider'
]
And set the filter order in Bootstrap.groovy:
def init = {servletContext ->
//insert our custom filter just after the filter security interceptor
SpringSecurityUtils.clientRegisterFilter('interchangeFilterSecurityInterceptor', SecurityFilterPosition.SECURITY_CONTEXT_FILTER.order + 10)
<snip />
}
When I hit a URL, I get the following exception which stumps me:
2010-07-30 15:07:16,763 [http-8080-1] ERROR [/community-services].[default] - Servlet.service() for servlet default threw exception
java.lang.NullPointerException
at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:171)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.invoke(FilterSecurityInterceptor.java:106)
at org.springframework.security.web.access.intercept.FilterSecurityInterceptor.doFilter(FilterSecurityInterceptor.java:83)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.springframework.security.web.authentication.rememberme.RememberMeAuthenticationFilter.doFilter(RememberMeAuthenticationFilter.java:112)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter.doFilter(SecurityContextHolderAwareRequestFilter.java:54)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:187)
at org.codehaus.groovy.grails.plugins.springsecurity.RequestHolderAuthenticationFilter.doFilter(RequestHolderAuthenticationFilter.java:40)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.codehaus.groovy.grails.plugins.springsecurity.MutableLogoutFilter.doFilter(MutableLogoutFilter.java:79)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.springframework.security.web.context.SecurityContextPersistenceFilter.doFilter(SecurityContextPersistenceFilter.java:79)
at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:355)
at org.springframework.security.web.FilterChainProxy.doFilter(FilterChainProxy.java:149)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.servlet.filter.GrailsReloadServletFilter.doFilterInternal(GrailsReloadServletFilter.java:104)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.servlet.mvc.GrailsWebRequestFilter.doFilterInternal(GrailsWebRequestFilter.java:67)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.codehaus.groovy.grails.web.filters.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:66)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.springframework.web.filter.CharacterEncodingFilter.doFilterInternal(CharacterEncodingFilter.java:88)
at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)
at org.springframework.web.filter.DelegatingFilterProxy.invokeDelegate(DelegatingFilterProxy.java:237)
at org.springframework.web.filter.DelegatingFilterProxy.doFilter(DelegatingFilterProxy.java:167)
at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:235)
at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:206)
at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:233)
at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:191)
at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:128)
at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:102)
at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:109)
at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:293)
at org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:849)
at org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:583)
at org.apache.tomcat.util.net.JIoEndpoint$Worker.run(JIoEndpoint.java:454)
at java.lang.Thread.run(Thread.java:637)
So where am I messing up, or did I make this too complex and I missed something simple?
In the end, I implemented a custom filter, but not a FilterSecurityInterceptor. I inserted my filter after the OOTB rememberMe filter. Furthermore I found my authentication implementation to be somewhat resource-intensive and slow, so I made it set the rememberMe cookie on successful authentication. On the whole, this was a painful experience so I'll make an attempt to document it here.
My filter implementation was as follows:
import javax.servlet.FilterChain
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import javax.servlet.ServletException
import javax.servlet.ServletRequest
import javax.servlet.ServletResponse
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.ApplicationEventPublisherAware
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.core.Authentication
import org.springframework.security.core.AuthenticationException
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.web.filter.GenericFilterBean
class CustomRememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
def authenticationManager
def eventPublisher
def customService
def rememberMeServices
def springSecurityService
//make certain that we've specified our beans
void afterPropertiesSet() {
assert authenticationManager != null, 'authenticationManager must be specified'
assert customService != null, 'customService must be specified'
assert rememberMeServices != null, 'rememberMeServices must be specified'
}
void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req
HttpServletResponse response = (HttpServletResponse) res
if (SecurityContextHolder.getContext().getAuthentication() == null) {
Authentication auth
try {
auth = customService.getUsernamePasswordAuthenticationToken(request)
if (auth != null) {
springSecurityService.reauthenticate(auth.getPrincipal(), auth.getCredentials())
logger.debug("SecurityContextHolder populated with auth: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'")
onSuccessfulAuthentication(request, response, SecurityContextHolder.getContext().getAuthentication())
} else {
logger.debug('customService did not return an authentication from the request')
}
} catch (AuthenticationException authenticationException) {
logger.warn("SecurityContextHolder not populated with auth, as "
+ "springSecurityService rejected Authentication returned by customService: '"
+ auth + "'", authenticationException)
onUnsuccessfulAuthentication(request, response, auth)
} catch(e) {
logger.warn("Unsuccessful authentication in customRememberMeAuthenticationFilter", e)
onUnsuccessfulAuthentication(request, response, auth)
}
} else {
logger.debug("SecurityContextHolder not populated with auth, as it already contained: '"
+ SecurityContextHolder.getContext().getAuthentication() + "'")
}
chain.doFilter(request, response)
}
protected void onSuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, Authentication authResult) {
//sets the rememberMe cookie, but cannot call "loginSuccess" because that filters out requests
//that don't set the rememberMe cookie, like this one
rememberMeServices.onLoginSuccess(request, response, authResult)
}
protected void onUnsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException failed) {
//clear the rememberMe cookie
rememberMeServices.loginFail(request, response)
}
public void setApplicationEventPublisher(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher
}
}
Hopefully others find this helpful when implementing their custom authentication solutions. We found this worked for us when integrating our application with our legacy system.
Given where it's failing (fairly unrelated) I'd guess that it's the nested properties. Try
grails.plugins.springsecurity.dao.hideUserNotFoundExceptions = true
grails.plugins.springsecurity.providerNames = [
'interchangeAuthenticationProvider',
'daoAuthenticationProvider',
'anonymousAuthenticationProvider',
'rememberMeAuthenticationProvider'
]
My guess is that it's resetting the rest of the config (a Grails/ConfigSlurper quirk) and that this will merge in the properties instead. You shouldn't need to set "active = true" but I'm guessing you needed to add that since it's also getting reset.
btw - you can remove the getters from InterchangeAuthenticationToken since public fields generate getters automatically.
This appears to be a bug in the spring-security-core plugin because the securityMetadataSource
isn't injected into the default spring security FilterSecurityInterceptor
. Perhaps the plugin gets confused and injects the securityMetadataSource
into your custom FilterSecurityInterceptor
and neglects the other (default one)? Burt might be willing to look deeper info that.
Perhaps you could try replacing the default FilterSecurityInterceptor
with your custom one using the grails.plugins.springsecurity.filterNames
property...
精彩评论