Spring boot security blocks unauthorized access to the endpoints
List of Contents
Dependencies
▶ Security
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
Test
After adding the dependency run the app then you will the password generated in the console
The browser will ask for the username and password if you try to access the endpoint. The default user name is 'user'
We can change the default user name and the password in the application.property using the property below
spring.security.user.name=<name>
spring.security.user.password=<password>
Adding Users
To create users, we can either use the application (in memory) or the database (JDBC). Config class' setup overrides the application.property setup
Creating a Config Class
Create a package and add a configuration class with the annotation shown below
@Configuration
In Memory
In the config file add a method to create users and another authorization method. {noop} in front of the password is one of the IDs to differentiate the type of password stored. {noop} stores the password as a plain text
package com.example.employeepractice.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
public class SecurityConfig {
@Bean
public InMemoryUserDetailsManager inMemoryUserDetailsManager() {
UserDetails john = User.builder()
.username("john")
.password("{noop}1234")
.roles("EMPLOYEE")
.build();
UserDetails tom = User.builder()
.username("tom")
.password("{noop}1234")
.roles("EMPLOYEE", "MANAGER")
.build();
UserDetails jack = User.builder()
.username("jack")
.password("{noop}1234")
.roles("EMPLOYEE", "MANAGER", "ADMIN")
.build();
return new InMemoryUserDetailsManager(john, tom, jack);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(config ->
config
.requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.PUT, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
);
http.httpBasic(Customizer.withDefaults());
http.csrf(csrf -> csrf.disable());
return http.build();
}
}
Using DB
Next, let's see how we can get users from a database. First, the Link database
How to Create Bcrypt Password
Go to this site https://www.bcryptcalculator.com/
Using the Default Tables
Spring uses a defined set of names for the tables and fields so we have to use the same name for this to work. We have to encrypt the password that will be stored in the DB using {bcrypt} <hashedPassword>
DROP TABLE IF EXISTS `authorities`;
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` (
`username` varchar(50) NOT NULL,
`password` varchar(68) NOT NULL,
`enabled` tinyint NOT NULL,
PRIMARY KEY(`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `users`
VALUES
('john', '{bcrypt}$2a$10$VlimMGxj5O5/VM64Pvq0Vu80cK9MW/lZKagKAzHNPV3IOXZHvawb6', 1),
('tom', '{bcrypt}$2a$10$PQxxeKzFuHrZvXpgZJijBOpIoNVJK2jF8FPu410Q1TyTvA15VZm6.', 1),
('jack', '{bcrypt}$2a$10$H5ruiZZJ3je6sqV/Iu2EsezmSH2UH5nhFtvQPo2I4RLdoeNVK96/2', 1);
CREATE TABLE `authorities` (
`username` varchar(50) NOT NULL,
`authority` varchar(50) NOT NULL,
UNIQUE KEY `authorities_idx_1` (`username`, `authority`),
CONSTRAINT `authorities_idfk_1` FOREIGN KEY (`username`) REFERENCES `users` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `authorities`
VALUES
('john', 'ROLE_EMPLOYEE'),
('tom', 'ROLE_EMPLOYEE'),
('tom', 'ROLE_MANAGER'),
('jack', 'ROLE_MANAGER'),
('jack', 'ROLE_EMPLOYEE'),
('jack', 'ROLE_ADMIN');
In the config file, add the code shown below
package com.example.employeepractice.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
return new JdbcUserDetailsManager(dataSource);
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(config ->
config
.requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.PUT, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
);
http.httpBasic(Customizer.withDefaults());
http.csrf(csrf -> csrf.disable());
return http.build();
}
}
Using the Custom Tables
As we have seen using the default option is convenient but lacks options. To use custom table names and field names additional settings are needed
Create a custom table using the query below
DROP TABLE IF EXISTS `roles`;
DROP TABLE IF EXISTS `members`;
CREATE TABLE `members` (
`id` varchar(50) NOT NULL,
`pw` varchar(68) NOT NULL,
`active` tinyint NOT NULL,
PRIMARY KEY(`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `members`
VALUES
('john', '{bcrypt}$2a$10$VlimMGxj5O5/VM64Pvq0Vu80cK9MW/lZKagKAzHNPV3IOXZHvawb6', 1),
('tom', '{bcrypt}$2a$10$PQxxeKzFuHrZvXpgZJijBOpIoNVJK2jF8FPu410Q1TyTvA15VZm6.', 1),
('jack', '{bcrypt}$2a$10$H5ruiZZJ3je6sqV/Iu2EsezmSH2UH5nhFtvQPo2I4RLdoeNVK96/2', 1);
CREATE TABLE `roles` (
`id` varchar(50) NOT NULL,
`role` varchar(50) NOT NULL,
UNIQUE KEY `authorities_idx_2` (`id`, `role`),
CONSTRAINT `authorities_idfk_2` FOREIGN KEY (`id`) REFERENCES `members` (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;
INSERT INTO `roles`
VALUES
('john', 'ROLE_EMPLOYEE'),
('tom', 'ROLE_EMPLOYEE'),
('tom', 'ROLE_MANAGER'),
('jack', 'ROLE_MANAGER'),
('jack', 'ROLE_EMPLOYEE'),
('jack', 'ROLE_ADMIN');
In the UserDetailManager Add the methods shown below to change the default settings. '?' will be replaced with the incoming values
jdbcUserDetailsManager.setUsersByUsernameQuery(
"select id, pw, active from members where id=?"
);
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
"select id, roles from where id=?"
);
package com.example.employeepractice.security;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.JdbcUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import javax.sql.DataSource;
@Configuration
public class SecurityConfig {
@Bean
public UserDetailsManager userDetailsManager(DataSource dataSource) {
JdbcUserDetailsManager jdbcUserDetailsManager = new JdbcUserDetailsManager(dataSource);
jdbcUserDetailsManager.setUsersByUsernameQuery(
"select id, pw, active from members where id=?"
);
jdbcUserDetailsManager.setAuthoritiesByUsernameQuery(
"select id, role from where id=?"
);
return jdbcUserDetailsManager;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(config ->
config
.requestMatchers(HttpMethod.GET, "/api/employees").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.GET, "/api/employees/**").hasRole("EMPLOYEE")
.requestMatchers(HttpMethod.POST, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.PUT, "/api/employees").hasRole("MANAGER")
.requestMatchers(HttpMethod.DELETE, "/api/employees/**").hasRole("ADMIN")
);
http.httpBasic(Customizer.withDefaults());
http.csrf(csrf -> csrf.disable());
return http.build();
}
}
Debug
Add the property below to the application.property to show more detailed error messages
logging.level.org.springframework.security=DEBUG
Login Page
Add .formLogin to the configuration file
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(config ->
config
.requestMatchers("/").hasRole("EMP")
.requestMatchers("/manage/**").hasRole("MAN")
.requestMatchers("/system/**").hasRole("ADM")
.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/loginProcess")
.permitAll()
)
return httpSecurity.build();
}
Create an endpoint in the controller
@GetMapping("/login")
public String login() {
return "login";
}
Create a template. Note that whenever there is an error 'error' is appended to the URL as a parameter. We can use that parameter to show error messages
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.com">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<form action="#" th:action="@{/loginProcess}" method="POST">
<!--error message-->
<div th:if="${param.error}">
<p>Invalid</p>
</div>
<p>
<input type="text" name="username"/>
</p>
<p>
<input type="password" name="password"/>
</p>
<input type="submit" value="Login"/>
</form>
</body>
</html>
Logout
Add .logout to the configuration file
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(config ->
config
.requestMatchers("/").hasRole("EMP")
.requestMatchers("/manage/**").hasRole("MAN")
.requestMatchers("/system/**").hasRole("ADM")
.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/loginProcess")
.permitAll()
)
.logout(logout -> logout.permitAll()
)
return httpSecurity.build();
}
Add a form with below configurations (Other things such as the endpoint are provided by the Spring automatically)
<form action="#" th:action="@{/logout}" method="POST">
<button type="submit">Logout</button>
</form>
Error Handling
Add . exceptionHandling to the configuration file
@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeHttpRequests(config ->
config
.requestMatchers("/").hasRole("EMP")
.requestMatchers("/manage/**").hasRole("MAN")
.requestMatchers("/system/**").hasRole("ADM")
.anyRequest().authenticated()
).formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/loginProcess")
.permitAll()
)
.logout(logout -> logout.permitAll()
)
.exceptionHandling(config -> config.accessDeniedPage("/access-denied")
);
return httpSecurity.build();
}
Add an endpoint to the controller
@GetMapping("/access-denied")
public String accessDenied() {
return "access-denied";
}
Create a template
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.com">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h3>Access Denied</h3>
<a th:href="@{/}">Back</a>
</body>
</html>
In this writing, we have seen the Spring boot starter actuator.
References
Spring Security Reference
The authenticator is also responsible for retrieving any required user attributes. This is because the permissions on the attributes may depend on the type of authentication being used. For example, if binding as the user, it may be necessary to read them
docs.spring.io
▶ bcrypt
Bcrypt Calculator
www.bcryptcalculator.com
'Backend > Java' 카테고리의 다른 글
Spring Boot Form (10) | 2023.10.28 |
---|---|
Spring Beans - Dependency Injection (1) | 2023.10.07 |
Spring Boot Actuator (0) | 2023.10.01 |
Data Persistency Tools (JPA, Hybernate, Mybatis) (3) | 2023.09.09 |
Spring Boot View Template Tools (0) | 2023.09.02 |