4 분 소요

Spring MVC 기반 애플리케이션의 인증과 인가(Authorization or 권한 부여) 기능을 지원하는 보안 프레임워크로써, Spring MVC 기반 애플리케이션에 보안을 적용하기 위한 사실상의 표준.



Spring Security 보안 강화 기능

  • 다양한 유형(폼 로그인 인증, 토큰 기반 인증, OAuth 2 기반 인증, LDAP 인증)의 사용자 인증 기능
  • 애플리케이션 사용자의 역할에 따른 권한 레벨 적용
  • 애플리케이션에서 제공하는 리소스에 대한 접근 제어
  • 민감한 정보에 대한 데이터 암호화
  • SSL 적용
  • 일반적으로 알려진 웹 보안 공격 차단
  • SSO, 클라이언트 인증서 기반 인증, 메서드 보안, 접근 제어 목록 등




Spring Security 기본 구조


주의

공부 목적으로 작성된 코드이므로 여기서 사용된

`InMemoryUserDetailsManager()` 라는 구현체는 메모리상에서 UserDetail를 관리한다

이 방식은 테스트 환경 또는 데모 환경에서만 유용하게 사용할 수 있는 방식이다.


withDefaultPasswordEncoder() 메서드의 Deprecated는 Production 환경에서 인증을 위한

사용자 정보를 고정해서 사용하지 말라는 경고의 의미를 나타내고 있는 것이니

반드시 테스트 환경이나 데모 환경에서만 사용한다.


여기선 데이터베이스를 연동하지 않고,

Spring Security에서 지원하는 InMemory User를 사용하고 있다.


build.gradle

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:3.1.0'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.mapstruct:mapstruct:1.5.2.Final'
annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.2.Final'
implementation 'com.google.code.gson:gson'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-security'

// sec 태그 사용
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'



Spring Security Configuration 적용

package com.codestates.config;

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.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.provisioning.UserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfiguration {
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf().disable() // CSRF 공격 보호 비활성화
                .formLogin() // 폼 로그인 방식 지정
                .loginPage("/auths/login-form") // 로그인 페이지 경로
                .loginProcessingUrl("/process_login") // 로그인 인증 요청 URL
                .failureUrl("/auths/login-form?error") // 로그인 실패 화면
                .and() // 메서드 체인 형태 
                .logout() // 로그아웃 설정을 위한 LogoutConfigurer 리턴
                .logoutUrl("/logout") // request URL 지정
                .logoutSuccessUrl("/") // 리다이렉트 URL 지
                .and()
                .exceptionHandling().accessDeniedPage("/auths/access-denied") // 403
                .and()
                .authorizeHttpRequests(authorize -> authorize
                        .antMatchers("/orders/**").hasRole("ADMIN") // ADMIN Role만 접근
                        .antMatchers("/members/my-page").hasRole("USER") // USER Role만 접근
                        .antMatchers("/**").permitAll()); // 이외에 접근 허용

        return http.build();
    }
    @Bean
    public UserDetailsManager userDetailsManager(){
        UserDetails user =
                User.withDefaultPasswordEncoder() //password() 암호화
                        .username("kevin@gmail.com") // 아이디 
                        .password("1111") // 비번
                        .roles("USER") // 역할 지정
                        .build();

        UserDetails admin =
                User.withDefaultPasswordEncoder()
                        .username("admin@gmail.com")
                        .password("2222")
                        .roles("ADMIN")
                        .build();
        return new InMemoryUserDetailsManager(user,admin);
    }
}



login.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org"
      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
      layout:decorate="layouts/common-layout">
    <head>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
        <title>Hello Spring Security Coffee Shop</title>
    </head>
    <body>
        <div class="container" layout:fragment="content">
            <form action="/process_login" method="post">
                <!-- (1) 로그인 실패에 대한 메시지 표시 -->
                <div class="row alert alert-danger center" role="alert" th:if="${param.error != null}">
                    <div>로그인 인증에 실패했습니다.</div>
                </div>
                <div class="row">
                    <div class="col-xs-2">
                        <input type="email" name="username"  class="form-control" placeholder="Email" />
                    </div>
                </div>
                <div class="row" style="margin-top: 20px">
                    <div class="col-xs-2">
                        <input type="password" name="password"  class="form-control" placeholder="Password" />
                    </div>
                </div>

                <button class="btn btn-outline-secondary" style="margin-top: 20px">로그인</button>
            </form>
            <div style="margin-top: 20px">
                <a href="/members/register">회원가입</a>
            </div>
        </div>
    </body>
</html>



header.html

<html xmlns:th="http://www.thymeleaf.org"
      xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">
    <body>
        <div align="right" th:fragment="header">
            <a href="/members/register" class="text-decoration-none">회원가입</a> |
            <span sec:authorize="isAuthenticated()">
                <span sec:authorize="hasRole('USER')">
                    <a href="/members/my-page" class="text-decoration-none">마이페이지</a> |
                </span>
                <a href="/logout" class="text-decoration-none">로그아웃</a>
                <span th:text="${#authentication.name}">홍길동</span></span>

            <span sec:authorize="!isAuthenticated()">
                <a href="/auths/login-form" class="text-decoration-none">로그인</a>
            </span>
        </div>
    </body>
</html>




Spring Security 기본 구조 2 (회원가입 구현)


1. PasswordEncoder Bean 등록

  • PasswordEncoder는 Spring Security에서 제공하는 패스워드 암호화 기능 컴포넌트이다.
  • PasswordEncoder의 디폴트 암호화 알고리즘은 bcrypt 이다.


SecurityConfiguration 클래스에 추가

