2024. 11. 14. 03:15ㆍProject
개인 프로젝트를 진행하면서 처음으로 Spring에서 지원하는 Spring Security를 활용하여 회원기능을 구현해보았다.
나도 아직 Spring boot를 배우면서 프로젝트를 진행하는 중이라 기능들을 구현하는 데에 시간이 좀 오래걸렸다.
잘 모르는 부분들은 Chat gpt를 활용하면서 하나씩 해결해 나갔다.
오늘은 그럼 Spring Security가 무엇인지, 그리고 이를 어떤식으로 프로젝트에 적용 시켰는지 기록 해볼것이다.
Spring Security란?
먼저 왜 Spring Security를 사용하여 회원 기능을 구현해야 하는지에 대해서 말하자면, 웹사이트에서 로그인, 로그아웃 등의 기능을 구현하면 이에 대한 권한 부여 / 관리 등이 필요하다.
이를 Spring에서 쉽고 효율적으로 구현할 수 있게 개발된 것이 Spring Security 이다. Spring Security를 사용하여 개발하면
인증 / 인가 등의 보안적인 기능을 개발하는 데에 있어서 아주 효율적이고 신속하게 개발이 가능하다.
예를들어, 로그인을 한 사용자에게만 사이트 접속 권한을 부여한다던지 이런 기능들을 효율적으로 처리할 수 있다.
Spring Security 사용 방법
그렇다면 Spring Security는 어떻게 사용하는 걸까?
<Spring Boot gradle을 사용한 나의 프로젝트 기준>
먼저, build.gradle에 아래와 같이 의존성을 추가해주어야 한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
testImplementation 'org.springframework.security:spring-security-test'
<-- 기타 의존성 생략 -->
}
그리고 config 클래스를 생성하여 Spring Security 기능을 구현해주어야 한다.
먼저, src > main > java > com.xx.MyProject 경로 안에 config 패키지를 생성 해주고, config 패키지 안에 SecurityConfig 클래스를 생성해주었다.
그럼 나의 SecurityConfig 클래스의 구성에 대해서 하나하나 자세히 알아보자.
전체 코드
package com.yubin.SpringBootTest.config;
import com.yubin.SpringBootTest.service.CustomUserDetailsService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
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.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.LogoutConfigurer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailsService userDetailsService;
public SecurityConfig(CustomUserDetailsService userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
auth.authenticationProvider(authenticationProvider());
return auth.build();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register", "/login","/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login") // 로그인 처리 URL 설정
.defaultSuccessUrl("/home", true) // 로그인 성공 후 이동할 URL 지정
.failureUrl("/login?error=true") // 로그인 실패 시 이동할 경로
.permitAll()
)
.logout(LogoutConfigurer::permitAll)
.csrf(AbstractHttpConfigurer::disable); // CSRF 비활성화;
return http.build();
}
}
PasswordEncoder
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
사용자의 패스워드를 암호화 하기 위한 메서드이다.
BCryptPasswordEncoder는 SpringSecurity에서 지원하는 클래스로, 비밀번호를 bcrypt 해시 함수로 암호화하고 검증하는 기능을 제공한다.
해시 생성 시 매번 무작위로 salt를 추가하여 같은 비밀번호도 항상 다른 해시 값을 가지게 만들어 보안에 아주 강력하다.
DaoAuthenticationProvider
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
DaoAuthenticationProvider는 UserDetailsService를 통해 데이터베이스에서 사용자 정보를 로드하고, PasswordEncoder를 사용하여 비밀번호를 검증한다. 그리고 CustomUserDetailsService를 이용해 사용자 정보를 조회하고, 사용자 인증 시 암호를 인코딩해 매칭한다.
여기서 UserDetailsService가 무엇이냐?
UserDetailsService는 UserDetails 객체를 반환하며, Spring Security가 인증 절차에서 사용자 세부 정보를 관리할 수 있도록 표준화된 인터페이스이다.
그럼 CustomUserDetailsService 클래스를 따로 생성해주는 이유는?
UserDetailsService가 세부 정보를 관리할 수 있게 해주었다면, CustomUserDetailsService에서 사용자 정보 등을 데이터 베이스와 연동하여 조회, 또는 이 정보를 기반으로 인증을 수행한다.
<CustomUserDetailsService>
package com.yubin.SpringBootTest.service;
import com.yubin.SpringBootTest.model.User;
import com.yubin.SpringBootTest.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.isEnabled(),
true,
true,
true,
List.of(new SimpleGrantedAuthority("ROLE_USER")) // 기본 권한 추가
);
}
}
AuthenticationManager
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);
auth.authenticationProvider(authenticationProvider());
return auth.build();
}
AuthenticationManager 는 로그인 시도 시 사용자의 신원을 검증하는 역할을 하며, 성공적으로 인증된 Authentication 객체를 반환하거나 인증 실패 시 예외를 던진다.
흐름을 한번 살펴보자.
1. 사용자가 로그인 페이지를 통해 사용자명과 비밀번호를 입력하고 제출하면, Spring Security는 로그인 요청을 받아 AuthenticationManager.authenticate()를 호출.
2. AuthenticationManager는 여러 AuthenticationProvider 목록을 가지고 있으며, 요청된 Authentication 객체를 각 AuthenticationProvider에 전달하여 인증을 시도.
(현재 나의 코드에서는 위에서 확인 했듯이 DaoAuthenticationProvide임)
3. 인증에 성공하면 세션에 인증 정보를 저장하고, 사용자를 인증된 상태로 유지한다.
인증에 실패하면 AuthenticationProvider는 예외를 던져 인증 실패를 처리한다.
SecurityFilterChain
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/register", "/login", "/error").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/home", true)
.failureUrl("/login?error=true")
.permitAll()
)
.logout(LogoutConfigurer::permitAll)
.csrf(AbstractHttpConfigurer::disable);
return http.build();
}
SecurityFilterChain은 DaoAuthenticationProvider를 사용하여 사용자 정보를 로드하고, SecurityFilterChain으로 HTTP 요청을 보호하면서 지정된 페이지에 대한 접근 권한을 설정한다.
- requestMatchers 메서드를 통해 "/register", "/login", "/error" 경로를 모든 사용자에게 허용 (permitAll) 한다.
- formLogin을 통해 로그인 페이지와 로그인 처리 URL, 성공 or 실패 시 이동할 경로를 지정한다.
- loginPage("/login") : 사용자 정의 로그인 페이지 경로를 설정한다.
- loginProcessingUrl("/login") : 로그인 요청을 처리할 URL.
- defaultSuccessUrl("/home", true) : 로그인 성공 시 이동할 페이지를 지정한다. true는 로그인 페이지를 강제로 /home으로 지정하여 마지막 페이지가 아닌 특정 페이지로 이동하게 한다.
- failureUrl("/login?error=true") : 로그인 실패 시 이동할 URL.
- 로그아웃 (logout) : permitAll로 설정하여 모든 사용자에게 로그아웃을 허용한다.
그리고 내가 이 SecurityFilterChain 부분을 공부하고 적용하면서 엄청 애를 많이 먹었는데, 분명히 reauestMatchers에
"/register", "/login" 등의 경로를 지정해 주었다. 그런데도 계속 프로젝트를 실행하면 403에러가 뜨는 것이다.
그래서 끙끙대며 머리를 싸매며 해결 방법을 찾다가 csrf를 disable 해주어야 한다는 글을 보게 되었다.
기본적으로 Spring Security를 사용하면 csrf가 활성화 되어 있다.
여기서 csrf란 Cross Site Request forgery, 사이트 간 위조 요청으로 일종의 보안 위협 요소이다.
여러 글들을 찾아봤는데 대부분의 가이드 글들이 csrf를 disable 하고 사용하라고 나와 있어서 그렇게 해보니 403 에러가 사라지고 요청이 잘 되었다.
왜 대부분 csrf를 사용하지 않는지 궁금하여 한 번 찾아봤는데,
Rest API를 이용한 서버라면, session 기반 인증과는 다르게 stateless하기 때문에 서버에 인증 정보를 보관하지 않는다. 일반적으로 jwt 같은 토큰을 사용하여 인증하기 때문에 해당 토큰을 Cookie에 저장하지 않는다면 csrf 취약점에 대해서는 어느 정도 안전하다고 한다.
모르는 부분을 하나씩 새로 배워가면서 프로젝트를 진행하다 보니 진행 속도가 많이 더디지만, 그래도 알차게 제대로 배워가는 것 같아서 뿌듯하긴 하다.
아 그리고 csrf.disable()은 곧 지원이 중단된다고 하길래 그럼 어떻게 해줘야 할지 gpt에게 물었더니 위 코드처럼
.csrf(AbstractHttpConfigurer::disable) 라고 작성하면 된다고 한다.
'Project' 카테고리의 다른 글
[Project] Google Gemini API 활용 (3) | 2024.11.20 |
---|---|
[Project] 프로젝트 후기 (5) | 2024.10.10 |
[Project] Spring 팀 프로젝트 시작 (0) | 2024.10.10 |
[Project] JSP프로젝트 - OTT 커뮤니티 사이트 (1) | 2024.10.09 |