본문 바로가기
[개발] Spring Framework/Spring Security

Spring Security (1) 대략적인 내용

by 비어원 2020. 8. 9.
728x90

회원관리, 로그인, 로그아웃, 인증, 권한 인가 등등.. '회원' 이라는 것이 들어있는 웹 애플리케이션에서는 시큐리티 로직이 반드시 필요하다. 이번 글에서는 Spring에서 제공하는 Spring Security에 대한 내용들을 간단하게 정리하고, 실습하는 내용을 정리해보겠다. 거의 모든 내용들은 공식문서에서 찾으려고 애썼다. (공식문서와 친해지기 위해서..)

 

인증과 인가

Spring Security에서 제공하는 기능을 크게 두 가지로 나누면 인증인가가 있다. 일단 두 용어에 대해 간단히 정리해보겠다.

 

인증
인증이란, 요청을 한 사용자가 누구인지 확인하는 것이다. 예를 들어, 회원 전용 게시판에 글을 쓰는 기능을 누가 요청을 할 때, 요청한 사용자가 비회원이면 글을 쓰지 못하도록 막아야 한다. 이 때 인증 단계에서 회원인지 비회원인지 파악한다.

 

인가
인가는 해당 사용자가 해당 요청을 할 권한이 있는지 확인하는 것이다. 예를 들어, VIP 회원 전용 게시판이 있다고 할 때, 일반 회원이 VIP 회원 전용 게시판을 읽는 것을 막아야 한다. 이 때는 인가 단계에서 회원이 VIP 회원인지 확인한 후 게시판 내용을 보여줄지 결정한다. (물론 비회원은 인증 단계에서 막힌다.)

Spring Security에서는 인증과 인가에 대한 로직을 분리한 구조로 되어있고, 인증 절차를 거친 후 인가 절차를 거치도록 되어있다.

 

맛보기

먼저, 스프링 시큐리티는 내용이 복잡하기 때문에 아주 최소한의 로직이 들어있는 애플리케이션을 구현하는 것 부터 시작해서 흐름을 파악한 뒤, 인증과 인가에 대해 파는 방향으로 진행할 예정이다. 이해가 되지 않더라도 만들어보자. 코드는 공식문서를 참고하였다.

 

애플리케이션 컨셉

스프링 시큐리티가 어떤건지 대강 알아보기 위해서 몇 가지 기능을 구현할 것이다.

  1. 웹페이지 화면

    • 홈 페이지
    • 로그인 페이지
    • 로그인 후 회원만이 볼 수 있는 페이지
  2. 인증 설정

 

의존성 관리

먼저 의존성은 SpringBoot2, 타임리프, Spring Security를 사용할 것이다.

 

gradle

import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
    id("org.springframework.boot") version "2.3.0.RELEASE"
    id("io.spring.dependency-management") version "1.0.9.RELEASE"
    war
    kotlin("jvm") version "1.3.72"
    kotlin("plugin.spring") version "1.3.72"
}

group = "com.example"
version = "0.0.1-SNAPSHOT"
java.sourceCompatibility = JavaVersion.VERSION_11

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
    implementation("org.springframework.boot:spring-boot-starter-security")
    implementation("org.springframework.boot:spring-boot-starter-web")
    implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
    implementation("org.jetbrains.kotlin:kotlin-reflect")
    implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
    developmentOnly("org.springframework.boot:spring-boot-devtools")
    providedRuntime("org.springframework.boot:spring-boot-starter-tomcat")
    testImplementation("org.springframework.boot:spring-boot-starter-test") {
        exclude(group = "org.junit.vintage", module = "junit-vintage-engine")
    }
    testImplementation("org.springframework.security:spring-security-test")
}

tasks.withType<Test> {
    useJUnitPlatform()
}

tasks.withType<KotlinCompile> {
    kotlinOptions {
        freeCompilerArgs = listOf("-Xjsr305=strict")
        jvmTarget = "1.8"
    }
}

 

간단한 페이지 만들기

위에서 언급한 대로, 1.홈 페이지 2. 로그인 페이지 3.회원만이 사용할 수 있는 페이지를 먼저 만들 것이다.

프로젝트 구성은 다음과 같다.

먼저 화면을 만들어보자.

 

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>

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>

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>

그 다음에는 요청URL과 화면을 매핑해주는 설정을 해야한다.

 

MvcConfig.kt

package com.example.security.configuration

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

@Configuration
class MvcConfig: WebMvcConfigurer {
    /**
     * URL에 대응되는 view를 매핑시켜주는 역할을 한다.
     */
    override fun addViewControllers(registry: ViewControllerRegistry) {
        registry.apply {
            addViewController("/home").setViewName("home")
            addViewController("/").setViewName("home")
            addViewController("/hello").setViewName("hello")
            addViewController("/login").setViewName("login")
        }
    }
}

이 클래스는 간단히 /home과 /로 들어오는 URL에 대해서는 home.html을 리턴하고 /hello로 들어오는 URL에 대해서는 hello.html, /login으로 들어오는 URL에 대해서는 login.html을 리턴하도록 설정한다.

 

