Member
엔티티 관련 기본 구현SecurityConfig
작성UserDetails
오늘은 스프링 시큐리티의 설정을 위한 Configuration Class를 작성하고, 회원가입을 위한 간단한 프론트 페이지도 추가해보자.
그전에 스프링 시큐리티의 아키텍쳐에 대해 간단히 알아보자.
클라이언트로부터의 요청은 여러 개의 필터들을 순차적으로 엮은 필터 체인을 통과하게 된다. 그런데 이 필터 체인은 서블릿 컨테이너에 등록되는 것으로, 스프링 컨테이너와는 연관이 없다. 그렇기 때문에 원래는 필터를 스프링 빈으로 등록하거나, 혹은 필터에 빈을 주입하는 일은 불가능했다.
하지만 스프링 기술을 필터에서도 사용할 필요가 있게 되면서 DelegatingFilterProxy
가 추가됐다. DelegatingFilterProxy
도 서블릿 컨테이너에서 관리되는 필터지만, 이는 Filter
를 구현하는 스프링 빈으로 작업을 위임함으로써 필터에서도 스프링 기술을 사용할 수 있게 한다.
FilterChainProxy
는 스프링 시큐리티에서 제공되는 특별한 종류의 필터이며, SecurityFilterChain
을 통해 어떤 필터들로 작업을 위임할지를 결정한다.
간단하게 아키텍처에 대해 살펴봤으니, 아래와 같이 스프링 시큐리티의 설정 클래스를 만들고 그 안에 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)
HttpSecurity
는 SecurityFilterChain
을 구현하는 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)
formLogin(AbstractHttpConfigurer::disable)
sessionManagement(...)
cors(cors -> cors.configurationSource(corsConfigurationSource())
corsConfigurationSource()
로 CORS 관련 설정을 재정의한다.csrf(AbstractHttpConfigurer::disable)
@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");
corsConfig.setAllowCredentials(true);
corsConfig.addAllowedHeader("*"); corsConfig.addAllowedMethod("*");
source.registerCorsConfiguration("/**", corsConfig);
지금은 가장 기본적인 것들만 세팅해놨지만, 이후 구현을 해가면서 계속 추가추가 해나가게 될 것이다.
서버에서 구현할 기능 테스트용이니 대충 대충 필요한 것들만 추가해보자.
// 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;