IT/Back-end

[Spring] 웹 어플리케이션 보안

omaeng 2020. 4. 1. 17:55

이번 포스팅은 스프링 시큐리티의 보호를 받는 웹 어플리케이션 구축에 대해서 말씀드리겠습니다.

 

앞으로 우리가 구축하게될 웹 어플리케이션은 Spring MVC인데, 회원 목록에 있는 사용자만 선별하여 로그인 시키도록 하겠습니다.

 

준비사항


  • 약 15분의 시간
  • 선호하는 애디터나 IDE
  • JDK 1.8 이상
  • gradle 4이상이나 maven 3.2이상

 

Spring Initializr


https://start.spring.io/

불러오는 중입니다...

스프링 이니셜라이저로 접속해 프로젝트를 생성 하겠습니다.

 

프로젝트는 Maven이나 gradle 상광없으니 편하신 것으로 선택 해주세요.

 

Java와 Boot 버전 2.2.6을 선택해주시고(default를 선택해주시면 됩니다. Spring측에서 주장하는 가장 안정적인 버전 입니다.)

 

그룹은 그대로 두시고, artifact를 securing-web으로 작성해 주세요.

마지막으로 의존성(dependency)는 Spring Web과 Thymleaf를 추가해 주시고 generate하시면

모든 요소가 추가된 프로젝트가 다운로드됩니다.

 

 

Maven을 사용하고 있으시다면, pom.xml에 있는 내용이 다음과 같은지 확인해 주세요

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.2.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>securing-web</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>securing-web</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

 

Gradle을 사용하고 있으시다면 build.gradle이 다음과 같은지 확인해 주세요.

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

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

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	testImplementation('org.springframework.boot:spring-boot-starter-test') {
		exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
	}
}

test {
	useJUnitPlatform()
}

 

웹 어플리케이션 만들기(보안적용X)


웹 어플리케이션에 보안을 적용하기 전에 웹 어플리케이션을 먼저 만들어야겠죠?

 

3개의 페이지로 구성된 간단한 웹 어플리케이션을 만들어 보도록 하겠습니다. 

 

일단 먼저 generate한 프로젝트의 압축을 풀어 주시구요,

 

vscode에서 프로젝트를 열어줍니다.

 

src 이하의 프로젝트 구조는 다음과 같습니다. 

우리는 \src\main\resources\templates 에 페이지와 src\main\java\com\example\demo에 클래스를 구현 할 것입니다.

 

 

먼저 \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>

그 다음으로 같은 위치에 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>

위에 작성된 두 페이지는 Thymeleaf template로 정의되었습니다. 

 

 

여기서 Thymeleaf template란?

 

Thymeleaf template


  • Thymeleaf는 스프링 부트가 자동 설정을 지원한느 웹 템플릿 엔진입니다. html문서에 html5 문법으로 서버족 로직을 수행하고 적용시킬 수 있습니다.
  • html 디자인에 전혀 영향을 미치지 않고 웹 템플릿 엔진을 통해 html을 생성할 수 있습니다.
  • 독자적으로 html을 생성하기 때문에 테스트 시 렌더링 결과를 확인하기 좋습니다.

 

html페이지를 보시면 

<a th:href="@{/hello}">

이런 구문 같은 경우 Context-relative URL를 사용해서 서버내 hello 위치로 이동하는 기능을 가지고 있습니다. 

 

html을 작성할때 

xmlns:th="https://www.thymeleaf.org"

위와 같이 명시해줘야 Thymeleaf가 알맞게 기능을 수행할 수 있습니다.

 

웹 애플리케이션은 스프링 MVC를 기반으로 합니다. 따라서 이러한 템플릿을 노출하도록 Spring MVC를 구성하고 뷰 컨트롤러를 설정해야 합니다. 

package com.example.securingweb;

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 {

	public void addViewControllers(ViewControllerRegistry registry) {
		registry.addViewController("/home").setViewName("home");
		registry.addViewController("/").setViewName("home");
		registry.addViewController("/hello").setViewName("hello");
		registry.addViewController("/login").setViewName("login");
	}

}

여기서 클래스에 @configuration 어노테이션이 붙어 있으면 스프링은 해당 클래스를 Java config로 간주합니다.

 

addViewControllers() 메서드(WebMvcConfigurer에서 동일한 이름의 메서드를 오버라이딩함)는 4개의 뷰 컨트롤러를 추가합니다. 뷰 컨트롤러 중 두 개는 이름이 home인 뷰(home.html에서 정의)를 참조하고, 다른 하나는 hello라는 이름의 뷰를 참조합니다(Hello.html에서 정의). 네 번째 뷰 컨트롤러는 로그인이라는 다른 뷰를 참조합니다. 지금은 "application run"을 하면 로그인하지 않고도 애플리케이션을 실행할 수 있습니다.

 

이제 보안이 적용되지 않은 웹 응용 프로그램에 보안을 추가해 보겠습니다.

 

gradle 사용자께서는 다음 내용을 추가해 주세요.

implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-test'

 

maven 사용자는 다음 내용을 추가해 주세요.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.security</groupId>
  <artifactId>spring-security-test</artifactId>
  <scope>test</scope>
</dependency>

이제 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>

이 Thymleaf 템플릿은 username과 password를 캡처한 양식을 제공하여 /login에 post합니다. Spring Security는 구성된 대로 해당 요청을 가로채고 사용자를 인증하는 필터를 제공합니다. 사용자가 인증에 실패하면 페이지가 /login?error로 리디렉션되고 페이지에 적절한 오류 메시지가 표시됩니다. 로그아웃을 성공하면, 당신의 애플리케이션이 /login?logout으로 전송되고, 당신의 페이지에 적절한 성공 메시지가 표시됩니다.

 

마지막으로 사용자에게 최근 username과 sigin out을 제공해줘야 합니다. 그렇게 하려면 hello.html을 조금 수정해야 합니다. 로그인한 사용자의 이름과 sign out 을 만들어 줍니다.

<!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>

우리는 Spring Security와 HttpServletRequest#getRemoteUser()의 통합을 이용하여 사용자 이름을 표시했습니다. "Sign Out" 양식은 /logout에 POST를 제출하는 방식입니다. 로그아웃에 성공하면 사용자를 /login?logout으로 리디렉션합니다.

 

마지막으로 로그인 페이지라는 명시와 유저의 정보를 입력하겠습니다.

package com.example.securingweb;

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);
    }
}

이제 프로젝트를 실행해 봅시다.

 

gradle spring-boot:run

실행하자마자 화면입니다.

here을 클릭한 화면입니다. User/password를 치면

hello 화면이 나옵니다.

로그아웃후 화면 사실은 같은 login.html 화면이지만

	<div th:if="${param.error}">
            Invalid username and password.
        </div>
        <div th:if="${param.logout}">
            You have been logged out.
        </div>

Thymeleaf의 if를 통해 로그 아웃이 된 것을 사용자에게 알려줍니다.