[Spring Security] (8) Spring Security x React로 유저 인증 구현하기-2

Park Yeongseo·2024년 6월 16일
2

Spring Security

목록 보기
8/13
post-thumbnail
  1. 사용자 정보를 담을 Member 엔티티 관련 기본 구현
  2. SecurityConfig 작성
  3. 예제에서 사용할 간단한 프론트 페이지 구현
  4. 이메일 인증 기능 추가
  5. 사용자 인증을 위한 UserDetails
  6. JWT 생성, 검출, 발급, 검증 구현
  7. 액세스 토큰
  8. 리프레시 토큰
  9. 로그인 유지

1. Introduction

오늘은 스프링 시큐리티의 설정을 위한 Configuration Class를 작성하고, 회원가입을 위한 간단한 프론트 페이지도 추가해보자.

2. 스프링 시큐리티의 아키텍쳐

그전에 스프링 시큐리티의 아키텍쳐에 대해 간단히 알아보자.

클라이언트로부터의 요청은 여러 개의 필터들을 순차적으로 엮은 필터 체인을 통과하게 된다. 그런데 이 필터 체인은 서블릿 컨테이너에 등록되는 것으로, 스프링 컨테이너와는 연관이 없다. 그렇기 때문에 원래는 필터를 스프링 빈으로 등록하거나, 혹은 필터에 빈을 주입하는 일은 불가능했다.

하지만 스프링 기술을 필터에서도 사용할 필요가 있게 되면서 DelegatingFilterProxy가 추가됐다. DelegatingFilterProxy도 서블릿 컨테이너에서 관리되는 필터지만, 이는 Filter를 구현하는 스프링 빈으로 작업을 위임함으로써 필터에서도 스프링 기술을 사용할 수 있게 한다.

FilterChainProxy는 스프링 시큐리티에서 제공되는 특별한 종류의 필터이며, SecurityFilterChain을 통해 어떤 필터들로 작업을 위임할지를 결정한다.

3. 구성 작성

간단하게 아키텍처에 대해 살펴봤으니, 아래와 같이 스프링 시큐리티의 설정 클래스를 만들고 그 안에 SecurityFilterChain을 작성해보자.

스프링 시큐리티에서는 기본적으로 폼 로그인 화면을 제공하고, 세션을 가지고서 로그인 여부를 관리한다. 하지만 이 프로젝트에서는 스프링 부트를 이용한 REST 서버를 만드는 게 목표이기 때문에 이런 기본 제공되는 폼 로그인 UI나 세션을 사용하지 않는다. 또한 프론트엔드도 스프링이 아닌 리액트로 만들기 때문에 CORS 관련 설정도 해줘야 한다.

@Configuration  
@EnableWebSecurity  
public class SecurityConfig {  
  
