I have secured my server using Spring security and tried testing it.
My test included logging-in a test user (which already exists in the DB), and then try to access a secured URL which requires the user to be authenticated and also to have a specific role.
Here is my code,
I created a security-context:
<!-- URL's that start with the "app/secured" prefix requires authentication -->
<http auto-config="false" use-expressions="true">
<form-login login-processing-url="/app/login"
authentication-success-handler-ref="ajaxAuthenticationSuccessHandler"
authentication-failure-handler-ref="ajaxAuthenticationFailureHandler" />
<intercept-url pattern="/**" access="permitAll" />
<intercept-url pattern="**/app/secured/**" access="isAuthenticated()" />
</http>
<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="myUserDetailsService">
<password-encoder ref="passwordEncoder">
<salt-source user-property="creationTime" />
</password-encoder>
</authentication-provider>
</authentication-manager>
<beans:bean id="passwordEncoder"
class="me.co.server.bl.security.ExtendedShaPasswordEncoder" />
<beans:bean id="ajaxAuthenticationSuccessHandler"
class="me.co.server.web.resource.register.login.AjaxAuthenticationSuccessHandler" />
<beans:bean id="ajaxAuthenticationFailureHandler"
class="me.co.server.web.resource.register.login.AjaxAuthenticationFailureHandler" />
<beans:bean id="myUserDetailsService"
class="me.co.server.bl.security.MyUserDetailsService" />
I added security-context and the security filters to the top of my web.xml:
<!-- 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>/*</url-pattern>
</filter-mapping>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/applicationContext.xml
/WEB-INF/spring/security-context.xml
</param-value>
</context-param>
Here is my web-layer jersey implementation:
@Path("/secured/{resourceName}")
@Component
public class SecuredResourceProvider extends ServiceResourceProvider {
/*--- Members ---*/
private ILogger logger = LogManager.getLogger(SecuredResourceProvider.class);
@Inject
protected SecuredResourceFactory securedResourceFactory;
/*--- Constructors ---*/
protected SecuredResourceProvider() {
super("Secured");
}
/*--- Public Methods ---*/
@GET
@Produces("application/json")
@Path("/{resourceId}")
public String getSecuredResource(@PathParam("resourceId") String resourceId, @PathParam("resourceName") String resourceName)
throws UnhandledResourceException, UnauthorizedAccessException, ServerInternalErrorException, JsonException, ResourceArgumentException {
// NOT IMPLEMENTED AT THE MOMENT //
return null;
}
@PUT
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
@Path("/{code}")
public String putSecuredResource(String resourceData, @PathParam("code") String ownerResourceId, @PathParam("resourceName") String resourceName)
throws UnhandledResourceException, UnauthorizedAccessException, ServerInternalErrorException, JsonException, ResourceArgumentException {
if (SecuredResources.isSecuredResource(resourceName)) {
IResource<String> resource = securedResourceFactory.getResourceInstance(resourceName);
String resourceString = resource.put(ownerResourceId, resourceData);
return createReturnResourceString(resourceString);
} else {
throw new UnhandledResourceException("Invoking a secured method for an unsecured resource " + resourceName);
}
}
@POST
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.TEXT_PLAIN)
@Path("/{resourceId}")
public String postSecuredResource(String resourceData, @PathParam("resourceName") String resourceName, @PathParam("resourceId") String resourceId)
throws UnhandledResourceException, UnauthorizedAccessException, ServerInternalErrorException, JsonException, ResourceArgumentException {
if (SecuredResources.isSecuredResource(resourceName)) {
IResource<String> resource = securedResourceFactory.getResourceInstance(resourceName);
String resourceString = resource.post(resourceId, resourceData);
return createReturnResourceString(resourceString);
} else {
throw new UnhandledResourceException("Invoking a secured method for an unsecured resource " + resourceName);
}
}
@DELETE
@Consumes(MediaType.TEXT_PLAIN)
@Produces(MediaType.TEXT_PLAIN)
@Path("/{code}")
public String deleteSecuredResource(String resourceId, @PathParam("code") String ownerResourceId, @PathParam("resourceName") String resourceName)
throws UnhandledResourceException, UnauthorizedAccessException, ServerInternalErrorException, JsonException, ResourceArgumentException {
if (SecuredResources.isSecuredResource(resourceName)) {
IResource<String> resource = securedResourceFactory.getResourceInstance(resourceName);
String resourceString = resource.delete(ownerResourceId, resourceId);
return createReturnResourceString(resourceString);
} else {
throw new UnhandledResourceException("Invoking a secured method for an unsecured resource " + resourceName);
}
}
}
Nothing special here besides that this spring bean is supposed to be secured (requires authenticated user) according to my url’s interception definitions.
The web layer delegates the request to the business logic layer, which is secured using method-security annotations:
@Override
@Secured("ROLE_BRAND_MANAGER")
public String post(String gymCode, String trainingSessionJson) throws UnhandledResourceException, ServerInternalErrorException,
ResourceArgumentException, JsonException {
...
}
For this to work, I added the global-method-security declaration to my application-context:
<security:global-method-security secured-annotations="enabled" proxy-target-class="true"/>
The issued user is a brand-manager (as far as authorities wise) but at the moment the suthorities are not persisted but returned hard-coded from the entity:
@Entity
@Table(name = "brand_managers")
public class BrandManager implements Serializable, UserDetails {
/** Serial version unique id */
private static final long serialVersionUID = -7992146584570782015L;
public static final String ROLE = "ROLE_BRAND_MANAGER";
/*--- Members ---*/
/** The unique, internal ID of the entity. */
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private long id;
/**
* The creation time of this user
*/
@Column(name = "creation_time")
protected long creationTime;
/**
* The hashed password
*/
@Column(name = "password")
protected String password;
@Column(name = "first_name")
protected String firstName;
@Column(name = "last_name")
protected String lastName;
@Column(name = "email")
protected String eMail;
@Column(name = "address1")
protected String address1;
@Column(name = "address2", nullable = true)
protected String address2;
@Column(name = "city")
protected String city;
@Column(name = "state")
protected String state;
@Column(name = "zip", nullable = true)
protected String zip;
@Column(name = "country")
protected String country;
@Column(name = "phone")
protected String phone;
@Column(name = "brand_id")
protected int brandId;
/*--- Constructors ---*/
/**
* default
*/
public BrandManager() {
setCreationTime(Calendar.getInstance().getTimeInMillis());
}
public BrandManager(String password, String firstName, String lastName, String eMail, String address1, String address2, String city,
String state, String zip, String country, String phone, int brandId) {
this();
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.eMail = eMail;
this.address1 = address1;
this.address2 = address2;
this.city = city;
this.state = state;
this.zip = zip;
this.country = country;
this.phone = phone;
this.brandId = brandId;
}
/*--- Overridden Methods ---*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// currently not holding authorities in DB, but returning hard-coded
return AuthorityUtils.createAuthorityList(ROLE);
}
... getter and setters...
}
My UserDetailsService simple load the user from the DB (using a dao delegation).
Now, As I’m testing my server (running in tomcat) and calling the secured URL I see something strange.
I put a break point in the “decide” method of the AffirmativeBased class, which is the default implementation of the DecisionManager of Spring security:
public void decide(Authentication authentication, Object object, Collection configAttributes)
throws AccessDeniedException {
int deny = 0;
for (AccessDecisionVoter voter : getDecisionVoters()) {
int result = voter.vote(authentication, object, configAttributes);
if (logger.isDebugEnabled()) {
logger.debug("Voter: " + voter + ", returned: " + result);
}
switch (result) {
case AccessDecisionVoter.ACCESS_GRANTED:
return;
case AccessDecisionVoter.ACCESS_DENIED:
deny++;
break;
default:
break;
}
}
and when invoking my service method, which is annotated with the @Secured annotation and I see that the “configAttributes” property contains one element with the value of [permitAll], which is not what I expected to see. I expected to see “ROLE_BRAND_MANAGER”. Am I debugging the wrong place? How can I know that my security-related code is right?
Thanks in advance,
Yogi
As suggested by php-coder:
I had to switch the order of my url-interception definitions and now the flow makes sense.