2024. 3. 7. 15:01ใSpring/[2024] Spring Boot
โ ์ํ ํ๋ก์ ํธ๋ฅผ ๋ง๋ค๊ธฐ๋ก ๊ฒฐ์ ํ ์ด์
-> ์๊ฐ์ด ์กฐ๊ธ ์ฌ์ ๋ก์ด ์์ฆ, ๊ณง ๊ฐ๋ฐ ๋ค์ด๊ฐ๊ธฐ ์ ์ ๊ฐ๋จํ๊ฒ ์ํ ํ๋ก์ ํธ ๋ง๋ค๋ฉด์ ๊ฐ ์ก๊ณ ? ์๊ณ ์ถ์ด์!
๊ทธ๋ฆฌ๊ณ ๋ง๋ ๊น์ ์ค๋๋ง์ ํฐ์คํ ๋ฆฌ์ ๊ธ๋ ๋ง์ด ์ฌ๋ฆฌ๋ฉด์ ์งํํ ์์ ์ด๋ค.
โ ๊ฐ๋ฐ ํ๊ฒฝ
- Jdk 17
- SpringBoot 3.2.3
- Spring Security
- JPA + H2 (์ํ ํ๋ก์ ํธ๋ผ ๊ทธ๋ฅ ๊ฐ๋จํ ๊ตฌํํ ์ ์๋ h2 ์ฌ์ฉํ ์์ ์ด๋ค.)
- Spring REST Docs + mockMvc (REST Docs ๋ฅผ ์ด์ฉํด์ api ๋ฌธ์ ์๋ํํ ์์ ์ด๋ค.)
โ ์ง๊ธ ์๊ฐํ๋ ๊ธฐ๋ฅ
- ๋ก๊ทธ์ธ ๋ฐ ํ์๊ฐ์
- ๊ฒ์๊ธ ์์ฑ, ์กฐํ, ์์ , ์ญ์
- ๋๊ธ ์์ฑ, ์กฐํ, ์์ , ์ญ์
( + ์ฌ์ฉ์ ๊ด๋ฆฌ? )
์ด๋ ๊ฒ ์๊ฐํ๊ณ ์๋ค. ์ฐ์ ๊ธฐ์ด์ ์ธ ๊ฒ๋ค ๋จผ์ ๊ตฌํํ๊ณ , ํ๋ฉด์ ๊ณ์ ์ถ๊ฐ ์ถ๊ฐ ํ๋๊ฒ ๋ด ๋ฐ๋?์ด๋ค ใ ใ
โ ์ํ๋ฆฌํฐ ๋์ ๋ฐฉ์
- ์ฌ์ฉ์๊ฐ ๋ก๊ทธ์ธ ํ์ด์ง์ ์ ๊ทผ:
- ์ฌ์ฉ์๊ฐ ์ ํ๋ฆฌ์ผ์ด์ ์ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋ํ๊ฑฐ๋, ๋ณดํธ๋ ๋ฆฌ์์ค์ ์ ๊ทผํ์ฌ ๋ก๊ทธ์ธ์ด ํ์ํ ๊ฒฝ์ฐ ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋๋๋ค.
- ์ฌ์ฉ์๊ฐ ์๊ฒฉ ์ฆ๋ช
์ ๊ณต:
- ์ฌ์ฉ์๋ ์์ด๋์ ๋น๋ฐ๋ฒํธ ๋ฑ์ ์๊ฒฉ ์ฆ๋ช ์ ๋ก๊ทธ์ธ ํผ์ ํตํด ์ ๊ณตํ๋ค.
- AuthenticationFilter ์คํ:
- ์คํ๋ง ์ํ๋ฆฌํฐ์ AuthenticationFilter๊ฐ ๋์ํ๋ฉด์ ์ฌ์ฉ์๊ฐ ์ ๊ณตํ ์๊ฒฉ ์ฆ๋ช ์ ์์งํ๋ค.
- AuthenticationManager ๊ฒ์ฆ:
- AuthenticationFilter๋ ์์งํ ์๊ฒฉ ์ฆ๋ช ์ AuthenticationManager์ ์ ๋ฌํ๋ค.
- AuthenticationManager๋ ๋ฑ๋ก๋ AuthenticationProvider๋ค์ ์ํํ๋ฉฐ ์ค์ ์ธ์ฆ ์์ ์ ์ํํ๋ค.
- AuthenticationProvider ๋์:
- ๊ฐ AuthenticationProvider๋ ํน์ ์ธ์ฆ ์ ํ(์: ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ๋น๋ฐ๋ฒํธ)์ ๋ํ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ค.
- ์๋ฅผ ๋ค์ด, DaoAuthenticationProvider๋ UserDetailsService๋ฅผ ํตํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ ๋น๋ฐ๋ฒํธ๋ฅผ ํ์ธํ์ฌ ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋ค.
- ์ธ์ฆ ์ฑ๊ณต ๋๋ ์คํจ:
- ๋ง์ฝ ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด, AuthenticationManager๋ ์ธ์ฆ๋ ์ฌ์ฉ์๋ฅผ ๋ํ๋ด๋ Authentication ๊ฐ์ฒด๋ฅผ ์์ฑํ๋ค.
- ์ธ์ฆ์ด ์คํจํ๋ฉด ์์ธ๊ฐ ๋ฐ์ํ๊ฑฐ๋ null์ด ๋ฐํ๋๋ค.
- SecurityContextHolder์ Authentication ์ค์ :
- ์ฑ๊ณต์ ์ผ๋ก ์ธ์ฆ๋ ๊ฒฝ์ฐ, SecurityContextHolder์ ํ์ฌ ์ฌ์ฉ์์ Authentication ๊ฐ์ฒด๊ฐ ์ ์ฅ๋ฉ๋๋ค. ์ด ์ ๋ณด๋ ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ธ์ ๋ ์ง ์ฌ์ฉํ ์ ์๋ค.
- ์ธ์ฆ ์ด๋ฒคํธ ๋ฐ์:
- ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด AuthenticationSuccessEvent๊ฐ ๋ฐ์ํฉ๋๋ค. ์ด๋ฅผ ํตํด ์ปค์คํ ๋ก๊ทธ ๋ฐ ์ด๋ฒคํธ ํธ๋ค๋ง์ด ๊ฐ๋ฅํ๋ค.
- ์ธ์ฆ๋ ์ฌ์ฉ์ ๋ฆฌ๋ค์ด๋ ํธ ๋๋ ์ด๋:
- ์ผ๋ฐ์ ์ผ๋ก ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด ๋ณดํธ๋ ๋ฆฌ์์ค๋ก ๋ฆฌ๋ค์ด๋ ํธํ๊ฑฐ๋, ์ค์ ๋ ์ฑ๊ณต URL๋ก ์ด๋ํ๋ค.
0๏ธโฃ build.gradle
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.3'
id 'io.spring.dependency-management' version '1.1.4'
id 'org.asciidoctor.jvm.convert' version '3.3.2'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
ext {
set('snippetsDir', file("build/generated-snippets"))
}
dependencies {
// web, lombok, devtools
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
// security
implementation "org.springframework.boot:spring-boot-starter-security"
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
// JDBC Session
//implementation "org.springframework.session:spring-session-jdbc"
// ๋น๋ฐ๋ฒํธ ์ํธํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
// https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto
implementation 'org.springframework.security:spring-security-crypto'
// https://mvnrepository.com/artifact/org.bouncycastle/bcprov-jdk15on
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
// jpa, h2
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
// test, restdocs
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
}
tasks.named('test') {
outputs.dir snippetsDir
useJUnitPlatform()
}
tasks.named('asciidoctor') {
inputs.dir snippetsDir
dependsOn test
}
tasks.named('asciidoctor') ์ด๋ฐ ๋ชจ๋ฅด๋ ์ค์ ๋ค์ Spring REST Docs ๊ด๋ จ ์ค์ ๋ค์ด๋ ์ผ๋จ ๊ทธ๋ฌ๋ ค๋ ํ๊ณ ๋์ด๊ฐ๋ค.
1๏ธโฃ Spring Security ์ค์
์คํ๋ง ์ํ๋ฆฌํฐ ์์กด์ฑ ์ถ๊ฐํด์ฃผ๊ณ , ์ด์ ์ ์ผ ๋จผ์ Spring Security ์ค์ ์ ํด์ค ๊ฒ์ด๋ค.
SecurityConfig
package com.example.restful.config;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.WebSecurityCustomizer;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.crypto.scrypt.SCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.example.restful.api.repository.MemberRepository;
import com.example.restful.security.CustomUserDetailService;
import com.example.restful.security.filter.EmailPasswordAuthFiler;
import com.example.restful.security.handler.LoginFailHandler;
import com.example.restful.security.handler.LoginSuccessHandler;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig
{
@Value("${custom.static.url}")
private String[] staticUrlArray;
@Value("${custom.permit.url}")
private String[] permitUrlArray;
private final ObjectMapper objectMapper;
private final MemberRepository memberRepository;
@Bean
public WebSecurityCustomizer webSecurityCustomizer()
{
return web -> web.ignoring().requestMatchers(staticUrlArray)
.requestMatchers(permitUrlArray)
.requestMatchers(PathRequest.toH2Console());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception
{
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authorize ->
authorize.anyRequest().permitAll()
)
.addFilterBefore(emailPasswordAuthFiler(), UsernamePasswordAuthenticationFilter.class)
.build();
}
@Bean
public EmailPasswordAuthFiler emailPasswordAuthFiler()
{
EmailPasswordAuthFiler filter = new EmailPasswordAuthFiler("/auth/login", objectMapper);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(objectMapper));
filter.setAuthenticationFailureHandler(new LoginFailHandler(objectMapper));
return filter;
}
@Bean
public AuthenticationManager authenticationManager()
{
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new CustomUserDetailService(memberRepository));
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new SCryptPasswordEncoder(16, 8, 1, 32, 64);
}
}
-> ์ ์ฒด์ฝ๋์ด๊ณ ๋ฐ์์ ํ๋ํ๋ ๋ฏ์ด๋ณผ ์์ ์ด๋ค.
์ฐ์ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ผ๋ก
@Slf4j
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig
{
-> @EnableWebSecurity @Configuration ์ด๋ ธํ ์ด์ ์ ๋ถ์ฌ์ผ ํ๋ค. ๊ทธ๋์ผ ์ํ๋ฆฌํฐ ์ค์ ํ์ผ์ด๋ ๊ฑธ ์ธ์ํ๋ค !
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception
{
return httpSecurity
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(
authorize ->
authorize.anyRequest().permitAll()
)
.addFilterBefore(emailPasswordAuthFiler(), UsernamePasswordAuthenticationFilter.class)
.build();
}
-> ์คํ๋ง ์ํ๋ฆฌํฐ๋ฅผ ๋~~~๋ฌด ์ค๋๋ง์ ๋ค์ ํด๋ณด๋๋ฐ @Bean์ผ๋ก ๋ฑ๋กํด์ ์ฌ์ฉํ๋ฉฐ, ๋๋ค์์ผ๋ก ๋ณ๊ฒฝ๋๋ค.
(์ด๊ฑธ..์ด์ ์์ผ ๋ค์ ํด๋ณด๋ค๋....)
SecurityFilterChain ์ Spring Security์์ ์ฌ์ฉ๋๋ ํํฐ ์ฒด์ธ์ ์ ์ํ๋ ์ธํฐํ์ด์ค์ด๋ค.
์ด ์ธํฐํ์ด์ค๋ ์ฌ๋ฌ ๋ณด์ ํํฐ๋ฅผ ์กฐํฉํด์ ํ๋์ ํํฐ์ฒด์ธ์ ๋ง๋ค ์ ์๋ค.
์ด ํํฐ ์ฒด์ธ์ ํ์ฉํด์ ๋ค์ํ ๋ณด์ ๊ธฐ๋ฅ์ ์ ์ฉํ๋๋ฐ ์ฌ์ฉํ ์ ์๋ค.
-> ์ธ์ฆ, ๊ถํ ๋ถ์ฌ, ์ธ์ ๊ด๋ฆฌ, CSRF ๋ฐฉ์ด ๋ฑ์ด ์๋ค.
CRSF ๋ RESTful api ์์๋ disable ํ๋ค.
๐ฝ ์ด์ ์ ๋ํด์๋ ChatGPT์ ๋์์ ์ป์๋ค. ๐๐
Spring Security์์ RESTful API๋ฅผ ๊ฐ๋ฐํ ๋ CSRF(Cross-Site Request Forgery) ๋ณดํธ๋ฅผ ํด์ (disable)ํ๋ ๊ฒฝ์ฐ๊ฐ ์์ต๋๋ค. RESTful API๋ ์ฃผ๋ก statelessํ๋ฉฐ, CSRF๋ ์ฃผ๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ๋ฐ์ํ๋ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๊ธฐ ์ํ ๊ฒ์ ๋๋ค. ๊ทธ๋ฌ๋ฏ๋ก RESTful API์์ CSRF ๋ณดํธ๋ฅผ ํด์ ํ๋ ์ด์ ์ ๋ํด ์ดํด๋ณด๊ฒ ์ต๋๋ค:
1. Stateless Nature of RESTful APIs:
RESTful API๋ ์ฃผ๋ก statelessํ๊ฒ ์ค๊ณ๋์ด ์์ต๋๋ค. ๊ฐ ์์ฒญ์ ์ด์ ์์ฒญ๊ณผ ๋ ๋ฆฝ์ ์ด๋ฉฐ, ์๋ฒ๋ ํด๋ผ์ด์ธํธ์ ์ํ๋ฅผ ๊ธฐ์ตํ์ง ์์ต๋๋ค.
CSRF ๊ณต๊ฒฉ์ ํน์ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๊ฐ ์ ์์ ์ธ ๊ณต๊ฒฉ์์ ์ํด ์ด์ฉ๋๋ ๊ณต๊ฒฉ์ผ๋ก, ์ฃผ๋ก ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ธ์ ์ ์ด์ฉํ ๊ณต๊ฒฉ์ผ๋ก ๋ฐ์ํฉ๋๋ค. Statelessํ API์์๋ ์ด๋ฌํ ์ธ์ ๊ณต๊ฒฉ์ด ๋ ์ ์ฉ๋๋ฏ๋ก CSRF๋ฅผ ์ฌ์ฉํ ํ์๊ฐ ์ค์ด๋ญ๋๋ค.
2. CSRF๋ ์ฃผ๋ก ์น ํ์ด์ง์ ์ ์ฉ๋จ:
CSRF๋ ์ฃผ๋ก ์น ํ์ด์ง ๊ฐ ์์ฒญ์ ํตํ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๊ธฐ ์ํ ๊ฒ์ ๋๋ค. RESTful API๋ ์ผ๋ฐ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์์ฒญ๋๋ ๊ฒ์ด ์๋๋ผ, ํ๋ก๊ทธ๋๋ฐ ๋ฐฉ์์ผ๋ก ํธ์ถ๋๊ธฐ ๋๋ฌธ์ ๋ธ๋ผ์ฐ์ ์์์ CSRF ๊ณต๊ฒฉ ๊ฐ๋ฅ์ฑ์ด ์ ์ต๋๋ค.
3. ์ธ์ฆ ๋ฐฉ๋ฒ์ ์ฐจ์ด:
RESTful API๋ ์ฃผ๋ก ํ ํฐ ๊ธฐ๋ฐ์ ์ธ์ฆ ๋ฐฉ์์ ์ฌ์ฉํฉ๋๋ค. ํ ํฐ ๊ธฐ๋ฐ์ ์ธ์ฆ์ ์์ฒญ๋ง๋ค ํ ํฐ์ ํฌํจํ์ฌ ์ ๋ฌ๋์ด์ผ ํ๋ฉฐ, ์ด๋ ์ผ๋ฐ์ ์ผ๋ก ๋ธ๋ผ์ฐ์ ์ ์ฟ ํค์๋ ๋ค๋ฆ ๋๋ค. CSRF๋ ์ฟ ํค๋ฅผ ํตํ ์ธ์ ๊ณต๊ฒฉ์ ๋ฐฉ์ดํ๊ธฐ ์ํ ๊ฒ์ด๋ฏ๋ก, ํ ํฐ ๊ธฐ๋ฐ์ ์ธ์ฆ์์๋ ์ด์ ๋ฏผ๊ฐํ์ง ์์ ์ ์์ต๋๋ค.
* ๊ทธ๋ฌ๋ CSRF๋ฅผ ๋นํ์ฑํํ๋ ๊ฒ์ ๋ณด์ ์ธก๋ฉด์์ ์ฃผ์ํด์ผ ํ ์ฌํญ์ ๋๋ค. ๋ง์ฝ์ ๋ธ๋ผ์ฐ์ ๋ฅผ ํตํด API์ ์ ๊ทผํ๋ ๊ฒฝ์ฐ๋, ๋ณด์ ์ ์ค์ํ ์์ ์ ์ํํ๋ API๊ฐ ์๋ค๋ฉด, CSRF ๋ณดํธ๋ฅผ ๊ณ ๋ คํด์ผ ํฉ๋๋ค. CSRF ๊ณต๊ฒฉ์ ์ทจ์ฝํ์ง ์์ ์ํฉ์ด๋๋ผ๋, API์ ํน์ฑ๊ณผ ๋ณด์ ์๊ตฌ์ฌํญ์ ๊ณ ๋ คํ์ฌ ๊ฒฐ์ ํ๋ ๊ฒ์ด ์ค์ํฉ๋๋ค.
์ฐ์ ์ํ๋ฆฌํฐ ์ค์ ์ ํตํด ๋ชจ๋ ์์ฒญ์ permit ํ๊ณ , ๊ธฐ๋ณธ์ผ๋ก ์ ์ฉ๋๋ UsernamePassworddAuthenticationFilter ๋ฅผ ํ์ฅํด ๋ด๊ฐ ๋ง๋ค CustomFilter -> emailPasswordAuthFilter๋ฅผ ์ฌ์ฉํด AuthenticationManager์๊ฒ ์ ๋ฌํ ๊ฒ์ด๋ค.
๊ทธ๋ฆฌ๊ณ build !
UsernamePassworddAuthenticationFilter ์ Spring security์์ ์ฌ์ฉ์ ์ด๋ฆ๊ณผ ๋น๋ฐ๋ฒํธ๋ฅผ ์ฌ์ฉํ์ฌ ์ธ์ฆ์ ์ํํ๋ ๋ฐ ์ฌ์ฉ๋๋ ํํฐ ์ค ํ๋์ด๋ค. ์ด ํํฐ๋ฅผ ํตํด ์ฃผ๋ก HTTP POST ์์ฒญ์์ ์ฌ์ฉ์ ๋ก๊ทธ์ธ ์ ๋ณด๋ฅผ ๋ฐ์์์ ์ค์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ค.
๊ธฐ๋ณธ์ ์ผ๋ก /login ์์ ๋์ํ๋ฉฐ ์์ฒญ์์ ์ฌ์ฉ์ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ์ถ์ถํ๊ณ , UsernamePasswordAuthenticationToken ๊ฐ์ฒด๋ฅผ ์์ฑํดAuthenticationManager์๊ฒ ์ ๋ฌํ๋ค.
@Bean
public WebSecurityCustomizer webSecurityCustomizer()
{
return web -> web.ignoring().requestMatchers(staticUrlArray)
.requestMatchers(permitUrlArray)
.requestMatchers(PathRequest.toH2Console());
}
WebSecurityCustomizer๋ Spring Security ์์ ์ ๊ณตํ๋ ํจ์ํ ์ธํฐํ์ด์ค๋ก, ์น ๋ณด์ ๊ตฌ์ฑ์ ์ปค์คํ ํ ์ ์๋ค.
์ํ๋ฆฌํฐ๋ฅผ ๊ฑฐ์น์ง ์์ url ๋ค์ ๋ฐ๋ก ์ค์ ํด์ ๋น์ผ๋ก ๋ฑ๋กํ๋ค.
->
staticUrlArray : /favicon.ico, /index.html
permitUrlArray : /login/**, /error/**, /test/**
/h2-console ๋ ์ํ๋ฆฌํฐ ๊ฑฐ์น๋ฉด ํ์ธํ๊ธฐ ์ด๋ ต๊ธฐ ๋๋ฌธ์ ์ ์ธํด์ค๋ค.
(์ด๊ฑด appliation.yml์ ์ ์ฅํด๋๊ณ , ๊ฐ์ ธ์์ ์ฌ์ฉํ๋ค.)
@Bean
public EmailPasswordAuthFiler emailPasswordAuthFiler()
{
EmailPasswordAuthFiler filter = new EmailPasswordAuthFiler("/auth/login", objectMapper);
filter.setAuthenticationManager(authenticationManager());
filter.setAuthenticationSuccessHandler(new LoginSuccessHandler(objectMapper));
filter.setAuthenticationFailureHandler(new LoginFailHandler(objectMapper));
return filter;
}
@Bean
public AuthenticationManager authenticationManager()
{
DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
provider.setUserDetailsService(new CustomUserDetailService(memberRepository));
provider.setPasswordEncoder(passwordEncoder());
return new ProviderManager(provider);
}
@Bean
public PasswordEncoder passwordEncoder()
{
return new SCryptPasswordEncoder(16, 8, 1, 32, 64);
}
-> ๋ค์ EmailPasswordAuthFilter๋ ์ด์ UsernamePasswordAuthenticationFilter๋ฅผ ํ์ฅํด ๋ด๊ฐ ์ปค์คํ ํ filter ์ด๋ค.
๋๋ email์ id๋ก ์ฌ์ฉํ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ EmailPasswordAuthFilter๋ผ๊ณ ์ ํ๋ค.
๊ทธ๋ฆฌ๊ณ formLogin์ ์ฌ์ฉํ์ง ์๊ณ restful api์ด๋๊น json์ผ๋ก email (id)์ password๋ฅผ ๋ฐ์ ๊ฒ์ด๊ธฐ ๋๋ฌธ์ ์ปค์คํ ํ๋ค.
filter๋ฅผ ๋ง๋ค๊ณ , filter๋ก UsernamePasswordAuthenticationToken ์์ฑํ ๋ค์, AuthenticationManager์ ์ ๋ฌํด์ผ ํ๋ฏ๋กAuthenticationManager ๋ ๋น์ผ๋ก ๋ฑ๋กํด์ ์ค์ ํด์ฃผ๊ณ , ์ธ์ฆ ํ SuccessHandler์ FailHandler๋ฅผ ๊ฐ๊ฐ ๋ง๋ค์ด์ ์ค์ ํด์ค๋ค.
AuthenticationManager์๋ ์ค์ ์ธ์ฆ์ ์ฒ๋ฆฌํ AuthenticationProvider๋ฅผ ์ง์ ํด์ค๋ค.
๊ทธ๋ฆฌ๊ณ UserDetailService๋ฅผ ๋ด๊ฐ ๋ง๋ CustomUserDetailService๋ฅผ ๋๊ฒจ์ค๋ค.
๊ทธ๋ฆฌ๊ณ ์ด๋ค PasswordEncoder๋ฅผ ์ฌ์ฉํ ์ง ์ค์ ํด์ฃผ๋ฉด ๋๋ค.
(๋๋ ScryptPasswordEncoder๋ผ๋ ๊ฑธ ์์กด์ฑ ์ถ๊ฐํด์ ์ฌ์ฉํ๋ค.)
https://mvnrepository.com/artifact/org.springframework.security/spring-security-crypto
AuthenticationManager ๋ Spring Security์์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ํต์ฌ ์ธํฐํ์ด์ค์ด๋ค.
์ด ์ธํฐํ์ด์ค๋ Authentication ๊ฐ์ฒด๋ฅผ ์ ๋ ฅ๋ฐ์ ์ค์ ๋ก ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋ ์ญํ ์ ๋ด๋นํ๋ค.
Spring Security๋ AuthenticationManager๋ฅผ ํตํด ์ฌ์ฉ์๊ฐ ์ ๊ณตํ ์ธ์ฆ ์ ๋ณด๋ฅผ ๊ฒ์ฌํ๊ณ , ์ธ์ฆ์ด ์ฑ๊ณตํ๋ฉด
Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํ๋ค. ๊ทธ๋ฆฌ๊ณ ์ด ๊ฐ์ฒด์๋ ์ฌ์ฉ์์ ์ธ์ฆ๋ ์ ๋ณด ๋ฐ ๊ถํ ์ ๋ณด๊ฐ ํฌํจ๋์ด ์๋ค.
Spring Security์์๋ AuthenticationManager๋ฅผ ์ฌ์ฉํ์ฌ ๋ก๊ทธ์ธ ์ฒ๋ฆฌ ๋ฐ ๋ณด์ ๊ด๋ จ ์์ ์ ์ํํ๋ค.
AuthenticationProvider๋ Spring Security์์ ์ธ์ฆ์ ๋ด๋นํ๋ ์ธํฐํ์ด์ค๋ก, ์ด ์ธํฐํ์ด์ค๋ ์ค์ ๋ก ์ฌ์ฉ์๋ฅผ ์ธ์ฆํ๋ ์ญํ ์ ์ํํ๋ฉฐ, ๋ค์ํ ์ธ์ฆ ๋ฉ์ปค๋์ฆ์ ์ง์ํ๋ค.
DaoAuthenticationProvider, JwtAuthenticationProvider, LdapAuthenticationProvider ๋ฑ์ด AuthenticationProvider
๋ฅผ ๊ตฌํํ๊ณ ์๋ค.
AuthenticationProvider ์ ์ฃผ์ ๋ฉ์๋๋ authenticate ์ด๋ค. ์ด ๋ฉ์๋๋ Authentication ๊ฐ์ฒด๋ฅผ ์ธ์๋ก ๋ฐ์ ์ค์ ์ธ์ฆ์ ์ํํ๊ณ , ์ฑ๊ณตํ ๊ฒฝ์ฐ Authentication ๊ฐ์ฒด๋ฅผ ๋ฐํํ๊ณ ์คํจํ ๊ฒฝ์ฐ AuthenticationException ์ ๋์ง๋ค.
EmailPAsswordAuthFilter
public class EmailPasswordAuthFiler extends AbstractAuthenticationProcessingFilter
{
private final ObjectMapper objectMapper;
public EmailPasswordAuthFiler(String defaultFilterProcessUrl, ObjectMapper objectMapper)
{
super(defaultFilterProcessUrl);
this.objectMapper = objectMapper;
}
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException
{
EmailPassword emailPassword = objectMapper.readValue(request.getInputStream(), EmailPassword.class);
UsernamePasswordAuthenticationToken authRequest =
UsernamePasswordAuthenticationToken.unauthenticated(emailPassword.getEmail(), emailPassword.getPassword());
authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request));
return this.getAuthenticationManager().authenticate(authRequest);
}
}
EmailPasswordAuthFilter์์ request๋ฅผ EmailPassword๋ก ๊ฐ์ ๋ฐ์์ค๊ณ ,
์ด ๊ฐ์ ํตํด UsernamePasswordAuthenticationToken ์์ฑํ๋ค.
๊ทธ๋ฆฌ๊ณ AutheticationManager์ ์ด ํ ํฐ ๊ฐ์ ๋๊ฒจ์ฃผ๋ฉด ๋๋ค.
AbstractAuthenticationProcessingFilter๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ ๊ณตํ๋ ํํฐ ์ค ํ๋๋ก, ์ฌ์ฉ์์ ์ธ์ฆ(authentication)์ ์ฒ๋ฆฌํ๋ ์ญํ ์ ํ๋ ์ถ์ ํด๋์ค์ ๋๋ค. ์ด ํด๋์ค๋ ์ค์ ๋ก ์ฌ์ฉ์์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๋ ๊ตฌํ์ ํ๊ธฐ ์ํด ์์๋ฐ์์ผ ํ๋๋ฐ, ์ฃผ๋ก ๋ก๊ทธ์ธ(form login)์ ์ฌ์ฉ๋๋ค.
attemptAuthentication
-> ์ด ๋ฉ์๋๋ ์ค์ ๋ก ์ฌ์ฉ์์ ์ธ์ฆ์ ์๋ํ๋ ๋ฉ์๋์ด๋ค. ์ด ๋ฉ์๋๋ฅผ ๊ตฌํํด์ ์ฌ์ฉ์๊ฐ ์ ๊ณตํ ์๊ฒฉ ์ฆ๋ช ์ ํ์ธํ๊ณ AuthenticationManager๋ฅผ ํตํด ์ค์ ์ธ์ฆ์ ์ํํ๋ฉด ๋๋ค.
-> Provider๋ฅผ ํตํด ์ค์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๊ณ , ์ด๋ ์ธ์ฆ ์ฑ๊ณต์ Authtication ๊ฐ์ฒด ๋ฐํํ๊ณ ์คํจ์ null ๋ฐํํ๋ค.
UsernamePasswordAuthenticationToken์์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉ์์ ์์ด๋์ ๋น๋ฐ๋ฒํธ๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํ๋ ์ธ์ฆ์ ์ํํ๋ ๋ฐ ์ฌ์ฉ๋๋ ๊ตฌ์ฒด์ ์ธ Authentication ๊ฐ์ฒด๋ค. ์ด ํ ํฐ์ ์ฃผ๋ก ํผ ๋ก๊ทธ์ธ(form login)์์ ์ฌ์ฉ์์ ์ธ์ฆ์ ์ฒ๋ฆฌํ ๋ ์์ฑ๋๊ณ ํ์ฉ๋๋ค.
UsernamePasswordAuthenticationToken์ Authentication์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํด์ ์ฌ์ฉ์์ ์ธ์ฆ ์ ๋ณด๋ฅผ ๋ด๋ ๊ฐ์ฒด๋ก์ ์ญํ ์ ํ๋ค.
EmailPassword
@Getter
@Setter
public class EmailPassword
{
private String email;
private String password;
}
CustomUserDetailService
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService
{
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException
{
Member member = memberRepository.findByEmail(username).orElseThrow(() -> new UsernameNotFoundException("[" + username + "] ์ฌ์ฉ์๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค."));
return new UserPrincipal(member);
}
}
-> AuthenticationManager์์ ์ธ์ฆ์ ์ฒ๋ฆฌํ๊ธฐ ์ํด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํด์ผ ํ๋ค.
๋ฐ์ email ์ ๋ณด๋ฅผ ๊ฐ์ง๊ณ (์ฌ๊ธฐ์๋ username์ด id ์ฆ, email์ด๋ค.) memberRepository์์ ์กฐํํด ํด๋น ์ฌ์ฉ์๊ฐ ์กด์ฌํ๋์ง ํ์ธํ๋ค.
์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ฐํํ ๋ค์ Customํ UserPrincipal๋ก ๋ฐํํ๋ค.
UserPrincipal
@Getter
public class UserPrincipal extends User
{
private final Long userId;
public UserPrincipal(Member member)
{
super(member.getEmail(), member.getPassword(), List.of(new SimpleGrantedAuthority("ROLE_USER")));
this.userId = member.getId();
}
}
User๋ UserDetails๋ฅผ ๊ตฌํํ ํด๋์ค์ด๊ณ , ์ด๋ฅผ ์์๋ฐ์ UserPrincipal ์ ๋ง๋ค์๋ค.
๊ทธ๋ฆฌ๊ณ ์ฐ์ ๊ถํ์ USER ๊ถํ์ ๋ถ์ฌํ๋ค.
UserDetailsService ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ก๋ํ๋ ์ธํฐํ์ด์ค๋ค. ์ฃผ๋ก ์ฌ์ฉ์ ์ธ์ฆ(authentication) ๊ณผ์ ์์ ์ฌ์ฉ๋๋ฉฐ, ์คํ๋ง ์ํ๋ฆฌํฐ๊ฐ ์ฌ์ฉ์์ ์์ด๋(username)๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ํด๋น ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๊ฐ์ ธ์ค๋ ์ญํ ์ ์ํํ๋ค.
UserDetailsService ๋ฅผ ๊ตฌํํ๋ ค๋ฉด, loadUserByUsername ๋ฉ์๋๋ฅผ ๊ตฌํํด์ผ ํ๋ค. ์ด ๋ฉ์๋๋ ์ฌ์ฉ์์ ์์ด๋๋ฅผ ๋ฐ์ ํด๋น ์ฌ์ฉ์์ ์ ๋ณด๋ฅผ ๋ฐํํ๋ ์ญํ ์ ํ๋ค.
UserDetails ๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ํ๋ด๋ ์ธํฐํ์ด์ค์ด๋ค. ์ด ์ธํฐํ์ด์ค๋ฅผ ๊ตฌํํ ๊ฐ์ฒด๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ์ฌ์ฉ์ ์ธ์ฆ, ๊ถํ ๋ถ์ฌ, ๊ณ์ ์ ๊ธ ๋ฑ๊ณผ ๊ด๋ จ๋ ์์ ์ ์ํํ๋ ๋ฐ ์ฌ์ฉ๋๋ค.
LoginSucessHandler
@Slf4j
@RequiredArgsConstructor
public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException
{
UserPrincipal principal = (UserPrincipal) authentication.getPrincipal();
log.info("****** LoginSuccessHandler.onAuthenticationSuccess user={} ******", principal.getUsername());
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setStatus(HttpStatus.OK.value());
objectMapper.writeValue(response.getWriter(), ApiResponse.ok(principal));
}
}
-> ๋ก๊ทธ์ธ ์ฑ๊ณตํ์ ๋ ์ํํ๋ ํธ๋ค๋ฌ์ด๋ค. ์ง๊ธ์ ๋ก๊ทธ์ธ ์ฑ๊ณตํ๋ฉด json์ผ๋ก principal ๊ฐ์ ๋ด๋ ค์ฃผ๊ณ ์๋ค. (์ํ์ด๋๊น ใ )
LoginFailHandler
@Slf4j
@RequiredArgsConstructor
public class LoginFailHandler implements AuthenticationFailureHandler
{
private final ObjectMapper objectMapper;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException
{
log.info("****** LoginSuccessHandler.onAuthenticationFailure ******");
ApiResponse<Object> apiResponse = ApiResponse.of(HttpStatus.BAD_REQUEST, "์์ด๋ ๋๋ ๋น๋ฐ๋ฒํธ๊ฐ ์ฌ๋ฐ๋ฅด์ง ์์ต๋๋ค.", null);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);
response.setCharacterEncoding(StandardCharsets.UTF_8.displayName());
response.setStatus(HttpStatus.BAD_REQUEST.value());
objectMapper.writeValue(response.getWriter(), apiResponse);
}
}
-> ๋ก๊ทธ์ธ ์คํจํ์ ๋ ํธ๋ค๋ฌ์ด๋ค.
AuthenticationSuccessHandler๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ์ ๋์ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ ์ธํฐํ์ด์ค์ด๋ค. ๋ก๊ทธ์ธ์ด ์ฑ๊ณตํ๋ฉด ํด๋น ํธ๋ค๋ฌ๋ฅผ ํตํด ์ถ๊ฐ์ ์ธ ๋์์ด๋ ๋ฆฌ๋ค์ด๋ ์ ์ ์ํํ ์ ์๋ค.
onAuthenticationSuccess๋ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์ฑ๊ณตํ์ ๋ ์คํ๋๋ ๋ฉ์๋์ด๋ค.์ฌ๊ธฐ์์ ์ํ๋ ๋ก์ง์ ์ถ๊ฐํ๊ฑฐ๋ ๋ฆฌ๋ค์ด๋ ์ ์ ์ค์ ํ ์ ์๋ค.์ผ๋ฐ์ ์ผ๋ก HttpServletRequest, HttpServletResponse, Authentication ๊ฐ์ฒด ๋ฑ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค.
AuthenticationFailureHandler๋ ์คํ๋ง ์ํ๋ฆฌํฐ์์ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์คํจํ์ ๋์ ์ฒ๋ฆฌ๋ฅผ ๋ด๋นํ๋ ์ธํฐํ์ด์ค์ด๋ค. ๋ก๊ทธ์ธ์ด ์คํจํ๋ฉด ํด๋น ํธ๋ค๋ฌ๋ฅผ ํตํด ์ถ๊ฐ์ ์ธ ๋์์ด๋ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ฒ๋ฆฌํ ์ ์๋ค.
onAuthenticationFailure๋ ๋ก๊ทธ์ธ ์ธ์ฆ์ ์คํจํ์ ๋ ์คํ๋๋ ๋ฉ์๋์ด๋ค.์ฌ๊ธฐ์์ ์ํ๋ ๋ก์ง์ ์ถ๊ฐํ๊ฑฐ๋ ์คํจ ์์ ์๋ฌ ๋ฉ์์ง๋ฅผ ์ค์ ํ ์ ์๋ค.์ผ๋ฐ์ ์ผ๋ก HttpServletRequest, HttpServletResponse, AuthenticationException ๊ฐ์ฒด ๋ฑ์ ํ๋ผ๋ฏธํฐ๋ก ๋ฐ๋๋ค.
๐ฉ๐ป : ์ฐ์ ์ค๋์ ๊ธฐ๋ณธ์ ์ธ Security ์ค์ + CustomAuthFilter๋ฅผ ๋ง๋ค์ด์ ์ธํ ํด ๋ก๊ทธ์ธ ๊ตฌํ์ ์๋ฃํ๋ค.
RESTful api ๋ฐฉ์์ด๋๊น form login ์ด ์๋๋ผ json์ผ๋ก email, pasword ๋ฅผ ๋ฐ์์ ์ฒ๋ฆฌํ๊ธฐ ์ํด filter๋ฅผ ์ปค์คํ ํด์ ๋ง๋ค์๋ค. ์ฌ์ค ์ฒ์์๋ ๋ง ๋ฐ๋ผํ๋ฉด์ ๋ง๋ ๊ฑฐ๋ผ ์ข ์ ๋ฆฌ๊ฐ ์๋๋๋ฐ ๊ธ ์์ฑํ๋ฉด์ ๋ ์ ๋ฆฌ๊ฐ ๋๋ ๊ธฐ๋ถ ใ ใ
๊ทผ๋ฐ ๊ธ์ ์ฐ๋ค๋ณด๋ ์ฌ์ค ํ์ ๊ฐ์ ๋ถํฐ ๋จผ์ ํ์ด์ผ ํ๋ ์ถ์๋ฐ ์ด๋ฏธ ์คํ๋ง ์ค์ ๋ถํฐ ๊ธ์ฐ๊ธฐ๋ฅผ ์์ํด๋ฒ๋ ธ๋ค ๐
๊ทธ๋์ ์์ง ์๊ฐ ์ํ MemberRepository๊ฐ ์์์ ์ด์ง ๋์๋๋ฐ, ์ด๋ ๋ค์ ๊ธ์์ ํ์ธํ ์ ์์ ๊ฒ์ด๋ค ํท