    @Bean  
    public SecurityFilterChain httpFilterChain(HttpSecurity http) throws Exception {  
        http  
                .httpBasic(AbstractHttpConfigurer::disable)  
                .cors(cors ->  
                        cors.configurationSource(corsConfigurationSource()))  
                .csrf(AbstractHttpConfigurer::disable)  
                .formLogin(AbstractHttpConfigurer::disable)  
                .sessionManagement(sessionManagement -> sessionManagement  
                        .sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
                .exceptionHandling(handling -> handling  
                        .authenticationEntryPoint((request, response, authException) -> {  
                            response.setStatus(401);  
                        })  
                );  
  
        return http.build();  
    }  

    @Bean  
    public CorsConfigurationSource corsConfigurationSource(){  
        CorsConfiguration corsConfig = new CorsConfiguration();  
  
        corsConfig.addAllowedOrigin("http://localhost:3000");  
        corsConfig.setAllowCredentials(true);  
        corsConfig.addAllowedHeader("*");  
        corsConfig.addAllowedMethod("*");  
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
        source.registerCorsConfiguration("/**", corsConfig);  
        return source;  
    } 
}

해설

SecurityConfig

@Configuration     // 구성 클래스다.
@EnableWebSecurity // Spring Security의 구성 정보를 활성화한다. 
public class SecurityConfig {  
	//...
}

SecurityFilterChain httpFilterChain(HttpSecurity http)

HttpSecuritySecurityFilterChain을 구현하는 DefaultSecurityFilterChain의 빌더다. 이 빌더를 받아 필요한 필터 체인을 구성해 빌드해 빈으로 등록한다.

@Bean  
public SecurityFilterChain httpFilterChain(HttpSecurity http) throws Exception {  

	http  
			.httpBasic(AbstractHttpConfigurer::disable)  
			.formLogin(AbstractHttpConfigurer::disable)  
			.sessionManagement(sessionManagement -> sessionManagement  
					.sessionCreationPolicy(SessionCreationPolicy.STATELESS))  
			.cors(cors ->  
					cors.configurationSource(corsConfigurationSource()))  
			.csrf(AbstractHttpConfigurer::disable)  
		;  

	return http.build();  
 
}
  • httpBasic(AbstractHttpConfigurer::disable)
    + Spring Security에서는 기본적으로 HTTP 기본 인증 방식이 켜져 있다. JWT를 이용한 인증 방식을 사용할 것이기 때문에 끄도록 하자.
  • formLogin(AbstractHttpConfigurer::disable)
    + Spring Security에서는 기본적으로 폼 로그인도 켜져 있다. 마찬가지로 끈다.
  • sessionManagement(...)
    + 스프링 시큐리티에서는 기본적으로 세션을 통해 로그인 여부를 확인한다. JWT를 사용할 것이므로 Stateless로 설정한다.
  • cors(cors -> cors.configurationSource(corsConfigurationSource())
    + 아래의 corsConfigurationSource()로 CORS 관련 설정을 재정의한다.
  • csrf(AbstractHttpConfigurer::disable)
    + CSRF 토큰을 이용한 CSRF 방어 옵션이다. 기본적으로는 켜져 있다. 세션/쿠키로 로그인을 구현하는 것이 아닌 경우에는 해당 옵션 비활성화해도 좋다(고 한다).
	@Bean  
	public CorsConfigurationSource corsConfigurationSource(){  
	    CorsConfiguration corsConfig = new CorsConfiguration();  
	  
	    corsConfig.addAllowedOrigin("http://localhost:3000");  
	    corsConfig.setAllowCredentials(true);  
	    corsConfig.addAllowedHeader("*");  
	    corsConfig.addAllowedMethod("*");  
	    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();  
	    source.registerCorsConfiguration("/**", corsConfig);  
	    return source;  
	}
  • corsConfig.addAllowedOrigin("http://localhost:3000");
    + 프론트엔드의 origin을 등록한다.
    + 리액트의 기본 포트 3000번을 사용하고 있다.
  • corsConfig.setAllowCredentials(true);
    + 쿠키 사용을 허용한다.
    + 리프레시 토큰을 위해 허용한다.
  • corsConfig.addAllowedHeader("*"); corsConfig.addAllowedMethod("*");
    + 모든 헤더와 메서드도 허용해주자.
    + 필요하다면 나중에 제한을 두도록 하자.
  • source.registerCorsConfiguration("/**", corsConfig);
    + 모든 URI 패턴에 이 설정을 적용한다.

지금은 가장 기본적인 것들만 세팅해놨지만, 이후 구현을 해가면서 계속 추가추가 해나가게 될 것이다.

4. 간단한 회원 가입 페이지

서버에서 구현할 기능 테스트용이니 대충 대충 필요한 것들만 추가해보자.

// App.js
import SignupForm from './component/SignupForm';

function App() {
  return (
    <div className="App">
      <header className="App-header">
        <SignupForm></SignupForm>
      </header>
    </div>
  );
}

export default App;
// ./component/SignupForm.js
import React, { useState } from 'react'
import axios from 'axios'

function SignupForm() {
    const [username, setUsername] = useState('');
    const [password, setPassword] = useState('');

    const handleSubmit = async (e) => {
        e.preventDefault();
        const body = {
            username: username,
            password: password
        };

        axios.post('http://localhost:8080/member/register', body)
            .then((res) => {
                if (res.status === 201) {
                    console.log("yes")
                }
            })
            .catch((res) => {
                console.log(res);
            })
    }

    return (
        <div className="formContainer">
            <form onSubmit={handleSubmit} className="form">
                <div className="formGroup">
                    <input className="formInput" onChange={e=>setUsername(e.target.value)} type="id" placeholder="이메일 입력" />
                </div>
                <div className="formGroup">
                    <input className="formInput" onChange={e=>setPassword(e.target.value)} type="password" placeholder="비밀번호 입력" />
                </div>
                <button onClick={handleSubmit} className='submitButton' type='submit'> sign up </button>
            </form>
        </div>
    )
}

export default SignupForm;

0개의 댓글