What is it?
A security framework, that competes with a framework that are built in Java EE and theirs servers. And is a mandatory requirement when running Spring Boot.
Prerequisite
- Will use Spring Tools (STS) version 3 and not version 4, since latest version lacks the built in Tomcat (actually it is a modified Spring Tomcat, Pivotal TC Server).
- Will use my previous blog example of Getting Started with Spring MVC .
- Will use jetty maven plugin (org.eclipse.jetty:jetty-maven-plugin) to test and run the application via 'mvn jetty:run'.
Setup
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${springframework.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${springframework.version}</version>
</dependency>
Add to the src/main/webapp/WEB-INF/web.xml.
<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>
and /WEB-INF/spring/security-context.xml to your contextConfigLocation. The complete web.xml looks now with Spring MVC:
<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_3_1.xsd"
version="3.1">
<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>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>
/WEB-INF/spring/application-context.xml
/WEB-INF/spring/security-context.xml
</param-value>
</context-param>
<servlet>
<servlet-name>DispatcherServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value></param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>DispatcherServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
And now an example /WEB-INF/spring/security-context.xml which protectes all HTTP with the role 'ROLE_USER' and in-memory user 'foo' with password 'changeit' and role 'ROLE_USER'.
<?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.xsd">
<security:http auto-config="true" use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>
<security:authentication-manager>
<security:authentication-provider>
<security:password-encoder ref="passwordEncoder" />
<security:user-service>
<security:user name="foo" password="changeit" authorities="ROLE_USER" />
</security:user-service>
</security:authentication-provider>
</security:authentication-manager>
<!-- java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for
the id "null" -->
<bean id="passwordEncoder"
class="org.springframework.security.crypto.password.NoOpPasswordEncoder"
factory-method="getInstance" />
</beans>
Architectual Overview
Authorization
Either via XML (or Java which is the requirement for Spring Boot).
<security:http auto-config="true" use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_USER" />
</security:http>
What is important is that the rule with highest clearance must come first and then in decreased clearance. The reason for this is because the first rule that passes, will exit the process. Example of a misconfiguration.
<security:http auto-config="true" use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_ANONYMOUS" />
<security:intercept-url pattern="/topsecret/*" access="ROLE_TOP_SECRET" />
</security:http>
This configuration will lead to that everyone will be granted access to '/topsecret/*', since the first rule will be successfull and will overlay any further more restricted authorization rules.
Another way to enforce authoriation is via class or method annotation @org.springframework.security.access.annotation.Secured. Example:
@Secured({ "ROLE_USER" })
public void create(Contact contact) {...}
Authentication
Basic
/WEB-INF/spring/security-context.xml
<security:http auto-config="true" use-expressions="false">
<security:http-basic />
...
</security:http>
Form
/WEB-INF/spring/security-context.xml
<security:http auto-config="true" use-expressions="false">
<security:form-login login-page="/login"
login-processing-url="/login" default-target-url="/"
always-use-default-target="true"
authentication-failure-url="/login?error=true" />
...
</security:http>
View in JSP.
<form action=${loginVar} method="POST">
Username: <input type="text" name="username" /></br>
Password: <input type="password" name="password" /></br>
<input type="submit" value="Login" />
</form>
Logout
View in JSP.
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
...
<a href="<c:url value="/logout" />">Logout</a>
...
/WEB-INF/spring/security-context.xml
<security:http auto-config="true" use-expressions="false">
...
<security:logout logout-success-url="/login?logout=true"/>
...
</security:http>
JDBC Module
Add maven dependency
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-jpa</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.185</version>
</dependency>
WEB-INF/spring/application-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:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc"
xmlns:jdbc="http://www.springframework.org/schema/jdbc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<tx:annotation-driven />
<jdbc:embedded-database id="datasource" type="H2">
<jdbc:script location="classpath:initdb.sql" />
</jdbc:embedded-database>
...
</beans>
src/main/resources/initdb.sql
drop table users if exists;
drop table authorities if exists;
create table users(
username varchar_ignorecase(50) not null primary key,
password varchar_ignorecase(50) not null,
enabled boolean not null
);
create table authorities (
username varchar_ignorecase(50) not null,
authority varchar_ignorecase(50) not null,
constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
insert into users(username, password, enabled) values ('foodb', 'changeit', true);
insert into authorities(username, authority) values ('foodb', 'ROLE_USER');
src/main/webapp/WEB-INF/spring/security-context.xml
<security:authentication-manager>
<security:authentication-provider>
<security:password-encoder ref="passwordEncoder" />
<security:jdbc-user-service data-source-ref="datasource" />
</security:authentication-provider>
</security:authentication-manager>
Advanced JDBC Module - Groups
src/main/webapp/WEB-INF/spring/security-context.xml
<security:authentication-manager>
<security:authentication-provider>
<security:password-encoder ref="passwordEncoder" />
<security:jdbc-user-service data-source-ref="datasource"
group-authorities-by-username-query="select g.id, g.group_name, ga.authority
from groups g, group_members gm, group_authorities ga
where gm.username = ? and g.id = ga.group_id and g.id = gm.group_id" />
</security:authentication-provider>
</security:authentication-manager>
src/main/resources/initdb.sql add the Group Authorities tables
create table groups (
id bigint generated by default as identity(start with 0) primary key,
group_name varchar_ignorecase(50) not null
);
create table group_authorities (
group_id bigint not null,
authority varchar(50) not null,
constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);
create table group_members (
id bigint generated by default as identity(start with 0) primary key,
username varchar(50) not null,
group_id bigint not null,
constraint fk_group_members_group foreign key(group_id) references groups(id)
);
And remove the 'insert into authorities' and instead insert into group authorities tables.
insert into users(username, password, enabled) values ('foodb', 'changeit', true);
--insert into authorities(username, authority) values ('foodb', 'ROLE_USER');
insert into groups(id, group_name) values (1, 'Admin');
insert into group_authorities(group_id, authority) values (1, 'ROLE_USER');
insert into group_members(id, username, group_id) values (1, 'foodb', 1);
Hash Algorithm
https://docs.spring.io/spring-security/site/docs/current/api/org/springframework/security/crypto/password/PasswordEncoder.html
LDAP Module
Will use The Apache Directory, which is written in Java.
Download Apache DS. Download Archive zip/tar.gz.
Apache Directory Studio. Download Archive zip/tar.gz.
Unzip both zip/tar.gz to your desktop.
$ cd /home/magnuskkarlsson/bin/apacheds-2.0.0.AM25/bin
$ ./apacheds.sh start
Using ADS_HOME: /home/magnuskkarlsson/bin/apacheds-2.0.0.AM25
Using JAVA_HOME:
Starting ApacheDS instance 'default'...
Now start your 'ApacheDirectoryStudio'. Goto view 'LDAP Servers', click new and add ApacheDS 2.0.0 server. Then stop your apacheds server.
$ ./apacheds.sh stop
Return to studio and right click on you ApacheDS server and choose 'Create a Connection' and right click again and choose 'Run'.
Now we need to add a new partition for your database. Switch to LDAP Servers and double click on your local ldap, in the main space click Partitions and click Add.
After editing click Save. Then stop you local ldap and then start. After restarted double click on Connection and you will see your new partition.
Now we are going to import our users. Right click on Root DSE and choose Import and LDIF Import.
src/test/resources/spring-security-ldap.ldif
dn: dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: domain
objectclass: extensibleObject
dc: magnuskkarlsson
dn: ou=groups,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: organizationalUnit
ou: groups
dn: ou=people,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: organizationalUnit
ou: people
dn: uid=ben,ou=people,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Ben Alex
sn: Alex
uid: ben
userPassword: changeit
dn: uid=bob,ou=people,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Bob Hamilton
sn: Hamilton
uid: bob
userPassword: changeit
dn: cn=user,ou=groups,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: groupOfUniqueNames
cn: user
uniqueMember: uid=ben,ou=people,dc=magnuskkarlsson,dc=se
uniqueMember: uid=bob,ou=people,dc=magnuskkarlsson,dc=se
dn: cn=admin,ou=groups,dc=magnuskkarlsson,dc=se
objectclass: top
objectclass: groupOfUniqueNames
cn: admin
uniqueMember: uid=ben,ou=people,dc=magnuskkarlsson,dc=se
Now configure Spring Security LDAP, but first we need to add maven dependency.
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
<version>${springframework.version}</version>
</dependency>
src/main/webapp/WEB-INF/spring/security-context-ldap.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.xsd">
<security:http auto-config="true" use-expressions="false">
<security:intercept-url pattern="/**" access="ROLE_USER" />
<security:logout logout-success-url="/login" />
</security:http>
<security:authentication-manager>
<security:ldap-authentication-provider server-ref="ldapServer"
user-dn-pattern="uid={0},ou=people" group-search-base="ou=groups">
</security:ldap-authentication-provider>
</security:authentication-manager>
<security:ldap-server id="ldapServer"
url="ldap://localhost:10389/dc=magnuskkarlsson,dc=se"
manager-dn="uid=admin,ou=system" manager-password="secret" />
</beans>
Expression-Based Access Control/Spring Expression Language(SPEL)
First you need to declare SPEL bean then change 'use-expressions' to true. Then you can use your expression.
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#el-access
<bean class="org.springframework.security.web.access.expression.DefaultWebSecurityExpressionHandler" />
<security:http auto-config="true" use-expressions="true">
<security:intercept-url pattern="/**" access="hasRole('ROLE_USER') and hasIpAddress('192.168.1.0/24')" />
...
</security:http>
Spring Security Taglib
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${springframework.version}</version>
</dependency>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#taglibs
Example in your JSP page.
<sec:authorize access="isAuthenticated()">
Welcome
<sec:authentication property="principal.firstName"/>
<sec:authentication! property="principal.lastName"/>
</sec:authorize>
<sec:authorize access="hasRole('ROLE_ADMIN')">
TOP SECRET
</sec:authorize>
Access Control List, ACL
The ACL is used to grant access to specific person and/or rule to specfic instances of a object.
Example. You have reports and some reports (db with primary key e.g. 1 and 3) should only be accessed by user (db primary key user e.g. 1, 5). That can you enforce with ACL.
Or specific reports should only be granted read access, that can also be enforced with ACL.
The downside with ACL is management of this finegrained access control.
https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle/#dbschema-acl
https://www.baeldung.com/spring-security-acl
CSRF
Use spring security form taglib
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<form:form ...>
</form>
Eitherwise you need to manually add it.
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %>
<form ...>
<sec:csrfInput />
</form>
Java Configuration Part
So far I have configured Spring Security through XML, but today in the spring community is moving to writing java code instead. My opinion is that XML is more concisely and more easy to overview, but XML has a bad reputation in the spring community.
Start class.
package se.magnuskkarlsson.example.springmvc;
import org.springframework.security.web.context.AbstractSecurityWebApplicationInitializer;
public class SecurityWebApplicationInitializer extends AbstractSecurityWebApplicationInitializer {
public SecurityWebApplicationInitializer() {
super(SecurityConfig.class);
}
}
Then the actual authentication and authorization code.
package se.magnuskkarlsson.example.springmvc;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
@Configuration
@EnableWebSecurity
@ImportResource("/WEB-INF/spring/application-context.xml")
@ComponentScan("se.magnuskkarlsson.example")
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("foojava").password("changeit").roles("USER");
}
}