Gihak111 Navbar

인증과 인가

스프링 서큐리티는 인증, 인가를 우한 옵션을 HTTP 방화벽, 필터체인, IETF와 W3C 표준의관범위한 사용 등등 의 메커니즘을 결합해 애플리케이션의 보안성을 높인다.

인증: 사실, 또는 진짜를 보여주는 행위
누군가가 자신이 주장하는 사람임을 증명하기
용자가 누구인지 확인하는 과정
인가: 권한 부여: 더 많은 정보에 접근할 권한을 부여
누군가가 특정 리소스나 작업에 접근할 수 있는지 확인하기
인증된 사용자가 애플리케이션의 특정 리소스에 접근할 수 있는 권한을 가지고 있는지 확인하는 과정

스프링 시큐리티 의존성 추가

스프링 프레임워크의 보안 모듈로, 애플리케이션 보안을 간편하게 구현할 수 있다.
pom.xml에 의존성을 추가하는 것으로 구현할 수 있다.
인증과 인가를 간편하게 구현할 수 있다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security</groupId>
    <artifactId>spring-security-oauth2-jose</artifactId>
</dependency>

각각의 의존성은 다음과 같다.
spring-boot-starter-security
스프링 시큐리티의 기본 기능을 제공. 인증과 인가를 위한 기본 설정과 필터를 자동으로 추가해 준다.

spring-security-oauth2-client
OAuth2 클라이언트 기능을 제공. 이를 통해 애플리케이션이 외부 OAuth2 제공자(구글, 페이스북)를 사용하여 사용자를 인증할 수 있다.

spring-security-oauth2-jose
이 의존성은 JSON Web Token(JWT)과 관련된 기능을 제공. OAuth2와 함께 사용하여 토큰 기반 인증을 구현할 때 유용하다.
필터 체인을 통해 작동. HTTP 요청이 들어오면, 이 필터 체인을 통해 요청을 검사하고 필요한 인증과 인가 절차를 수행한다.

보안 설정

위 의존성을 활용해서 코드를 작성해 보자.
SecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

// @Configuration: 스프링 컨텍스트에 이 클래스가 설정 클래스임을 알리는 어노테이션
@Configuration
// @EnableWebSecurity: 스프링 시큐리티의 웹 보안 지원을 활성화하는 어노테이션
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 사용자 인증을 설정하는 메소드
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 메모리 기반 인증을 설정
        auth.inMemoryAuthentication()
            // 사용자 이름이 'user'인 사용자 추가
            .withUser("user")
            // 비밀번호 'password'를 BCrypt 해시로 암호화
            .password(passwordEncoder().encode("password"))
            // 사용자의 역할을 'USER'로 설정
            .roles("USER");
    }

    // HTTP 보안을 설정하는 메소드
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 모든 요청은 인증을 요구하도록 설정
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            // 기본 로그인 폼을 사용하도록 설정
            .formLogin()
                .permitAll() // 로그인 페이지는 인증 없이 접근 가능
                .and()
            // 로그아웃을 허용하도록 설정
            .logout()
                .permitAll(); // 로그아웃도 인증 없이 접근 가능
    }

    // 비밀번호를 암호화하기 위한 PasswordEncoder 빈을 생성
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

위 기능으로 user와 password를 사용하여 로그인을 구현할 수 있다.

HTTP 방화벽

웹 애플리케이션을 보호하기 위해 사용되는 방화벽.
스프링 시큐리티는 기본적으로 요청을 검사하여 XSS(Cross-Site Scripting) 및 SQL 인젝션 공격을 방지한다.

import org.springframework.security.web.firewall.HttpFirewall;
import org.springframework.security.web.firewall.StrictHttpFirewall;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

// @Configuration 애너테이션은 이 클래스가 하나 이상의 @Bean 메서드를 포함하고 있음을 나타내며,
// 스프링 컨테이너에서 이 클래스를 구성 클래스로 인식하게 한다.
@Configuration
public class FirewallConfig {

