[스프링/Spring] 스프링 시큐리티(Spring Security) 가이드라인 따라하기

kindof

·

2021. 10. 30. 23:06

0. 소개

스프링 시큐리티는 스프링 기반의 어플리케이션의 보안(인증, 권한)을 담당하는 프레임워크입니다. 스프링 시큐리티가 없다면 자체적인 세션 체크나 리다이렉트 등의 기능 구현이 필요하죠.

 

스프링 시큐리티는 필터(filter) 기반으로 동작하기 때문에 spring MVC와 분리되어 관리 및 동작합니다. 

 

이번 포스팅에서는 아래 링크의 가이드라인을 따라서 스프링 시큐리티(Spring Security)를 실습해보고, 스프링 시큐리티에 대한 기본 개념을 익혀보고자 합니다.

 

Securing a Web Application

this guide is designed to get you productive as quickly as possible and using the latest Spring project releases and techniques as recommended by the Spring team

spring.io

 

그럼 시작해보겠습니다.

 

1. 시작하기

인텔리제이(Intellij)의 스프링 이니셜라이저를 통해 스프링 프로젝트를 생성합니다. 자바 11 버전을 선택, Gradle 프로젝트를 생성하고 아래와 같이 기본적인 의존성(thymeleaf, starter-web)만을 추가해주도록 하겠습니다.

 

[build.gradle]

plugins {
    id 'org.springframework.boot' version '2.5.6'
    id 'io.spring.dependency-management' version '1.0.11.RELEASE'
    id 'java'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
}

test {
    useJUnitPlatform()
}

 

그리고 이번 실습에서는 thymeleaf 템플릿 엔진을 사용하기 때문에 아래와 같이 기본적인 간단한 뷰(View)를 두 개 생성합니다.

[src/main/resources/templates/home.html]

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org" xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example</title>
    </head>
    <body>
        <h1>Welcome!</h1>
        
        <p>Click <a th:href="@{/hello}">here</a> to see a greeting.</p>
    </body>
</html>

 

[src/main/resources/templates/hello.html]

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1>Hello world!</h1>
    </body>
</html>

 

현재 웹 어플리케이션은 스프링 MVC를 기반으로 하기 때문에 뷰 컨트롤러(Veiw Controller)를 아래와 같이 만들어주도록 합니다. 

 

[src/main/java/com/example/springsecurity/MvcConfig.java]

package com.example.springsecurity;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    @Override
    public void addViewControllers(ViewControllerRegistry registry){
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
}

MvcConfig를 작성하게 되면, "/home"과 "/" 루트 모두 home 뷰를 보여주게 됩니다. 마찬가지로 "/hello", "/login" 역시 hello와 login 뷰로 등록이 됩니다.

 

지금까지 작성한 어플리케이션을 실행시켜보면 아래처럼 정말 간단한 뷰를 확인하실 수 있습니다. 

"/home", "/"
"/hello"

 

자, 그러면 이제 스프링 시큐리티를 적용해서 위의 어플리케이션에 어떤 기능을 구현할 수 있는지 알아보도록 하겠습니다.

 

 

2. 스프링 시큐리티 세팅하기

만약 현재 어플리케이션에서 인증되지 않은 사용자는 "/hello" 페이지에 접근할 수 없도록 만들려면 어떻게 해야 할까요? 현재는 어플리케이션에 어떤 기능도 없기 때문에 인증되지 않은 사용자를 필터링할 수 있는 방법이 존재하지 않습니다.

 

그런데 이 때, 스프링 시큐리티는 자동으로 모든 HTTP 엔드 포인트(endpoints)마다 기본적인 인증 절차를 통해 보안 기능을 마련하며, 개발자가 자유롭게 커스터마이징하여 스프링 시큐리티를 이용할 수 있도록 해줍니다.

 

자, 그럼 우선 스프링 시큐리티를 클래스패스에 추가하는 작업부터 진행해보겠습니다. build.gradle에서 아래와 같이 의존성을 추가해주도록 하겠습니다.

 

[build.gradle]

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.security:spring-security-test'
	testImplementation('org.springframework.boot:spring-boot-starter-test')
}

 

그리고 아래와 같이 security configuration 클래스를 작성해주도록 합니다. WebSecurityConfig.java는 보안이 설정된 페이지에 대해 인증된 사용자만 접근할 수 있도록 설정해줍니다.

package com.example.springsecurity;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception{
        http
                .authorizeRequests()
                    .antMatchers("/", "/home").permitAll()
                    .anyRequest().authenticated()
                    .and()
                .formLogin()
                    .loginPage("/login")
                    .permitAll()
                    .and()
                .logout()
                    .permitAll();
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        UserDetails user =
                User.withDefaultPasswordEncoder()
                        .username("user")
                        .password("password")
                        .roles("USER")
                        .build();

        return new InMemoryUserDetailsManager(user);
    }
}

