Spring MVC Security
Declarative Security
- Define application’s security constraints in configuration
- All Java config: @Configuration
- Provides separation of concerns between application code and security
Programmatic Security
- Spring Security provides an API for custom application coding
- Provides greater customization for specific app requirements
Spring MVC Security Project Setup
Development Process
- Create project at Spring Initializr website
- Add Maven dependencies for Spring MVC Web App, Security, Thymeleaf
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity6</artifactId>
</dependency>
- Develop our Spring controller
@Controller
public class DemoController {
@GetMapping("/")
public String showHome() {
// this home corresponds to home.html
return "home";
}
}
- Develop our Thymeleaf view page src/main/resources/templates/home.html
<html>
<body>
Welcome to the luv2code company home page!
</body>
</html>
Configuring Basic Security
- Look at rest-api-security
Spring Security - Custom Login Form
- Modify Spring Security Configuration to reference custom login form
DemoSecurityConfig.java
//Configure security of web paths in application, login, logout etc
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
// Any request to the app must be authenticated
configurer
.anyRequest().authenticated()
)
.formLogin(form ->
//customizing the form login process
form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
);
return http.build();
}
http.authorizeHttpRequestsrestrict access based on the HTTP request.loginPage("/showMyLoginPage")will show the custom form at the request mapping "/showMyLoginPage"- This will be linked up with the Controller in step 2.
.loginProcessingUrl("/authenticateTheUser")Login form should POST data to this URL for processing (check user id and password). Also, no Controller Request Mapping required to be defined. This is done automatically here..permitAll()allows everyone to see the login page, no need to be logged in.
- Develop a Controller to show the custom login form
LoginController.java
@Controller
public class LoginController {
@GetMapping("/showMyLoginPage")
public String showMyLoginPage() {
//thymeleaf UI
return "plain-login";
}
}
- Create custom login form
- HTML (CSS optional)
- Send data to login processing URL:
/authenticateTheUser - Login processing URL will be handled by Spring Security Filters
- Spring Security defines default names for login form fields
- User name field: username
- Password field: password
<form action="#" th:action="@{/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>
Context Root / Context Path (Best Practice)
-
Context Path = Context Root
- The root path for your web app
-
th:action=@{...}will give access to context path dynamically- Helps to keep links relative to application context path
- If you change context path of app, then links will still work
- Better than hard-coding the context path
Spring Security - Show Login Error
- When login fails, by default Spring Security will
- Send user back to your login page
- Append an error parameter: ?error
Development Process:
- Modify custom login form
- Check the error parameter
- If error parameter exists, show an error message
<form>
...
<!-- http://localhost:8080/myapp/showMyLoginPage?error -->
<div th:if="${param.error}">
<i>Sorry! You entered invalid username/password.</i>
</div>
<p>User name: <input type="text" name="username" /></p>
<p>Password: <input type="password" name="password" /></p>
</form>
Spring Security - Bootstrap Login Page
Development Process
- Modify form to point to our login processing URL
- Verify form fields for username and password
- Change our controller to use our new Bootstrap login form
Spring Security - Logout
When a logout is processed, by default Spring Security will
- Invalidate user’s HTTP session and remove session cookies, etc
- Send user back to your login page
- Append a logout parameter: ?logout
Development Process
- Add logout support to Spring Security Configuration
DemoSecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.anyRequest().authenticated()
)
.formLogin(form ->
form
.loginPage("/showMyLoginPage")
.loginProcessingUrl("/authenticateTheUser")
.permitAll()
)
.logout(logout -> logout.permitAll()
);
return http.build();
}
.logout(logout -> logout.permitAll());will add logout support for default URL "/logout"
-
Add logout button to home page
- Send data to default logout URL: /logout
- Logout URL will be handled by Spring Security Filters
- Must use POST method
<form action="#" th:action="@{/logout}" method="POST">
<input type="submit" value="Logout" />
</form>
Must use POST request for logout
- Update login form to display “logged out” message
- Check the logout parameter
- If logout parameter exists, show "logged out" message
plain-login.html
...
<form ... th:action="..." method="...">
<!-- http://localhost:8080/showMyLoginPage?logout -->
<div th:if="${param.logout}">
<i>You have been logged out.</i>
</div>
User name: <input type="text" name="username" />
Password: <input type="password" name="password" />
</form>
...
Display User ID and Roles
Development Process
- Display User ID
- Display User Roles
home.html
<body>
...
<!-- display user id -->
<p>User: <span sec:authentication="principal.username"></span>;</p>
<!-- display user roles -->
<p>Role(s): <span sec:authentication="principal.authorities"></span></p>
</body>
Role(s): <span sec:authentication="principal.authorities"></span> will output with a "ROLE_" prefix
i.g. [ROLE_EMPLOYEE]
Restrict Access Based on Roles
Development Process:
- Create supporting controller code and view pages
home.html
<body>
<!-- Add a link to point to /leaders ... this is for the managers -->
<p>
<a th:href="@{/leaders}">Leadership Meeting</a>
</p>
<!-- Add a link to point to /systems ... this is only for the admins -->
<p>
<a th:href="@{/systems}">IT System Meeting</a>
</p>
</body>
DemoController.java
public class DemoController{
...
// add a request mapping for /leaders
@GetMapping("/leaders")
public String showLeaders(){
//render thymeleaf UI
return "leaders";
}
// add a request mapping for /systems
@GetMapping("/systems")
public String showSystems(){
//render thymeleaf UI
return "systems";
}
}
leaders.html
<h2>Leaders Home Page</h2>
<p>Put whatever Leaders info here</p>
<a th:href="@{/}">Back to Home Page</a>
systems.html
<h2>Systems Home Page</h2>
<p>Put whatever Systems info here</p>
<a th:href="@{/}">Back to Home Page</a>
- Restrict Access based on Roles
- Update your Spring Security Java configuration file (.java)
requestMatchers(<< add path to match on >>).hasAnyRole(<< list of authorized roles >>)
list of authorized roles:- "ADMIN"
- "DEVELOPER"
- "VIP"
- "PLATINUM"
Restrict Path to EMPLOYEE
requestMatchers("/").hasRole("EMPLOYEE");
requestMatchers("/")matches on root path: "/"
Restrict Path /leaders to MANAGER
requestMatchers("/leaders/**").hasRole("MANAGER");
- This one matches on path: "/leaders" and all sub-directories (**)
Altogether Example
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers("/").hasRole("EMPLOYEE")
.requestMatchers("/leaders/**").hasRole("MANAGER")
.requestMatchers("/systems/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
...
}
Custom Access Denied Page
Development Process
- Configure custom page for access denied
DemoSecurityConfig.java
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(configurer ->
configurer
.requestMatchers(“/").hasRole("EMPLOYEE")
...
)
.exceptionHandling(configurer ->
configurer
.accessDeniedPage("/access-denied")
);
...
}
- Create supporting controller code and view page
LoginController.java
@Controller
public class LoginController {
...
//add request mapping for /access_denied
@GetMapping("/access-denied")
public String showAccessDenied(){
return "access-denied";
}
}
access-denied.html
<body>
<h2>Access Denied - You can write any unauthorized access comment here</h2>
<a th:href="@{/}">Back to Home Page</a>
</body>
Display Content Based on Roles
- Example: Only show this section for users with MANAGER role and ADMIN ROLE
...
<div sec:authorize="hasRole('MANAGER')">
<p>
<a th:href="@{/leaders}"> Leadership Meeting </a>
</p>
</div>
<div sec:authorize="hasRole('ADMIN')">
<p>
<a th:href="@{/systems}"> IT Systems Meeting </a>
</p>
</div>
Spring Security User Accounts Stored in Database
- Spring Security can read user account info from database
- Have to follow Spring Security’s predefined table schemas
- Can also customize the table schemas
- Useful if you have custom tables specific to your project / custom
- You will be responsible for developing the code to access the data
- JDBC, JPA/Hibernate
Development Process
- Develop SQL Script to set up database tables
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` varchar(50) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`,`authority`),
CONSTRAINT `authorities_ibfk_1`
FOREIGN KEY (`username`)
REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
<!-- 'authorities' is the same as 'roles' -->
<!-- Internally Spring Security uses “ROLE_” prefix -->
INSERT INTO `authorities`
VALUES
('john','ROLE_EMPLOYEE'),
('mary','ROLE_EMPLOYEE'),
('mary','ROLE_MANAGER'),
('susan','ROLE_EMPLOYEE'),
('susan','ROLE_MANAGER'),
('susan','ROLE_ADMIN');
- Add database support to Maven POM file
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- MySQL -->
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
- Create JDBC properties file
application.properties
#
# JDBC connection properties
#
spring.datasource.url=jdbc:mysql://localhost:3306/employee_directory
spring.datasource.username=springstudent
spring.datasource.password=springstudent
#
# Log JDBC SQL statements
#
# Only use this for dev/testing
# Do not use for production since it will log user names
logging.level.org.springframework.jdbc.core=TRACE
- Update Spring Security Configuration to use JDBC
@Configuration
public class DemoSecurityConfig {
//Inject data source, Auto-configured by Spring Boot
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
//Tell Spring Security to use JDBC authentication with our data source
return new JdbcUserDetailsManager(dataSource);
}
...
}
Spring Security Password Encryption
- use bcrypt as described in REST API Security.
Development Process
- Run SQL Script that contains encrypted passwords
INSERT INTO `users`
VALUES
(‘john','{bcrypt}$2a$10$qeS0HEh7urweMojsnwNAR.vcXJeXR1UcMRZ2WcGQl9YeuspUdgF.q',1),
('mary','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1),
('susan','{bcrypt}$2a$04$eFytJDGtjbThXa80FyOOBuFdK2IwjyWefYkMpiBEFlpBwDH.5PM0K',1);
- Modify DDL for password field, length should be 68
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` char(68) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
The password from db is NEVER decrypted
Because bcrypt is a one-way encryption algorithm
Spring Security Custom Tables
- Tell Spring how to query your custom tables
- Provide query to find user by user name
- Provide query to find authorities / roles by user name
Development Process
- Create our custom tables with SQL
CREATE TABLE `members` (
`user_id` varchar(50) NOT NULL,
`pw` char(68) NOT NULL,
`active` tinyint NOT NULL,
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
CREATE TABLE `roles` (
`user_id` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE KEY `authorities5_idx_1` (`user_id`, `role`),
CONSTRAINT `authorities5_ibfk_1` FOREIGN KEY (`user_id`) REFERENCES `members` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
- Update Spring Security Configuration
- Provide query to find user by user name
- Provide query to find authorities / roles by user name
@Configuration
public class DemoSecurityConfig {
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager theUserDetailsManager = new JdbcUserDetailsManager(dataSource);
theUserDetailsManager
.setUsersByUsernameQuery("select user_id, pw, active from members where user_id=?");
theUserDetailsManager
.setAuthoritiesByUsernameQuery("select user_id, role from roles where user_id=?");
return theUserDetailsManager;
}
...
}
Question mark “?”
Parameter value will be the user name from login