    // @Bean 애너테이션은 이 메서드가 반환하는 객체가 스프링 컨테이너에 의해 관리되는 빈(Bean)으로 등록된다는 것을 나타낸다.
    @Bean
    public HttpFirewall httpFirewall() {
        // StrictHttpFirewall 클래스의 인스턴스를 생성합니다. 이는 기본적으로 엄격한 HTTP 방화벽 정책을 적용한다.
        StrictHttpFirewall firewall = new StrictHttpFirewall();
        
        // URL에서 인코딩된 슬래시('/')를 허용하도록 설정합니다. 기본적으로는 허용되지 않는다.
        firewall.setAllowUrlEncodedSlash(true);
        
        // 설정된 StrictHttpFirewall 객체를 반환하여, 스프링 컨테이너에서 빈으로 관리하도록 한다.
        return firewall;
    }
}

StrictHttpFirewall을 사용하여 기본적으로 엄격한 HTTP 요청 검사를 수행하지만,
URL에 인코딩된 슬래시를 허용하도록 설정한다.

보안 필터 체인

위에서 사용한 보안 필터 체인 이다.
애플리케이션의 HTTP 요청 및 응답을 처리하는 일련의 필터들을 체인 형태로 구성한 것이며,
필터들은 스프링 시큐리티에서 제공하는 기능을 이용하여 요청이 애플리케이션의 컨트롤러로 전달되기 전에 다양한 보안 검사를 수행한다.
각 필터는 특정한 보안 기능을 담당(인증, 권한 부여, 세션 관리, CSRF 등)

보안 설정에서 사용한 코드를 재활용 해서 어떤 필더가 어떻게 구서오디어 이쓴ㄴ지 알아보자. SecurityConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

// @Configuration: 스프링 컨텍스트에 이 클래스가 설정 클래스임을 알리는 어노테이션
@Configuration
// @EnableWebSecurity: 스프링 시큐리티의 웹 보안 지원을 활성화하는 어노테이션
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    // 사용자 인증을 설정하는 메소드
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 메모리 기반 인증을 설정
        auth.inMemoryAuthentication()
            // 사용자 이름이 'user'인 사용자 추가
            .withUser("user")
            // 비밀번호 'password'를 BCrypt 해시로 암호화
            .password(passwordEncoder().encode("password"))
            // 사용자의 역할을 'USER'로 설정
            .roles("USER");
    }

    // HTTP 보안을 설정하는 메소드
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            // 모든 요청은 인증을 요구하도록 설정
            .authorizeRequests()
                .anyRequest().authenticated()
                .and()
            // 기본 로그인 폼을 사용하도록 설정
            .formLogin()
                .permitAll() // 로그인 페이지는 인증 없이 접근 가능
                .and()
            // 로그아웃을 허용하도록 설정
            .logout()
                .permitAll(); // 로그아웃도 인증 없이 접근 가능
    }

    // 비밀번호를 암호화하기 위한 PasswordEncoder 빈을 생성
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

필터 체인의 동작 과정
요청 초기화: SecurityContextPersistenceFilter가 요청 시작 시 SecurityContext를 복원. Spring Security의 기본 설정으로 SecurityContextPersistenceFilter는 요청의 SecurityContext를 설정. HttpSecurity 설정이 적용되기 전에 자동으로 처리
헤더 추가: HeaderWriterFilter가 응답 헤더에 보안 관련 헤더를 추가. HttpSecurity 설정 시 자동으로 응답 헤더에 보안 관련 헤더를 추가. 기본적으로 HttpSecurity가 설정되면 활성화
CSRF 보호: CsrfFilter가 CSRF 토큰을 생성 및 검증. HttpSecurity 설정이 기본적으로 CSRF 보호를 활성화. 별도로 CSRF 설정을 비활성화하지 않았기 때문에, 이 필터는 기본적으로 활성화
인증 처리: UsernamePasswordAuthenticationFilter가 로그인 폼 데이터를 기반으로 사용자를 인증.