1) @EnableWebSecurity 어노테이션은 스프링 스큐리티 설정들을 활성화하고 Spring MVC에 적용되어 사용될 수 있도록 합니다. 그리고 WebSecurityConfigurerAdapter를 상속받아 메서드를 오버라이딩합니다.

 

2) configure(HttpSecurity) 메서드는 각 URL 경로별로 보안을 설정합니다. 현재 "/"과 "/home" 경로는 모두에게 열려있고, 그 외의 경로(anyRequest())는 인증이 된(authenticated()) 사용자만 접근이 허용됩니다.

 

3) 따라서, 유저가 성공적으로 로그인하면 그 전에 인증을 요구했던 페이지로 리다이렉션이 됩니다. 그리고 여기서 loginPage()를 통해 커스텀 로그인 페이지("/login")를 만들 수 있습니다. 로그인 자체는 누구나 할 수 있어야 하기 때문에 로그인 페이지는 누구에게나 열려 있습니다.

 

4) userDetailService() 메서드는 In-Memory 상에서 한 명의 유저를 저장합니다. 유저 이름은 "user", 비밀번호는 "password", 역할은 "USER"로 설정되어 있습니다.

 

5) 자, 그럼 커스텀화 된 로그인 페이지를 이용하기 위해 아래와 같이 뷰를 작성해주도록 하겠습니다.

 

[src/main/resources/templates/login.html]

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Spring Security Example </title>
    </head>
    <body>
        <div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>
        <form th:action="@{/login}" method="post">
            <div><label> User Name : <input type="text" name="username"/> </label></div>
            <div><label> Password: <input type="password" name="password"/> </label></div>
            <div><input type="submit" value="Sign In"/></div>
        </form>
    </body>
</html>

타임리프 템플릿 엔진을 통해 폼을 작성했는데요. 해당 폼은 유저의 이름과 비밀번호를 POST를 통해 전송합니다. 그리고 위에서 설정한 것처럼 스프링 시큐리티는 이 요청을 중간에서 받아서 해당 유저의 인증을 처리하게 됩니다. 만약 유저가 인증에 실패하게 되면 "/login?error" 페이지로 리다이렉트되게 되고, 로그아웃을 할 때도 "/login?logout" 페이지로 리다이렉트되게 됩니다.

 

여기서 현재 로그인 한 사용자의 이름과 로그아웃 버튼을 보여주기 위해 처음에 작성했던 "hello.html" 파일을 아래와 같이 수정해주도록 하겠습니다.

 

[src/main/resources/templates/hello.html]

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="https://www.thymeleaf.org"
      xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity3">
    <head>
        <title>Hello World!</title>
    </head>
    <body>
        <h1 th:inline="text">Hello [[${#httpServletRequest.remoteUser}]]!</h1>
        <form th:action="@{/logout}" method="post">
            <input type="submit" value="Sign Out"/>
        </form>
    </body>
</html>

 

 

3. 어플리케이션 실행하기

지금까지 작성한 어플리케이션을 실행시켜보면 맨 처음에 localhost:8080 에서 홈화면은 아래와 같이 나타납니다. 로그인(인증)을 하지 않아도 모두에게나 공개되어 있도록 스프링 시큐리티 설정을 해두었기 때문이죠.

localhost:8080

 

여기서 'here' 버튼을 클릭하면 "/hello"로 넘어가야 하는데요. 그런데 해당 경로는 스프링 시큐리티에 의해 인증된 사용자만 접근이 가능하기 때문에 아래와 같이 로그인 화면으로 리다이렉트 되는 것을 볼 수 있습니다.

localhost:8080/login

 

그리고 위에서 등록해뒀던 (user, password)로 로그인을 시도하면 아래와 같이 정상적으로 "/hello"에 접근이 되고 유저의 이름(user)와 로그아웃 버튼이 나타는 것을 확인할 수 있습니다.

localhost:8080/hello

 

마지막으로 로그아웃을 하게 되면 아래와 같이 로그아웃을 했음을 알리는 문구('You have been logged out')이 나타나고, 다시 로그인을 하는 화면으로 리다이렉트 됩니다.

localhost:8080/login?logout

 

4. 나가면서

이렇게 이번 포스팅에서는 스프링 시큐리티 가이드라인을 따라서 실습을 진행해봤는데요. 사실 지금 혼자 진행하고 있는 프로젝트에서 스프링 시큐리티를 사용하고 있고, 기본적인 개념과 사용과 동작 방식을 이해하고 쓰기 위해 이런 실습을 따로 해봤습니다.

 

아마 다른 포스팅에서 스프링 시큐리티에 대해 몇 가지 글을 더 작성할 것 같습니다.

 

감사합니다.