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.