Spring security pre-authentication scenario assumes that a valid authenticated user is available via either Single Sign On (SSO) applications like Siteminder, Tivoli, etc or a X509 certification based authentication. The Spring security in this scenario will only be used for authorization.
The example shown below retrieves the user name via the HTTP headers.
Step 1: The dependency jars that are required.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-core</artifactId> <version>3.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-web</artifactId> <version>3.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-acl</artifactId> <version>3.1.0.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> <version>3.1.0.RELEASE</version> </dependency> |
Step 2: Define the Spring security filter via the web.xml file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
.... <!-- The definition of the Root Spring Container shared by all Servlets and Filters --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/META-INF/spring/applicationContext.xml</param-value> </context-param> <!-- Spring Security --> <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>/myapp/*</url-pattern> </filter-mapping> .... |
Step 3: The servlet filter configured above will make use of a spring context file like ssoContext.xml to define the authorization sequences. The ssoContext file can be imported via the applicationContext.xml file bootstrapped via the web.xml file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
<?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:p="http://www.springframework.org/schema/p" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:cache="http://www.springframework.org/schema/cache" xmlns:context="http://www.springframework.org/schema/context" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:jee="http://www.springframework.org/schema/jee" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:util="http://www.springframework.org/schema/util" xmlns:batch="http://www.springframework.org/schema/batch" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.1.xsd http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache-3.1.xsd"> <!-- Root Context: defines shared resources visible to all other web components --> <context:annotation-config /> <import resource="myServerContext.xml" /> <import resource="security/ssoContext.xml" /> </beans> |
Step 4: The ssoContext.xml is defined below showing how the user can be retrieved from HTTP header SM_USER for site minder and passed to your own implementation to retrieve the roles (aka authorities). All the classes configured below are Spring classes except for the UserDetailsServiceImpl, which is used to retrieve the authorities.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 |
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <context:component-scan base-package="com.myapp.dao.security" /> <context:component-scan base-package="com.myapp.model.security" /> <beans:bean id="springVoter" class="org.springframework.security.access.vote.RoleVoter" /> <beans:bean id="springAccessDecisionManager" class="org.springframework.security.access.vote.AffirmativeBased"> <beans:property name="allowIfAllAbstainDecisions" value="false" /> <beans:property name="decisionVoters"> <beans:list> <beans:ref local="springVoter" /> </beans:list> </beans:property> </beans:bean> <http auto-config="false" entry-point-ref="preAuthenticatedProcessingFilterEntryPoint"> <security:custom-filter position="PRE_AUTH_FILTER" ref="siteminderFilter" /> <intercept-url pattern="/**/details.csv*" access="ROLE_viewer, ROLE_standard, ROLE_senior" /> <logout logout-url="/j_spring_security_logout" logout-success-url="https://smlogin-dev.myapp.net/siteminderagent/ssologout/Logout.html" invalidate-session="true" /> </http> <beans:bean id="preAuthenticatedProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint"/> <beans:bean id="siteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter"> <beans:property name="principalRequestHeader" value="SM_USER" /> <beans:property name="authenticationManager" ref="authenticationManager" /> <beans:property name="exceptionIfHeaderMissing" value="false" /> </beans:bean> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider ref="preauthAuthProvider" /> </security:authentication-manager> <beans:bean id="preauthAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> <beans:property name="preAuthenticatedUserDetailsService"> <beans:bean id="userDetailsServiceWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <beans:property name="userDetailsService" ref="myUserDetailsService" /> </beans:bean> </beans:property> </beans:bean> <beans:bean id="myUserDetailsService" class="com.myapp.UserDetailsServiceImpl"> <beans:property name="appCd" value="appName" /> </beans:bean> </beans:beans> |
Step 5: Define the class UserDetailsServiceImpl class that needs to implement the Spring interface UserDetailsService and the required method “public UserDetails loadUserByUsername(String username)”. The returned model object “UserDetails” is a Spring class as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
package com.myapp.security; import java.util.ArrayList; import java.util.Collection; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Required; import org.springframework.dao.DataAccessException; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.GrantedAuthorityImpl; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; public class UserDetailsServiceImpl implements UserDetailsService { final static Logger LOG = LoggerFactory.getLogger(UserDetailsServiceImpl.class); protected String appCd; public UserDetailsServiceImpl() {} @Required public void setAppCd(String appCd) { if ( appCd != null && appCd.length() > 0 ) this.appCd = appCd; } // override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException, DataAccessException { String role = "ROLE_viewer" ; //hard coded, in real life retrieved via database or LDAP GrantedAuthorityImpl au_impl = new GrantedAuthorityImpl(role); Collection<GrantedAuthority> authorities = new ArrayList<GrantedAuthority>(); authorities.add(au_impl); User usr = new User(username, "", true, true, true, true, authorities); return usr; } } |
Start annotating your Java methods and the URLs that intercept the calls to verify the roles (or authorities) returned for a given user against the roles allowed for a method or URL.
Let’s will see how we can protect the controller and the service class methods by defining what roles are allowed.
Firstly, you can protect your controller as shown below.
Step 6: Define the URLs to be protected in the ssoContext.xml file something like
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
<http auto-config="false" access-decision-manager-ref="springAccessDecisionManager" once-per-request="true" create-session="ifRequired" entry-point-ref="MyAppAuthenticationEntryPoint"> <session-management invalid-session-url="/j_spring_security_logout" /> <!-- TODO: Would be cleaner if we didn't have to enumerate every role that can access some URL in the system. Consider hierarchical roles --> <intercept-url pattern="/**/*.css*" filters="none" /> <intercept-url pattern="/**/*.js*" filters="none" /> <intercept-url pattern="/**/*.png*" filters="none" /> <intercept-url pattern="/**/codemapping.rpc" access="ROLE_admin,ROLE_viewer" /> <intercept-url pattern="/**/generalLedgerService.rpc" access="ROLE_admin" /> <intercept-url pattern="/**/MyAppAdjustment.html" access="ROLE_admin,ROLE_viewer" /> <intercept-url pattern="/**/CodeMapping.html" access="ROLE_admin,ROLE_viewer" /> <intercept-url pattern="/**/myapp_test.html" access="ROLE_admin" /> <custom-filter ref="siteminderFilter" position="PRE_AUTH_FILTER" /> <access-denied-handler ref="accessDeniedHandler"/> .... </http> |
In the Spring MVC controller, you can use the annotation as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
@RolesAllowed( { "ROLE_viewer", "ROLE.standard", "ROLE_senior" }) @RequestMapping(value = "/portfolio/{portfoliocd}/details.csv", method = RequestMethod.GET, produces = "text/csv") @ResponseBody public void getCashForecastCSV( @PathVariable(value = "portfoliocd") String portfolioCode, @RequestParam(value = "valuationDate", required = true) @DateTimeFormat(pattern = "yyyyMMdd") Date valuationDate, HttpServletResponse response) throws Exception { //.............................. } |
The service class methods can be protected by declaring the following in your spring context file where the methods reside.
1 2 3 |
<!-- comment this line locally to bypass seurity access control in development. But don't check this in commented as security will be turned off --> <security:global-method-security secured-annotations="enabled" pre-post-annotations="enabled" jsr250-annotations="enabled"/> |
Once declared, you can protect your service class methods as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 |
@RolesAllowed( { "ROLE_viewer", "ROLE_standard", "ROLE_senior" }) @Override public ReconciliationResult getReconciliations(ReconciliationCriteria criteria) { //........................ } |
Q. How will you access Spring security context details within a method that is annotated with @RolesAllowed?
A. The answer is to use the SecurityContextHolder class’s static method. It is a ThreadLocal class. Each thread has its own value of ThreadLocal pointing to the same instance of SecurityContextHolder class. Here is the sample code snippet. You can get the principal and the authorities as shown below.
1 2 3 4 5 |
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); Collection authorities = authentication.getAuthorities(); Object principal = authentication.getPrincipal(); |
Q. How do you map multiple authentication managers?
A. You can define multiple authentiaction managers based on URL patterns as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <http auto-config="false" pattern="/app1/reporting/**" authentication-manager-ref="app1ReportingAuthenticationManager" entry-point-ref="app1ReportingProcessingFilterEntryPoint"> <security:custom-filter position="PRE_AUTH_FILTER" ref="app1ReportingSiteminderFilter" /> <logout logout-url="/j_spring_security_logout" logout-success-url="" invalidate-session="true" /> </http> <http auto-config="false" pattern="/app1/calculating/**" authentication-manager-ref="app1CalculatingAuthenticationManager" entry-point-ref="app1Calculating"> <security:custom-filter position="PRE_AUTH_FILTER" ref="app1CalculatingSiteminderFilter" /> <logout logout-url="/j_spring_security_logout" logout-success-url="" invalidate-session="true" /> </http> <http auto-config="false" pattern="/app2/validating/**" entry-point-ref="app2ValidatingProcessingFilterEntryPoint" entry-point-ref="app2Validating"> <security:custom-filter position="PRE_AUTH_FILTER" ref="app2ValidatingSiteminderFilter" /> <intercept-url pattern="/app2/**/details.csv*" access="ROLE_viewer, ROLE_standard, ROLE_senior" /> <logout logout-url="/j_spring_security_logout" logout-success-url="" invalidate-session="true" /> </http> </beans:beans> |
Here is a sample config for one of the http mappings:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
<?xml version="1.0" encoding="UTF-8"?> <beans:beans xmlns="http://www.springframework.org/schema/security" xmlns:beans="http://www.springframework.org/schema/beans" xmlns:security="http://www.springframework.org/schema/security" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <beans:bean id="app1ReportingProcessingFilterEntryPoint" class="org.springframework.security.web.authentication.Http403ForbiddenEntryPoint" /> <beans:bean id="app1ReportingSiteminderFilter" class="org.springframework.security.web.authentication.preauth.RequestHeaderAuthenticationFilter"> <beans:property name="principalRequestHeader" value="SM_USER" /> <beans:property name="authenticationManager" ref="app1ReportingAuthenticationManager" /> <beans:property name="exceptionIfHeaderMissing" value="false" /> </beans:bean> <security:authentication-manager id="app1ReportingAuthenticationManager"> <security:authentication-provider ref="app1ReportingAuthProvider" /> </security:authentication-manager> <beans:bean id="app1ReportingAuthProvider" class="org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationProvider"> <beans:property name="preAuthenticatedUserDetailsService"> <beans:bean id="myAppUserDetailsServiceWrapper" class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper"> <beans:property name="userDetailsService" ref="myUserDetailsService" /> </beans:bean> </beans:property> </beans:bean> <beans:bean id="myUserDetailsService" class="com.myapp.MyUserDetailsServiceImpl"> <beans:property name="domain" value="FUNDS" /> </beans:bean> </beans:beans> |
Q. The roles are stored in the session, how will you make sure that the roles are read every time from the database?
A. It is generally a good practice to leave the roles in the session, and the session gets invalidated on logout. There are times you want the roles to be read from the database and not from the session. This can be accomplished by
Step 7: Writing a response filter to remove the roles from the Http Session. Here is the code snippet.
1 2 3 4 |
HttpSession session = request.getSession(); session.removeAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY); |
The SecurityHolder.getContext( ) has a clearContents method, but it will only remove it from the current thread but not the session.