Spring Security

Last Updated on: February 8, 2021 pm

Spring Security - Overview

Model

  • Spring Security defines a framework for Security.
  • Implmented using Servlet filters in the background.
  • Two methods of securing a Web app: Declarative and Programmatic.

Big Pic

Big Picture

Flow Chart in Action

Flowchar in Action

Security Concepts

  • Authentication - Check user id or password with credentials stored in App/db.
  • Authorization - Check to see if user has an authorized role.

Declarative Security

Defines application’s security constraints in configuration.

  • All Java config
  • Or Spring XML Config

Programmatic Security

  • Provides an API for custom application coding.

  • Provides greater customization for specific app requirements.

Login Methods

  • HTTP
  • Default Login Form
  • Custom Login Form

Spring MVC - Java Config

Add Maven Dependencies

Import existing maven project into IntelliJ.

Since we are not using web.xml, customize Maven build and must add Maven WAR plugin.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- TODO: Add support for Maven WAR Plugin -->
<build>
<finalName>spring-security-demo</finalName>
<pluginManagement>
<plugins>
<plugin>
<!-- Add Maven Coordinates-->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>3.3.1</version>
</plugin>
</plugins>
</pluginManagement>
</build>

Create Spring @Configuration

Create a DemoAppConfig.java file in the source directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = "com.luv2code.springsecurity.demo")
public class DemoAppConfig {
// define a bean for view resolver
@Bean
public ViewResolver viewResolver(){
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/view/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
}

Create Spring Dispatcher Servlet Initializer

Spring MVC provides support for web app initialization.

Your code is used to initialize the servlet container.

AbstractAnnotationConfigDispatcherServletInitializer

  • Extend the abstract base class
  • Override some required methods
  • Specify servlet mapping and location of your app config
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MySpringMvcDispatcherServletInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
@Override
protected Class<?>[] getRootConfigClasses() {
return new Class[0];
}

@Override
protected Class<?>[] getServletConfigClasses() { // Dispatcher Servlet
return new Class[]{ DemoAppConfig.class }; // ContextConfigLocation
}

@Override
protected String[] getServletMappings() { // dispatcher and url pattern
return new String[] { "/" }; // map
}
}

Develop Spring Controller

1
2
3
4
5
6
7
@Controller
public class DemoController {
@GetMapping("/")
public String showHome(){
return "home";
}
}

Develop JSP View Page

Create a test.jsp file and run the tomcat server to test the demo.

Configure Basic Security

Create Spring Security Initializer

AbstractSecurityWebApplicationInitializer

A special class to register the Spring Security Filters.

TODO List: Just extends the abstract class! No methods to override!

1
2
3
public class SecurityWebApplicationInitializer
extends AbstractSecurityWebApplicationInitializer {
}

Create Spring Security Configuration

1
2
3
4
5
6
7
8
9
@Configuration
@EnableWebSecurity
public class DemoSecurityConfig
extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {

}
}

Add Users Passwords and Roles

Add users in DemoSecurityConfig.java

1
2
3
4
5
6
// add our users for in memory authentication
UserBuilder users = User.withDefaultPasswordEncoder();
auth.inMemoryAuthentication()
.withUser(users.username("John").password("test123").roles("EMPLOYEE"))
.withUser(users.username("Mary").password("test123").roles("MANAGER"))
.withUser(users.username("Susan").password("test123").roles("ADMIN"));

Add Custom LogIn Form

Modify Spring Security Configuration

To reference custom login form.

1
2
3
4
5
6
7
8
9
10
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll();
}

Override the configure method.

Develop a Controller

To show the custom login form.

1
2
3
4
5
6
7
@Controller
public class LoginController {
@GetMapping("/showMyLoginPage")
public String showMyLoginPage(){
return "plain-login";
}
}

Create custom login form

  • HTML
  • Spring MVC form tag

Why use Context Path?

  • Allows us to dynamically reference context path of application.
  • Helps to keep links relative to application context path.LL
  • If you change context path of app, then links will still work.
  • Much better than hard-coding the context path.
1
2
3
4
5
6
7
8
9
<body>
<h3> My Custom Login Page</h3>
<form:form action="${pageContext.request.contextPath}/authenticateTheUser" method="post">
<p>User Name: <input type="text" name="username"/></p>
<p>Password: <input type="password" name="password"/></p>
<input type="submit" value="Login"/>
</form:form>
</body>
</html>

Add Error Message