.formLogin()
    .permitAll()

익명 사용자 설정: AnonymousAuthenticationFilter가 인증되지 않은 사용자를 익명 사용자로 설정. HttpSecurity에서 인증되지 않은 사용자를 익명으로 설정하는 필터는 기본적으로 활성화 명 사용자 설정을 별도로 구성하지 않았기 때문에, 기본 설정이 사용됨.
권한 검사: FilterSecurityInterceptor가 사용자의 요청에 대해 접근 권한을 검사.

.authorizeRequests()
    .anyRequest().authenticated()

예외 처리: ExceptionTranslationFilter가 인증 및 권한 부여 과정에서 발생하는 예외를 처리. 인증 및 권한 부여 과정에서 발생하는 예외를 처리하는 필터는 HttpSecurity의 기본 설정에 포함 예외 처리는 필터 체인에서 자동으로 처리
로그아웃 처리: LogoutFilter가 로그아웃 요청을 처리.

.logout()
    .permitAll()

세션 관리: SessionManagementFilter가 세션 고정 공격을 방지. HttpSecurity 설정이 세션 관리와 관련된 기본 보안 설정을 포함. 추가적인 세션 관리 설정을 하지 않았기 때문에, 기본 설정이 사용

요약하면,
요청이 들어오면: 클라이언트의 HTTP 요청이 애플리케이션에 도달한다.
필터 체인 시작: 요청은 필터 체인의 첫 번째 필터로 전달된다.
필터 처리: 각 필터는 자신이 담당하는 보안 검사를 수행한다.
필터 체인 종료: 필터 체인의 마지막 필터까지 요청이 처리되면, 최종적으로 컨트롤러에 요청이 전달됩니다.
이렇게 돌아간다.

요청 밑 응답 해더

바로 위에서 본, 요청과 해더이다.
클라이언트와 서버 간의 메타데이터를 전달하는데 사용도니다.
여러 보안 관련 헤더를 자동으로 추가하여 보안을 강화한다.

스프링 시큐리티로 폼 기반 인증 및 인가 구현

스프링 시큐리티를 사용하여 폼 기반 인증 및 인가를 구현할 수 있다.
사용자는 로그인 폼을 통해 인증하고, 인증된 후 특정 리소스에 접근할 수 있다.
폼 기반 인증은 사용자 인증을 처리하는 기본적인 방법 중 하나로, 사용자가 로그인 폼을 통해 인증 정보를 제공하면 스프링 시큐리티가 이를 검증하고 사용자 세션을 생성한다.
예시를 통해 보자.
pom.xml

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

