본문 바로가기

Backend/Java

Spring Boot Security

반응형

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

 

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

 

Bcrypt Calculator

 

www.bcryptcalculator.com

 

 

728x90
반응형

'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