Modify the Login Form - Check for error

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<body>
<h3> My Custom Login Page</h3>
<form:form action="${pageContext.request.contextPath}/authenticateTheUser" method="post">
<c:if test="${param.error != null}">
<i class="failed">Sorry, you entered wrong credentials.</i>
</c:if>
<p>User Name: <input type="text" name="username"/></p>
<p>Password: <input type="password" name="password"/></p>
<input type="submit" value="Login"/>
</form:form>
</body>
</html>

Note: Change http://java.sun.com/jstl/core to http://java.sun.com/jsp/jstl/core, unless you will get Error: attribute test does not accept any expressions.

Add Logout Support

Add Logout to Configuration

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
.and()
.logout().permitAll();
}

Note: This will add logout support for default URL /logout

Add Logout Button to JSP

The button needs to send data to the default URL /logout

Logout URL will be handled by Spring Security Fileters - No coding required.

1
2
3
<form:form action="${pageContext.request.contextPath}/logout" method="post">
<input type="submit" value="Logout">
</form:form>

Update Form

1
2
3
4
5
<c:if test="${param.logout != null}">
<div class="alert alert-success col-xs-offset-1 col-xs-10">
You have been logged out.
</div>
</c:if>

Cross Site Request Forgery(CSRF)

What is CSRF?

CSRF(Cross Site Request Forgery)即跨站请求伪造。就是利用后台有规律的接口,例如 localhost/deleteAriticle.php?id=3&username=xiaoxiao ,攻击者在被攻击的网站页面嵌入这样的代码,当用户xiaoxiao访问该网站的时候,会发起这条请求。服务器会删除id为3的数据。
客户端防范:对于数据库的修改请求,全部使用POST提交,禁止使用GET请求。
服务器端防范:一般的做法是在表单里面添加一段隐藏的唯一的token(请求令牌)。

Spring Security CSRF Protection

Spring Security uses the Synchronizer Token Pattern.

  • Each request includes a session cookie and randomly generated token.

For request processing, Spring Security verifies token before processing.

All of this is handled by Spring Security Filters automatically.

When to use CSRF Protection?

Use CSRF protection for any normal browser web requests.

How to use CSRF Protection?

For form submissions use POST instead of GET.

Just use Spring MVC Form Tags.

<form:form> will automatically adds CSRF token.

Manually Add CSRF Token

1
2
3
4
5
<form action="" method="">
<input type="hidden"
name="${_csrf.parameterName}"
value="${_csrf.token}"/>
</form>

If you use plain <form> tag, then you need to manually add CSRF token authentication in the form submission.

Break It

Use plain <form> tag rather than Spring MVC <form:form> tag.

1
<form action="${pageContext.request.contextPath}/authenticateTheUser" method="POST" class="form-horizontal">

Then, we will get an ugly 403 - Forbidden error message.

The server understood the request but refused to authorize it.

Fix It

Add token manually like mentioned above.

Display Username and Roles

Add new Dependency in pom.xml

1
2
3
4
5
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-taglibs</artifactId>
<version>${springsecurity.version}</version>
</dependency>

Update JSP Page

1
2
3
4
5
<%@ taglib prefix="security" uri="http://www.springframework.org/security/tags" %>
<p>
Users: <security:authentication property="principal.username"/>
Roles: <security:authentication property="principal.authorities"/>
</p>

Restrict Access based on Role

Create Controller and Views

1
2
3
4
5
6
7
8
9
@GetMapping("/leader")
public String showLeaders(){
return "leader";
}

@GetMapping("/system")
public String showSystem(){
return "system";
}

Update Roles

1
2
3
4
auth.inMemoryAuthentication()
.withUser(users.username("John").password("test123").roles("EMPLOYEE"))
.withUser(users.username("Mary").password("test123").roles("EMPLOYEE","MANAGER"))
.withUser(users.username("Susan").password("test123").roles("EMPLOYEE","ADMIN"));

Authorize Access to Roles

1
2
3
4
5
6
http.authorizeRequests()
.antMatchers("/").hasRole("EMPLOYEE")
.antMatchers("/leader/**").hasRole("MANAGER")
.antMatchers("/system/**").hasRole("ADMIN")
.and()
.exceptionHandling().accessDeniedPage("/access-denied");

Create the Access-Denied Page

1
2
3
4
5
6
7
8
9
<body>
<h1>
Access Denied!
</h1>
<p>
<a href="${pageContext.request.contextPath}/">Back To Home Page</a>
</p>
</body>
</html>

