Spring Security with SSO Headers - integrating with OAM WebGate
Table of Contents
转载自: http://blog.sbeynon.net/2011/12/spring-security-with-sso-headers.html
How to integrate java web applications with SSO? Usually a simple J2EE filter looking for the correct header name can suffice, however, if you want to take advantage of Spring Security you need to set it up with a "PreAuthentication" filter. This states that spring is not performing authentication, it's already done.
This solution may be overkill, but it provides more control/log info, and the ability to test outside of the SSO environment without implementing alternate authentication providers (although it would also be possible to have a form/database authentication fallback as well).
My environment: J2ee (any app server) fronted by a Oracle Access WebGate protected web server.
Spring Security 2.5+ (tested with 3.0.5)
What I need: Convert Request Headers OAM_REMOTE_USER and ROLES to map to an authenticated principal.
Making it usable: Allow a fallback security configuration for local testing.
Step 1. PreAuthentication Config
There are two custom beans in this file "HeaderAuthenticationFilter" and "HeaderAuthenticationDetails" and three configuration properties:
security.principal.header.name=OAM_REMOTE_USER security.roles.header.name=ROLES security.test.principal=no_header_in_test_mode applicationContext-security.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:context="http://www.springframework.org/schema/context" xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.5.xsd"
default-lazy-init="true">
<bean
class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<!-- this bean name must match the filter name defined in security.xml below -->
<bean id="ssoHeaderFilter"
class="com.sharnibey.sample.security.HeaderAuthenticationFilter">
<property name="principalRequestHeader" value="${security.principal.header.name}" />
<!-- fall back to other authentication providers is OAM SSO is not there -->
<property name="exceptionIfHeaderMissing" value="false" />
<!-- hard code a testUserId for local tests -->
<property name="testUserId" value="${security.test.principal}" />
<property name="authenticationManager" ref="authenticationManager" />
<property name="authenticationDetailsSource">
<bean class="com.sharnibey.sample.security.HeaderAuthenticationDetails">
<!-- look for the request header set by the webgate and map to local
roles -->
<property name="roleHeaderName" value="${security.roles.header.name}" />
<property name="userRoles2GrantedAuthoritiesMapper">
<bean
class="org.springframework.security.core.authority.mapping.SimpleAttributes2GrantedAuthoritiesMapper">
<property name="convertAttributeToUpperCase" value="true" />
</bean>
</property>
<!-- setup a testing role if not deployed with a webgate - this only
applies if ENV_NAME != uat/prod -->
<property name="testingRoles">
<set>
<value>USER</value>
</set>
</property>
<!-- all available roles for this application -->
<property name="allRoles">
<set>
<value>USER</value>
<value>ADMIN</value>
</set>
</property>
</bean>
</property>
</bean>
<bean id="preauthAuthProvider"
class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider">
<property name="preAuthenticatedUserDetailsService" ref="preAuthenticatedUserDetailsService" />
</bean>
<!-- magically map the user header to a valid user object -->
<bean id="preAuthenticatedUserDetailsService"
class="org.springframework.security.web.authentication.preauth.PreAuthenticatedGrantedAuthoritiesUserDetailsService" />
<bean id="securityContextHolderAwareRequestFilter"
class="org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter" />
</beans>
PreAuthentication Code
public class HeaderAuthenticationFilter
extends AbstractPreAuthenticatedProcessingFilter {
protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationFilter.class);
private String principalRequestHeader = "OAM_REMOTE_USER";
/**
* Configure a value in the applicationContext-security for local tests.
*/
private String testUserId = null;
/**
* Configure whether a missing SSO header is an exception.
*/
private boolean exceptionIfHeaderMissing = false;
/**
* Read and return header named by <tt>principalRequestHeader</tt> from Request
*
* @throws PreAuthenticatedCredentialsNotFoundException
* if the header is missing and
* <tt>exceptionIfHeaderMissing</tt> is set to <tt>true</tt>.
*/
protected Object getPreAuthenticatedPrincipal(HttpServletRequest request) {
String principal = request.getHeader(principalRequestHeader);
if (principal == null) {
if (exceptionIfHeaderMissing) {
throw new PreAuthenticatedCredentialsNotFoundException(principalRequestHeader
+ " header not found in request.");
} if (StringUtils.isNotBlank(testUserId)) {
log.warn("spring configuration has a test user id " + testUserId);
principal = testUserId;
} else if (request.getSession().getAttribute("session_user") != null) {
// A bit of a hack for testers - allow the principal to be
// obtained by session. Must be set by a page with no security filters enabled.
// should remove for production.
principal = (String) request.getSession().getAttribute("session_user");
}
}
// also set it into the session, sometimes that's easier for jsp/faces
// to get at..
request.getSession().setAttribute("session_user", principal);
return principal;
}
/**
* Credentials aren't applicable here for OAM WebGate SSO.
*/
protected Object getPreAuthenticatedCredentials(HttpServletRequest request) {
return "password_not_applicable";
}
public void setPrincipalRequestHeader(String principalRequestHeader) {
Assert.hasText(principalRequestHeader, "principalRequestHeader must not be empty or null");
this.principalRequestHeader = principalRequestHeader;
}
public void setTestUserId(String testId) {
if (StringUtils.isNotBlank(testId)) {
this.testUserId = testId;
}
}
/**
* Exception if the principal header is missing. Default <tt>false</tt>
* @param exceptionIfHeaderMissing
*/
public void setExceptionIfHeaderMissing(boolean exceptionIfHeaderMissing) {
this.exceptionIfHeaderMissing = exceptionIfHeaderMissing;
}
public void setAuthenticationDetailsSource(AuthenticationDetailsSource source) {
log.info("testing authenticationDetailsSource set " + source);
super.setAuthenticationDetailsSource(source);
}
}
public class HeaderAuthenticationDetails extends AuthenticationDetailsSourceImpl {
protected final Logger log = LoggerFactory.getLogger(HeaderAuthenticationDetails.class);
/**
* Can be setup in applicationContext-security if the ROLES header value is
* not found.
*/
private Set<string> testingRoles = new HashSet<string>();
/**
* Security principal will only contain roles from "allRoles" - letting us
* cut down the irrelevant values setup by the webgate SSO header.
*/
protected Set<string> allRoles = new HashSet<string>();
/**
* setup in applicationContext-security
*/
private String roleHeaderName = "ROLES";
protected Attributes2GrantedAuthoritiesMapper grantedAuthoritiesMapper
= new SimpleAttributes2GrantedAuthoritiesMapper();
public HeaderAuthenticationDetails() {
super.setClazz(PreAuthenticatedGrantedAuthoritiesWebAuthenticationDetails.class);
}
/**
* Build the authentication details object. If the specified authentication
* details class implements {@link MutableGrantedAuthoritiesContainer}, a
* list of pre-authenticated Granted Authorities will be set based on the
* roles for the current user.
*/
public Object buildDetails(Object context) {
Object result = super.buildDetails(context);
List<grantedauthority> userGas = new ArrayList<grantedauthority>();
if (result instanceof MutableGrantedAuthoritiesContainer) {
Collection<string> userRoles = getUserRoles(context, allRoles);
userGas = grantedAuthoritiesMapper.getGrantedAuthorities(userRoles);
((MutableGrantedAuthoritiesContainer) result).setGrantedAuthorities(userGas);
}
return result;
}
/**
* Allows the roles of the current user to be determined from the context
* object
*
* @param context
* the context object (HttpRequest, PortletRequest etc)
* @param mappableRoles
* the possible roles determined by the
* MappableAttributesRetriever
* @return Collection<string> subset of mappable roles current user has.
*/
protected Collection<string> getUserRoles(Object context, Set<string> mappableRoles) {
ArrayList<string> requestRoles = new ArrayList<string>();
if (((HttpServletRequest) context).getHeader(roleHeaderName) != null) {
String[] roles = ((HttpServletRequest) context).getHeader(roleHeaderName).split(",");
for (int i = 0; i < roles.length; i++) {
if (mappableRoles.contains(roles[i])) {
requestRoles.add(roles[i]);
}
}
} else if ( testingRoles != null) {
log.warn("Failed to retrieve Roles from Header, for debug purposes set to testingRole");
requestRoles.addAll(testingRoles);
} else {
log.warn("Failed to retrieve Roles from Header, setup as 'user' role.");
requestRoles.add("USER");
}
// add them to the session for convenience
((HttpServletRequest) context).getSession().setAttribute("ROLES", requestRoles);
return requestRoles;
}
/**
* @param mapper
* The Attributes2GrantedAuthoritiesMapper to use
*/
public void setUserRoles2GrantedAuthoritiesMapper(Attributes2GrantedAuthoritiesMapper mapper) {
grantedAuthoritiesMapper = mapper;
}
/**
* All available roles for this application
*
* @param allRoles
*/
public void setAllRoles(Set<string> allRoles) {
this.allRoles = allRoles;
}
/**
* @param roleHeaderName
*/
public void setRoleHeaderName(String roleHeaderName) {
this.roleHeaderName = roleHeaderName;
}
/**
* @param testingRole
*/
public void setTestingRoles(Set<string> testingRole) {
this.testingRoles = testingRole;
}
}
web.xml updates
These snippets will look familiar if you've ever used spring security; define the filter, map to all resources. Spring contextConfiguration locations should have your resource properties loaded first, then security config, and everything else.
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath:/ctx/applicationContext-resources.xml
classpath:/ctx/applicationContext-security.xml
/WEB-INF/security.xml
/WEB-INF/applicationContext*.xml
</param-value>
</context-param>
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
Final Step - Setup Spring Security
security.xml is application specific, but my sample application is based on appfuse - JSF version - so it should cover most example uses.
The intercept-url element defines the roles or authentication states that are required to access a URL path. Roles should be comma-delimited.
To restrict pages by user type instead of user role the following values can be used: IS_AUTHENTICATED_ANONYMOUSLY - Allow access to any user. IS_AUTHENTICATED_REMEMBERED - Allow access to logged-in users or users with a "remember me" cookie. IS_AUTHENTICATED_FULLY - Allow access to logged-in users.
To remove all Spring Security processing from a page use the filters="none" attribute.
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.0.xsd">
<http auto-config="true" lowercase-comparisons="false">
<intercept-url pattern="/images/**" filters="none" />
<intercept-url pattern="/styles/**" filters="none" />
<intercept-url pattern="/scripts/**" filters="none" />
<intercept-url pattern="/javax.faces.resource/**"
filters="none" />
<!-- direct xhtml access disallowed -->
<intercept-url pattern="/**/*.xhtml" access="ROLE_NOBODY" />
<!-- local authentication is unused, but this is how it's configured -->
<intercept-url pattern="/j_security*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/a4j.res/**"
access="ROLE_ANONYMOUS,ROLE_ADMIN,ROLE_USER" />
<intercept-url pattern="/admin/**" access="ROLE_ADMIN" />
<intercept-url pattern="/user/**" access="ROLE_ADMIN,ROLE_USER" />
<!-- show request headers and session variables for any user -->
<intercept-url pattern="/env.jsp" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<!-- matches the bean name for HeaderAuthenticationFilter class above -->
<custom-filter position="PRE_AUTH_FILTER" ref="ssoHeaderFilter" />
<form-login login-page="/login" authentication-failure-url="/login?error=true"
login-processing-url="/j_security_check" always-use-default-target="true"
default-target-url="/" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider ref="preauthAuthProvider" />
<!-- this is an example of alternate user authentication providers, although
we only have the PRE_AUTH_FILTER defined above, so it isn't used. -->
<authentication-provider>
<user-service>
<user authorities="ROLE_USER" name="guest" password="guest" />
</user-service>
</authentication-provider>
</authentication-manager>
</beans:beans>
If you read this far, and you want a Ready-To-Go example of how all this fits together, leave a comment and I will upload a full-source war to a temporary share site. Please don't put your email in the comments.