인증 설정

위의 단계까지 했다면, 등록되어있는 URL에 대해서 모든 사용자가 접근할 수 있다. 하지만, /hello는 로그인을 한 사용자만 볼 수 있도록 제한해야 한다고 가정해보자. 그러면 /hello 를 요청한 사용자에 대해서는 인증 로직을 거쳐야 한다. 이렇게 URL마다 인증 로직을 거쳐야 하는지에 대한 설정은 다음과 같이 할 수 있다.

 

WebSecuityConfig.kt

package com.example.security.configuration

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.UserDetailsService
import org.springframework.security.provisioning.InMemoryUserDetailsManager

@Configuration
@EnableWebSecurity
class WebSecurityConfig: WebSecurityConfigurerAdapter() {
    /**
     *  URL마다 보안 설정을 할 것인지 말 것인지 정의다.
     */
    override fun configure(http: HttpSecurity) {
        http.authorizeRequests()
            .antMatchers("/", "/home").permitAll()   // {/, /home} URL은 인증 과정을 거치지 않는다.
            .anyRequest().authenticated()   // 그 외 나머지 요청에 대해서는 인증 과정을 거친다.
            .and()
            .formLogin()
            .loginPage("/login") // 인증에 실패하면 이 페이지로 redirect 된다. (이 페이지도 인증 과정을 거치지 않는다.)
            .permitAll()
            .and()
            .logout()
            .permitAll()
    }

    /**
     * 로그인을 한 유저인지 검증하는 서비스.
     */
    @Bean
    override fun userDetailsService(): UserDetailsService {
        return InMemoryUserDetailsManager(
            User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build()
        )
    }
}
  • configure() 메소드는 요청 URL마다 인증 로직을 거칠 것인지 말 것인지 설정하는 메서드이다.
    • antMatcher로 같은 인증 로직을 사용할 요청 그룹을 설정할 수 있다.
    • permitAll()은 인증 절차를 거치지 않는다는 의미이다.
    • authenticated()는 해당 요청 그룹은 인증 과정을 거친다는 의미. (인증 과정이 여러가지로 나뉠 수 있는지는 아직 알아보지 못했음)
    • formLogin().loginPage(): 폼 형태의 로그인 페이지를 설정한다. 다른 URL에 접근하다가 인증에서 막힐 때 이 페이지로 redirect된다.
    • logout(): 로그아웃 프로세스에 대한 설정을 한다. (기본 url은 /logout, 사용자의 쿠키와 세션 정보를 다 지우고 로그인 페이지로 redirect되는 역할이 포함된다.)
  • userDetailsService(): 인증 로직에서 사용되는 UserDetailsService Bean을 등록한다.
    • UserDetailsSerivce를 이용하여 인증로직을 구현한다.
    • UserDetailisService에 로그인을 한 유저를 저장하고, 인증이 필요한 요청이 들어왔을 경우 사용자가 UserDetailsService에 저장되어있는 유저인지 확인함으로써 인증 성공/실패를 결정한다.
    • 해당 프로젝트에서는 user / password 로 로그인 했을 때만 로그인 인정

 

작동해보기

다 만들었으면 재생 버튼을 클릭해서 웹 서버를 띄워보자. 홈페이지는 이렇게 되어있는데

(디자인은 맘에 안든다.)

here를 클릭하면 로그인 화면이 등장한다.

user / password 외의 다른 것을 치고 로그인을 시도하면 실패를 하게 된다. UserDetailsService에서 저장되어있는 회원 정보가 user / password 뿐이기 때문이다.

user / password를 치고 로그인을 시도하면 성공하게 되고 다음과 같은 화면이 뜰 것이다.

여기서 Sign Out을 눌러보면 로그아웃이 되면서 로그인 페이지로 건너뛰게 된다.

 

마무리 하며

Spring Security 공식 홈페이지에서 제공되는 튜토리얼 과정을 거쳤다. 튜토리얼 과정에서는 단지 "스프링 시큐리티가 이런 역할을 하는구나" 정도를 익힐 수 있는 것 같다고 생각한다. 로직이 대략적으로 어떻게 흘러가는지 파악하기에는 도움이 되지만, 당장 이거만 가지고 프로젝트에 적용을 할 수는 없다고 생각한다. 이유를 정리하자면 대략 이렇다.

  • 인증 로직밖에 없다. (인가에 대한 내용은 설명되어있지 않다.)
  • 인증 로직을 InMemory로 구현하였다.
    • 회원 정보를 InMemory로 관리한다면.. 서버가 죽으면 다 날아가기 때문..

그래서 그 다음 목표는 이런 것을 알아볼 것이다.

  1. 인증과 인가에 대한 자세한 로직을 공부하고, 인증 로직을 어떻게 커스터마이징 할 것인지
  2. 권한 인가는 어떻게 구현 하는지
  3. 인가는 어떻게 커스터마이징을 하는지
728x90

'[개발] Spring Framework > Spring Security' 카테고리의 다른 글

Spring Security (2) 인증  (0) 2020.08.09

댓글