FormLoginSecurityConfig
WebSecurityConfigurerAdapter를 확장하여 보안 구성을 정의한다.

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
public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/login", "/resources/**").permitAll() // 로그인 페이지와 정적 리소스는 인증 없이 접근 허용
                .anyRequest().authenticated() // 나머지 요청은 인증이 필요함
                .and()
            .formLogin()
                .loginPage("/login") // 커스텀 로그인 페이지 URL
                .loginProcessingUrl("/authenticate") // 로그인 폼에서 전송될 URL
                .defaultSuccessUrl("/home", true) // 로그인 성공 시 이동할 기본 페이지
                .permitAll() // 로그인 페이지는 모든 사용자에게 접근 허용
                .and()
            .logout()
                .logoutUrl("/logout") // 로그아웃 URL
                .logoutSuccessUrl("/login?logout") // 로그아웃 후 이동할 페이지
                .permitAll(); // 로그아웃 페이지는 모든 사용자에게 접근 허용
    }

    @Bean
    @Override
    public UserDetailsService userDetailsService() {
        // 메모리 내 사용자 저장소 설정
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withDefaultPasswordEncoder()
                .username("user")
                .password("password")
                .roles("USER")
                .build());
        manager.createUser(User.withDefaultPasswordEncoder()
                .username("admin")
                .password("admin")
                .roles("ADMIN")
                .build());
        return manager;
    }
}

login.html 로그인 폼을 정의. 탬플릿에 정의한다.

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h2>Login Page</h2>
    <form action="/authenticate" method="post">
        <div>
            <label for="username">Username:</label>
            <input type="text" id="username" name="username"/>
        </div>
        <div>
            <label for="password">Password:</label>
            <input type="password" id="password" name="password"/>
        </div>
        <div>
            <button type="submit">Login</button>
        </div>
    </form>
</body>
</html>

로그아웃은 스프링 시큐리티의 기본 로그아웃 처리를 사용하거나, 커스텀 처리를 추가할 수 있다.
위에 만든 login 마냥 만들면 된다.

인증 및 인가를 위한 OIDC와 OAuth2 구현

현대 애플리케이션에서 인증 및 인가를 구현하는 데 자주 사용되는 프로토콜이며,
스프링 시큐리티는 이를 간편하게 통합할 수 있는 기능을 제공한다.
외부 인증 제공자를 통해 애플리케이션의 인증 및 인가 기능을 강화하는 좋은 방법이다.

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-oauth2-client</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- Optional: If you need to support Thymeleaf templates -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
    </dependency>
</dependencies>

application.properties

spring.security.oauth2.client.registration.google.client-id=YOUR_CLIENT_ID
spring.security.oauth2.client.registration.google.client-secret=YOUR_CLIENT_SECRET
spring.security.oauth2.client.registration.google.scope=profile,email
spring.security.oauth2.client.registration.google.redirect-uri={baseUrl}/login/oauth2/code/{registrationId}
spring.security.oauth2.client.registration.google.authorization-grant-type=authorization_code

spring.security.oauth2.client.provider.google.authorization-uri=https://accounts.google.com/o/oauth2/auth
spring.security.oauth2.client.provider.google.token-uri=https://oauth2.googleapis.com/token
spring.security.oauth2.client.provider.google.user-info-uri=https://www.googleapis.com/oauth2/v3/userinfo
spring.security.oauth2.client.provider.google.user-name-attribute=sub

OAuth2와 OIDC에 대한 설정을 추가

SecurityConfig

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.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
            .authorizeRequests()
                .antMatchers("/oauth2/**", "/login/**", "/public/**").permitAll() // OAuth2 관련 경로와 공개 경로 허용
                .anyRequest().authenticated() // 나머지 요청은 인증 필요
                .and()
            .oauth2Login()
                .loginPage("/login") // 커스텀 로그인 페이지
                .defaultSuccessUrl("/home", true) // 로그인 성공 시 이동할 페이지
                .failureUrl("/login?error=true") // 로그인 실패 시 이동할 페이지
                .and()
            .logout()
                .logoutSuccessUrl("/login?logout=true") // 로그아웃 후 이동할 페이지
                .permitAll();
        
        return http.build();
    }
}

LoginController

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {

    @GetMapping("/login")
    public String login() {
        return "login"; // 로그인 페이지 템플릿 이름
    }

    @GetMapping("/home")
    public String home() {
        return "home"; // 인증 후 이동할 페이지 템플릿 이름
    }
}

login.html Thymeleaf 사용

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
    <h1>Login</h1>
    <a href="/oauth2/authorization/google">Login with Google</a>
</body>
</html>

애플리케이션을 실행한 후, /login 페이지로 이동하면 Google 로그인 버튼이 표시된다.
이 버튼을 클릭하면 Google 인증 페이지로 리디렉션되고, 인증 후 애플리케이션으로 돌아와서 home 페이지로 이동하게 된다.
프링 부트 애플리케이션에서 OAuth2와 OIDC를 사용하여 강력한 인증 시스템을 구현하는 기본적인 방법리며,
실제 사용할 떄는 각 인증 제공자의 세부 설정을 참조하여 필요한 설정을 추가하고, 보안 요구 사항에 따라 추가적인 보안 조치를 고려해야 한다.