Spring Security Tutorial
Spring Security Tutorial
1. Web Configuration
The 'springSecurityFilterChain' filter needs to be configured to
intercept all URLs so Spring Security can control access to them. The filter
must be named this to match the default bean it retrieves from the Spring
context.
/WEB-INF/web.xml
<?xml
version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<display-name>simple-security</display-name>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*-context.xml
</param-value>
</context-param>
<!-- Enables Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>simple-form</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>simple-form</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
2. Spring Configuration
The security:global-method-security element
configures annotation based security so @Secured can be used to restrict access to methods.
The security:http is set to auto-configure
basic HTTP security. Inside the the login, logout, and main style sheet are set
to the anonymous role (unrestricted access). The rest of the site is restricted
to an authenticated user in the user role. The default login and logout
configuration is also customized to use custom pages to maintain the sites look
& feel.
The authentication is set
to use jdbc based user authentication. Only the DataSource needs to be set on the security:jdbc-user-service element
if the default tables are used. Although other tables can be used by setting
custom queries on the element.
/WEB-INF/spring/security-context.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:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<security:global-method-security
secured-annotations="enabled" />
<security:http
auto-config="true">
<!-- Restrict URLs based on role
-->
<security:intercept-url
pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY"
/>
<security:intercept-url
pattern="/logoutSuccess*"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url
pattern="/css/main.css"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url
pattern="/resources/**" access="IS_AUTHENTICATED_ANONYMOUSLY"
/>
<security:intercept-url
pattern="/**" access="ROLE_USER" />
<!-- Override default login and
logout pages -->
<security:form-login
login-page="/login.html"
login-processing-url="/loginProcess"
default-target-url="/index.jsp"
authentication-failure-url="/login.html?login_error=1" />
<security:logout
logout-url="/logout"
logout-success-url="/logoutSuccess.html" />
</security:http>
<security:authentication-manager>
<security:authentication-provider
>
<security:jdbc-user-service
data-source-ref="dataSource" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
3. JSP Example
The security tag is defined
at the top of the page with a prefix of 'sec'. Then around delete link the sec:authorize tag is configured to only
show the link if the user is in the role 'ROLE_ADMIN'. Now, this doesn't
actually stop someone from executing a delete query if they know the URL.
Below, in the PersonService, the @Secured tag is
configured to enforce the rule that only an admin can delete a record.
/WEB-INF/jsp/person/search.jsp
<%@
taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core"%>
<%@
taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"
%>
<%@
taglib prefix="form"
uri="http://www.springframework.org/tags/form"%>
<%@
taglib prefix="sec" uri="http://www.springframework.org/security/tags"
%>
<h1><fmt:message
key="person.search.title"/></h1>
<table
class="search">
<tr>
<th><fmt:message
key="person.form.firstName"/></th>
<th><fmt:message
key="person.form.lastName"/></th>
</tr>
<c:forEach
var="person" items="${persons}"
varStatus="status">
<tr>
<c:set var="personFormId"
value="person${status.index}"/>
<c:url var="editUrl"
value="/person/form.html">
<c:param name="id"
value="${person.id}" />
</c:url>
<sec:authorize
ifAllGranted="ROLE_ADMIN">
<c:url var="deleteUrl"
value="/person/delete.html"/>
<form
id="${personFormId}" action="${deleteUrl}"
method="POST">
<input id="id"
name="id" type="hidden" value="${person.id}"/>
</form>
</sec:authorize>
<td>${person.firstName}</td>
<td>${person.lastName}</td>
<td>
<a href='<c:out
value="${editUrl}"/>'><fmt:message
key="button.edit"/></a>
<sec:authorize
ifAllGranted="ROLE_ADMIN">
<a
href="javascript:document.forms['${personFormId}'].submit();"><fmt:message
key="button.delete"/></a>
</sec:authorize>
</td>
</tr>
</c:forEach>
</table>
4. Code Example
The
delete method has access restricted to users in the admin role by putting the @Secured annotation
above it and setting the allowed roles. Which in this case is only the
'ROLE_ADMIN' role. By securing the service interface, even if a non-admin user
tries to execute the delete URL they will not be able to delete a record.
public
interface PersonService {
/**
* Find person by id.
*/
public Person findById(Integer id);
/**
* Find persons.
*/
public Collection<Person> find();
/**
* Saves person.
*/
public Person save(Person person);
/**
* Deletes person.
*/
@Secured ({"ROLE_ADMIN"})
public void delete(Person person);
}
You may wonder why the
'/delete/person*' URL wasn't restricted. For the current application, this
would have been sufficient. But we don't really want to restrict the URL, we
want to restrict the actual delete action. Spring Security makes it very easy to
restrict access to actual methods. If at some point in the future the another
URL is made to also delete a record, our rule will still be enforced. Also, if
at some point someone creates a method that calls delete that is accessed by a
completely different URL, only admins will be able to execute this new part of
the application successfully. A new implementation of the interface will als
5. SQL Script
security_schema.sql
SET
IGNORECASE TRUE;
CREATE
TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(50) NOT NULL,
enabled BIT NOT NULL
);
CREATE
TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL
);
CREATE
UNIQUE INDEX ix_auth_username ON authorities (username, authority);
ALTER
TABLE authorities ADD CONSTRAINT fk_authorities_users foreign key (username)
REFERENCES users(username);
INSERT
INTO users VALUES ('david', 'newyork', true);
INSERT
INTO users VALUES ('alex', 'newjersey', true);
INSERT
INTO users VALUES ('tim', 'illinois', true);
INSERT
INTO authorities VALUES ('david', 'ROLE_USER');
INSERT
INTO authorities VALUES ('david', 'ROLE_ADMIN');
INSERT
INTO authorities VALUES ('alex', 'ROLE_USER');
INSERT
INTO authorities VALUES ('tim', 'ROLE_USER');
1. Web Configuration
<?xml
version="1.0" encoding="UTF-8"?>
<web-app
xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
version="3.0"
metadata-complete="true">
<display-name>simple-security</display-name>
<listener>
<listener-class>
org.springframework.web.context.ContextLoaderListener
</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/*-context.xml
</param-value>
</context-param>
<!-- Enables Spring Security -->
<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>
org.springframework.web.filter.DelegatingFilterProxy
</filter-class>
</filter>
<filter>
<filter-name>encoding-filter</filter-name>
<filter-class>
org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>encoding-filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<filter-mapping>
<filter-name>springSecurityFilterChain</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<servlet>
<servlet-name>simple-form</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
</servlet>
<servlet-mapping>
<servlet-name>simple-form</servlet-name>
<url-pattern>*.html</url-pattern>
</servlet-mapping>
<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
</web-app>
<?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:security="http://www.springframework.org/schema/security"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/security
http://www.springframework.org/schema/security/spring-security-3.1.xsd">
<security:global-method-security
secured-annotations="enabled" />
<security:http
auto-config="true">
<!-- Restrict URLs based on role
-->
<security:intercept-url
pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY"
/>
<security:intercept-url
pattern="/logoutSuccess*"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url
pattern="/css/main.css"
access="IS_AUTHENTICATED_ANONYMOUSLY" />
<security:intercept-url
pattern="/resources/**" access="IS_AUTHENTICATED_ANONYMOUSLY"
/>
<security:intercept-url
pattern="/**" access="ROLE_USER" />
<!-- Override default login and
logout pages -->
<security:form-login
login-page="/login.html"
login-processing-url="/loginProcess"
default-target-url="/index.jsp"
authentication-failure-url="/login.html?login_error=1" />
<security:logout
logout-url="/logout"
logout-success-url="/logoutSuccess.html" />
</security:http>
<security:authentication-manager>
<security:authentication-provider
>
<security:jdbc-user-service
data-source-ref="dataSource" />
</security:authentication-provider>
</security:authentication-manager>
</beans>
<%@
taglib prefix="c"
uri="http://java.sun.com/jsp/jstl/core"%>
<%@
taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"
%>
<%@
taglib prefix="form"
uri="http://www.springframework.org/tags/form"%>
<%@
taglib prefix="sec" uri="http://www.springframework.org/security/tags"
%>
<h1><fmt:message
key="person.search.title"/></h1>
<table
class="search">
<tr>
<th><fmt:message
key="person.form.firstName"/></th>
<th><fmt:message
key="person.form.lastName"/></th>
</tr>
<c:forEach
var="person" items="${persons}"
varStatus="status">
<tr>
<c:set var="personFormId"
value="person${status.index}"/>
<c:url var="editUrl"
value="/person/form.html">
<c:param name="id"
value="${person.id}" />
</c:url>
<sec:authorize
ifAllGranted="ROLE_ADMIN">
<c:url var="deleteUrl"
value="/person/delete.html"/>
<form
id="${personFormId}" action="${deleteUrl}"
method="POST">
<input id="id"
name="id" type="hidden" value="${person.id}"/>
</form>
</sec:authorize>
<td>${person.firstName}</td>
<td>${person.lastName}</td>
<td>
<a href='<c:out
value="${editUrl}"/>'><fmt:message
key="button.edit"/></a>
<sec:authorize
ifAllGranted="ROLE_ADMIN">
<a
href="javascript:document.forms['${personFormId}'].submit();"><fmt:message
key="button.delete"/></a>
</sec:authorize>
</td>
</tr>
</c:forEach>
</table>
public
interface PersonService {
/**
* Find person by id.
*/
public Person findById(Integer id);
/**
* Find persons.
*/
public Collection<Person> find();
/**
* Saves person.
*/
public Person save(Person person);
/**
* Deletes person.
*/
@Secured ({"ROLE_ADMIN"})
public void delete(Person person);
}
SET
IGNORECASE TRUE;
CREATE
TABLE users (
username VARCHAR(50) NOT NULL PRIMARY KEY,
password VARCHAR(50) NOT NULL,
enabled BIT NOT NULL
);
CREATE
TABLE authorities (
username VARCHAR(50) NOT NULL,
authority VARCHAR(50) NOT NULL
);
CREATE
UNIQUE INDEX ix_auth_username ON authorities (username, authority);
ALTER
TABLE authorities ADD CONSTRAINT fk_authorities_users foreign key (username)
REFERENCES users(username);
INSERT
INTO users VALUES ('david', 'newyork', true);
INSERT
INTO users VALUES ('alex', 'newjersey', true);
INSERT
INTO users VALUES ('tim', 'illinois', true);
INSERT
INTO authorities VALUES ('david', 'ROLE_USER');
INSERT
INTO authorities VALUES ('david', 'ROLE_ADMIN');
INSERT
INTO authorities VALUES ('alex', 'ROLE_USER');
INSERT
INTO authorities VALUES ('tim', 'ROLE_USER');
Comments
Post a Comment