@Configuration
public class SecurityConfiguration {
	...
	...

	@Bean
	public PasswordEncoder passwordEncoder() {
	    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
	}
}

PasswordEncoderFactories.createDelegatingPasswordEncoder(); 를 통해

DelegatingPasswordEncoder를 먼저 생성함 (PasswordEncoder 구현 객체)



2. MemberService Bean 등록을 위한 JavaConfiguration 구성

MemberService 인터페이스 생성 (MemberService Bean 등록을 위해)

// 학습 편의상 인터페이스로 구현
public interface MemberService {
    Member createMember(Member member);
}

회원 가입 폼에서 전달받은 정보를 이용해 새로운 사용자를 추가하는 기능만 있으면 되므로

createMember() 하나의 구현체만 있으면 된다.



InMemoryMemberService 클래스 생성 (InMemory User 등록을 위해)

public class InMemoryMemberService implements MemberService {
    @Override
    public Member createMember(Member member) {
        return null;
    }
}



DBMemberService 클래스 생성 (데이터베이스에 User를 등록하기 위해)

@Transactional
public class DBMemberService implements MemberService {
    @Override
    public Member createMember(Member member) {
        return null;
    }
}



JavaConfiguration 수정

@Configuration
public class JavaConfiguration {
    @Bean
    public MemberService inMemoryMemberService(UserDetailsManager userDetailsManager,
                                               PasswordEncoder passwordEncoder) {
        return new InMemoryMemberService(userDetailsManager, passwordEncoder);
    }
}

데이터베이스 연동 없이 메모리에 Spring Security의 User를 등록해야 하므로 UserDetailsManager 객체 필요,

User 등록 시 패스워드를 암호화한 후에 등록해야 하므로 PasswordEncoder 객체 필요



3. InMemoryMemberService 클래스 구현

InMemoryMemberService 구현

public class InMemoryMemberService implements MemberService {
   private final UserDetailsManager userDetailsManager;
   private final PasswordEncoder passwordEncoder;

    public InMemoryMemberService(UserDetailsManager userDetailsManager, PasswordEncoder passwordEncoder) {
        this.userDetailsManager = userDetailsManager;
        this.passwordEncoder = passwordEncoder;
    }

    public Member createMember(Member member){
        // User의 권한 목록을 List<CrantedAuthority> 로 생성
        List<GrantedAuthority> authorities = createAuthorities(Member.MemberRole.ROLE_USER.name());
        String encryptedPassword = passwordEncoder.encode(member.getPassword());
        // User 정보를 UserDetails로 관
        UserDetails userDetails = new User(member.getEmail(), encryptedPassword, authorities);
        userDetailsManager.createUser(userDetails);
        return member;
    }

    // String... roles는 roles라는 이름의 String 배열을 나타낸다 (가변 인자 문법)
    private List<GrantedAuthority> createAuthorities(String... roles){
        // 생성자 파라미터로 해당 User의 Role을 전달하면서 SimpleGrantedAuthority 객체를 생성한 후, List<SimpleGrantedAuthority> 형태로 리턴
        return Arrays.stream(roles)
                .map(role -> new SimpleGrantedAuthority(role))
                .collect(Collectors.toList());
    }
}

여태껏 @Service 애너테이션을 사용해 서비스 클래스를 Bean으로 등록하는 방법을 사용했지만

여기서는 @Service를 사용하지 않고, javaConfiguration을 이용해 Bean을 등록하고 있다.



추가 학습

용어 정리


Principal (주체)
애플리케이션에서 작업을 수행할 수 있는 사용자, 디바이스 또는 시스템 등이 될 수 있으며,
일반적으로 인증 프로세스가 성공적으로 수행된 사용자의 계정 정보를 의미


Authentication (인증)
애플리케이션을 사용하는 사용자가 본인이 맞음을 증명하는 절차
Authentication을 정상적으로 수행하기 위해서는 사용자를 식별하기 위한 정보가 필요한데 이를 Credential (신원 증명 정보) 라고 함


Authorization (인가 또는 권한 부여)
Authentication이 정상적으로 수행된 사용자에게 하나 이상의 권한을 부여하여 특정 애플리케이션의 특정 리소스에 접근할 수 있게 허가하는 과정


Access Control (접근 제어)
사용자가 애플리케이션의 리소스에 접근하는 행위를 제어하는 것



부모 클래스나 인터페이스를 재정의 하는 이유

  1. 기능 확장
    부모 클래스나 인터페이스에서 제공하는 기능에 추가적인 기능을 추가하여 확장할 수 있다.
    이렇게 하면 기존 기능을 유지하면서 새로운 기능을 추가할 수 있다.


  1. 기능 변경
    부모 클래스나 인터페이스에서 제공하는 기능을 변경해야 할 때, 해당 기능을 재정의하여 변경할 수 있다.


  1. 기능 제거
    부모 클래스나 인터페이스에서 제공하는 기능 중 필요 없거나 사용하지 않는 기능이 있다면, 해당 기능을 제거하기 위해 재정의 한다.


  1. 다형성 구현
    부모 클래스나 인터페이스를 재정의하여 다형성을 구현할 수 있다.
    이렇게 하면 같은 인터페이스나 부모 클래스를 구현하는 여러 클래스가 다른 동작을 수행할 수 있다.


  1. 구현 강제
    인터페이스를 구현하는 클래스는 인터페이스에서 정의한 모든 메서드를 구현해야 한다.
    따라서 인터페이스를 구현할 때 해당 메서드를 재정의하여 구현을 강제할 수 있다.



파일 경로

카테고리: ,

업데이트:

댓글남기기