Display Content Based on Roles

1
2
3
4
5
6
7
8
9
10
11
<security:authorize access="hasRole('MANAGER')">
<p>
<a href="${pageContext.request.contextPath}/leader">Leadership Meeting</a> (Only for Managers)
</p>
</security:authorize>

<security:authorize access="hasRole('ADMIN')">
<p>
<a href="${pageContext.request.contextPath}/system">Admin </a> (Only for Admins)
</p>
</security:authorize>

Add JDBC Database Authentication

Run SQL Scripts

1
2
3
4
5
INSERT INTO `users` 
VALUES
('john','{noop}test123',1),
('mary','{noop}test123',1),
('susan','{noop}test123',1);

Note: noop is the encoding algorithm of the password.

Add Database Support to pom.xml

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.45</version>
</dependency>

<dependency>
<groupId>com.mchange</groupId>
<artifactId>c3p0</artifactId>
<version>0.9.5.2</version>
</dependency>

Create JDBC Properties File

src/main/resources/persistence-mysql.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#
# JDBC connection properties
#
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_security_demo_plaintext?useSSL=false&serverTimezone=UTC
jdbc.user=springstudent
jdbc.password=springstudent

#
# Connection pool properties
#
connection.pool.initialPoolSize=5
connection.pool.minPoolSize=5
connection.pool.maxPoolSize=20
connection.pool.maxIdleTime=3000

Define DataSource in Spring Config

Introduce the Environment variable, this will hold data from the properties file.

Add the @Autowired annotation.

1
2
3
4
5
6
// set up a variable to store the properties
@Autowired
private Environment env;

// set up a logger for diagnostics
private Logger logger = Logger.getLogger(getClass().getName());

Define a Bean for the security data source.

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
@Bean
public DataSource securityDataSource(){
// create a connection pool
ComboPooledDataSource securityDataSource;
try {
securityDataSource = new ComboPooledDataSource();
} catch (Exception e){
throw new RuntimeException(e);
}

// set the jdbc driver class
// Read the db configs from the properties file
try {
securityDataSource.setDriverClass(env.getProperty("jdbc.driver"));
} catch (PropertyVetoException e) {
throw new RuntimeException(e);
}
// log the connection props
logger.info(">>> jdbc.url="+env.getProperty("jdbc.url"));
logger.info(">>> jdbc.user="+env.getProperty("jdbc.user"));

// set the database connection props
securityDataSource.setJdbcUrl(env.getProperty("jdbc.url"));
securityDataSource.setUser(env.getProperty("jdbc.user"));
securityDataSource.setPassword(env.getProperty("jdbc.password"));

// set the connection pool props
securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.initialPoolSize"));
securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.minPoolSize"));
securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.maxPoolSize"));
securityDataSource.setInitialPoolSize(getIntProperties("connection.pool.maxIdleTime"));

return securityDataSource;
}

Define a helper method to read the env properties and convert them to integer.

1
2
3
4
5
6
7
// need a helper method
// read env properties and convert to int
private int getIntProperties(String propsName){
String propVal = env.getProperty(propsName);
int intPropVal = Integer.parseInt(propVal);
return intPropVal;
}

Add JDBC Configuration

Retrieve a securityDataSource Bean from the factory and inject into DemoSecurityConfig.java

1
2
3
4
5
6
7
@Autowired
private DataSource securityDataSource;

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.jdbcAuthentication().dataSource(securityDataSource);
}

Diagram

demoSecurityConfig Diagram

demoAppConfig Diagram

Password Encryption

Run SQL Scripts

Change the DDL to Bcrypt hashing.

1
2
3
4
5
6
7
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` char(68) NOT NULL,
`enabled` tinyint(1) NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

Password must be 68 chars.

  • {bcrypt} - 8 chars.
  • {encodedPassword} - 60 chars.

Modify db Properties

1
jdbc.url=jdbc:mysql://localhost:3306/spring_security_demo_bcrypt?useSSL=false&serverTimezone=UTC

Behind the Scenes

  1. Retrieve the password from db for the user
  2. Read the encoding algorithm id
  3. For case of bcrypt, encrypt the plaintext password from the login form
  4. Compare the encrypted password from login from WITH encrypted password from db.
  5. If there is a matc, then login successful.

Note: The password from db is NEVER decrypted. Because Bcrypt is a one way encryption algorithm.