Reputation: 2222
I have a Spring MVC app which I wish to integrate Spring Security with (Spring 3.0.x).
web.xml contains:
<context-param>
<description>Context Configuration locations for Spring XML files</description>
<param-name>contextConfigLocation</param-name>
<param-value>
classpath*:spring/spring-model.xml
classpath*:spring/spring-compiler.xml
classpath*:spring/spring-ui.xml
classpath*:spring/spring-security.xml
</param-value>
</context-param>
<listener>
<description><![CDATA[
Loads the root application context of this web app at startup, use
contextConfigLocation paramters defined above or by default use "/WEB-INF/applicationContext.xml".
- Note that you need to fall back to Spring's ContextLoaderServlet for
- J2EE servers that do not follow the Servlet 2.4 initialization order.
Use WebApplicationContextUtils.getWebApplicationContext(servletContext) to access it anywhere in the web application, outside of the framework.
The root context is the parent of all servlet-specific contexts.
This means that its beans are automatically available in these child contexts,
both for getBean(name) calls and (external) bean references.
]]></description>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<servlet>
<description>Configuration for the Spring MVC webapp servlet</description>
<servlet-name>springmvc</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:spring/spring-mvc.xml</param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>springmvc</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>
I would like to add role-based security so that users can't access certain parts of the site.
e.g. a user should have the role CRICKET_USER
to be able to access http://example.com/sports/cricket
and the role FOOTBALL_USER
to access http://example.com/sports/football
.
The URIs in the application retain this hierarchy, so there might be resources such as http://example.com/sports/football/leagues/premiership
which should similarly require the user to have the role FOOTBALL_USER
.
I have a controller like so:
@Controller("sportsController")
@RequestMapping("/sports/{sportName}")
public class SportsController {
@RequestMapping("")
public String index(@PathVariable("sportName") Sport sport, Model model) {
model.addAttribute("sport", sport);
return "sports/index";
}
}
I've been trying to use the most idiomatic, obvious way to fulfil this requirement, but I'm not sure I've found it yet. I've tried 4 different approaches.
I've tried to use @PreAuthorize("hasRole(#sportName.toUpperCase() + '_USER')")
on each @RequestMapping method on that controller (and other controllers which handle URI requests further down the hierarchy. I haven't been able to get that working; no error, but it doesn't seem to do anything.
Bad points:
@Controller
. That isn't very DRY. Plus there is the potential for leaving a security hole if more functionality gets added and someone forgets to add the annotation to the new code.<http use-expressions="true">
<!-- note that the order of these filters are significant -->
<intercept-url pattern="/app/sports/**" access="hasRole(#sportName.toUpperCase() + '_USER')" />
<form-login always-use-default-target="false"
authentication-failure-url="/login/" default-target-url="/"
login-page="/login/" login-processing-url="/app/logincheck"/>
<!-- This action catch the error message and make it available to the view -->
<anonymous/>
<http-basic/>
<access-denied-handler error-page="/app/login/accessdenied"/>
<logout logout-success-url="/login/" logout-url="/app/logout"/>
</http>
This feels like it should work, would be obvious to other developers as to what it's doing but I've not been successful with this approach. My only pain-point with this approach is not being able to write a test that will flag a problem if something changes down the road.
java.lang.IllegalArgumentException: Failed to evaluate expression 'hasRole(#sportName.toUpper() + '_USER')'
at org.springframework.security.access.expression.ExpressionUtils.evaluateAsBoolean(ExpressionUtils.java:13)
at org.springframework.security.web.access.expression.WebExpressionVoter.vote(WebExpressionVoter.java:34)
...
Caused by:
org.springframework.expression.spel.SpelEvaluationException: EL1011E:(pos 17): Method call: Attempted to call method toUpper() on null context object
at org.springframework.expression.spel.ast.MethodReference.getValueInternal(MethodReference.java:69)
at org.springframework.expression.spel.ast.CompoundExpression.getValueInternal(CompoundExpression.java:57)
public class SportAuthorisationFilter extends GenericFilterBean {
/**
* {@inheritDoc}
*/
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String pathInfo = httpRequest.getPathInfo();
/* This assumes that the servlet is coming off the /app/ context and sports are served off /sports/ */
if (pathInfo.startsWith("/sports/")) {
String sportName = httpRequest.getPathInfo().split("/")[2];
List<String> roles = SpringSecurityContext.getRoles();
if (!roles.contains(sportName.toUpperCase() + "_USER")) {
throw new AccessDeniedException(SpringSecurityContext.getUsername()
+ "is not permitted to access sport " + sportName);
}
}
chain.doFilter(request, response);
}
}
and:
<http use-expressions="true">
<!-- note that the order of these filters are significant -->
<!--
Custom filter for /app/sports/** requests. We wish to restrict access to those resources to users who have the
{SPORTNAME}_USER role.
-->
<custom-filter before="FILTER_SECURITY_INTERCEPTOR" ref="sportsAuthFilter"/>
<form-login always-use-default-target="false"
authentication-failure-url="/login/" default-target-url="/"
login-page="/login/" login-processing-url="/app/logincheck"/>
<!-- This action catch the error message and make it available to the view -->
<anonymous/>
<http-basic/>
<access-denied-handler error-page="/app/login/accessdenied"/>
<logout logout-success-url="/login/" logout-url="/app/logout"/>
</http>
<beans:bean id="sportsAuthFilter" class="com.example.web.controller.security.SportsAuthorisationFilter" />
Plus points:
Bad points:
@Component
public class SportFormatter implements DiscoverableFormatter<Sport> {
@Autowired
private SportService SportService;
public Class<Sport> getTarget() {
return Sport.class;
}
public String print(Sport sport, Locale locale) {
if (sport == null) {
return "";
}
return sport.getName();
}
public Sport parse(String text, Locale locale) throws ParseException {
Sport sport;
if (text == null || text.isEmpty()) {
return new Sport();
}
if (NumberUtils.isNumber(text)) {
sport = sportService.getByPrimaryKey(new Long(text));
} else {
Sport example = new Sport();
example.setName(text);
sport = sportService.findUnique(example);
}
if (sport != null) {
List<String> roles = SpringSecurityContext.getRoles();
if (!roles.contains(sportName.toUpperCase() + "_USER")) {
throw new AccessDeniedException(SpringSecurityContext.getUsername()
+ "is not permitted to access sport " + sportName);
}
}
return sport != null ? sport : new Sport();
}
}
Plus points:
Bad points:
Please point out which part of the fine manual I'm missing.
Upvotes: 1
Views: 8498
Reputation: 49
I found a solution as well, use :
<security:global-method-security secured-annotations="enabled" proxy-target-class="true"/>
Hope it'll help any.
Upvotes: 0
Reputation: 242686
Instead of #sportName.toUpper()
you need to use something like #sport.name.toUpper()
, because #...
variables in @PreAuthorize
refer to method arguments:
@RequestMapping(...)
@PreAuthorize("hasRole(#sport.name.toUpper() + '_USER')")
public String index(@PathVariable("sportName") Sport sport, Model model) { ... }
See also:
Upvotes: 7