Spring Security란?
Spring Security는 Spring 프레임워크 기반의 애플리케이션에 보안 기능을 제공하는 강력하고 유연한 프레임워크입니다. 주로 인증(authentication)과 인가(authorization) 기능을 제공하여 애플리케이션을 보호합니다. 이를 통해 개발자는 보안 관련 기능을 손쉽게 구현할 수 있습니다.
주요 개념
- 인증 (Authentication):
- 사용자나 시스템이 주장하는 신원을 확인하는 과정입니다.
- 일반적으로 사용자 이름과 비밀번호를 통해 수행되며, 성공 시 사용자가 시스템에 접근할 수 있습니다.
- 인가 (Authorization):
- 인증된 사용자가 특정 리소스나 기능에 접근할 수 있는 권한이 있는지 확인하는 과정입니다.
- 사용자의 역할(Role)이나 권한(Authority)을 기반으로 접근을 제어합니다.
- 필터 (Filter):
- Spring Security는 여러 개의 필터를 체인 형태로 연결하여 보안 처리를 수행합니다. 예를 들어,
UsernamePasswordAuthenticationFilter
는 사용자 이름과 비밀번호를 사용한 인증을 처리합니다.
- 보안 컨텍스트 (Security Context):
- 인증 정보를 저장하는 컨테이너로, 현재 사용자의 인증 상태와 관련된 정보를 포함합니다.
- 애플리케이션 내에서 보안 컨텍스트를 통해 사용자 정보를 조회할 수 있습니다.
주요 구성 요소
- AuthenticationManager:
- 인증을 처리하는 인터페이스입니다. 다양한 인증 방법을 지원하며,
ProviderManager
가 가장 일반적으로 사용됩니다.
- UserDetailsService:
- 사용자 정보를 로드하는 인터페이스입니다. 데이터베이스나 외부 시스템에서 사용자 정보를 조회하여 반환합니다.
- GrantedAuthority:
- 사용자가 가진 권한을 나타내는 인터페이스입니다. 보통 사용자의 역할(Role)이나 특정 기능에 대한 권한을 나타냅니다.
- Security Configuration:
- 보안 설정을 정의하는 클래스입니다.
@EnableWebSecurity
어노테이션과WebSecurityConfigurerAdapter
클래스를 사용하여 보안 설정을 커스터마이징할 수 있습니다.
기본 사용 예시
설정 파일
@Configuration
public class SecurityConfig {
// 평문을 hash로 바꿔준다
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.csrf(c -> c.disable());
http.authorizeHttpRequests(r -> {
r.requestMatchers("/s/**").authenticated().anyRequest().permitAll();
}).formLogin(f -> f.loginPage("/login-form").loginProcessingUrl("/login").defaultSuccessUrl("/"));
return http.build();
}
}
Spring Security 설정을 정의하는
SecurityConfig
클래스입니다. 이 클래스는 사용자 인증과 인가, 비밀번호 암호화 등을 설정합니다.코드 설명
- 클래스 선언 및 @Configuration 어노테이션:
@Configuration
public class SecurityConfig
- PasswordEncoder 빈 설정:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
PasswordEncoder
빈을 생성합니다. BCryptPasswordEncoder
를 사용하여 평문 비밀번호를 안전한 해시 형식으로 변환합니다.BCryptPasswordEncoder를 사용한 이유에 대해서는 본문 최하단에 작성하였습니다.
- SecurityFilterChain 빈 설정:
@Bean
public SecurityFilterChain configure(HttpSecurity http) throws Exception {
http.csrf(c -> c.disable());
http.authorizeHttpRequests(r -> {
r.requestMatchers("/s/**").authenticated().anyRequest().permitAll();
}).formLogin(f -> f.loginPage("/login-form").loginProcessingUrl("/login").defaultSuccessUrl("/"));
return http.build();
}
HttpSecurity
객체를 통해 보안 설정을 구성합니다.※Detail :
a. CSRF 보호 비활성화:
http.csrf(c -> c.disable());
CSRF(Cross-Site Request Forgery)란 사용자가 자신도 모르는 사이에 다른 사이트에서 공격자가 의도한 악의적인 요청을 전송하도록 하는 웹 보안 취약점입니다. 이를 통해 공격자는 인증된 사용자가 웹 애플리케이션에서 수행할 수 있는 작업을 불법적으로 실행할 수 있습니다.
b. 요청 인가 설정:
http.authorizeHttpRequests(r -> {
r.requestMatchers("/s/**").authenticated().anyRequest().permitAll();
});
/s/**
경로에 대한 요청은 인증된 사용자만 접근 가능하도록 설정합니다.c. 폼 로그인 설정:
.formLogin(f -> f.loginPage("/login-form").loginProcessingUrl("/login").defaultSuccessUrl("/"));
/login-form
으로 설정합니다./login
으로 설정합니다./
로 설정합니다.return http.build();
SecurityFilterChain
객체를 생성하여 반환합니다.Security는 로그인을 시도할 때 비밀번호의 경우, 입력한 값을 hash로 변환하여 DB에 저장된 값과 비교하기 때문에 기존의 더미데이터로는 테스트 할 수 없습니다.
자세한 내용은 본문 최하단에 추가로 작성하였습니다.
추가 또는 변경된 기존 클래스 내용
회원가입을 위한 static class:
public class UserRequest {
@Data
public static class JoinDTO {
private String username;
private String password;
private String email;
// User를 초기화 할 때, 비밀번호는 기존 값을 hash코드로 변경한 값으로 변경하여 초기화 한 뒤 반환한다.
public User toEntity(PasswordEncoder encoder) {
String encPassword = encoder.encode(password);
User user = new User(null, username, encPassword, email, null);
return user;
}
}
}
- DB에 저장하기 위해서는 User 객체가 필요하기 때문에 toEntity 메서드를 통해 자신의 값을 User 타입으로 변경시킨 엔티티를 반환.
- 비밀번호는 Hash 코드로 변경시킨 값을 매개 변수로 전달합니다. (따라서 메소드 매개 변수로 PasswordEncoder를 받습니다.)
Controller(User)
@PostMapping("/join")
public String join(UserRequest.JoinDTO joinDTO) {
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
- 위의 메서드가 추가 되었습니다. (회원가입)
- Security가 로그인을 대신하고 있기 때문에 로그인 관련 PostMapping이 삭제되었습니다.
Service
@RequiredArgsConstructor
@Service
public class UserService implements UserDetailsService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
// DB에 저장할 때 비밀번호를 hash로 변경하여 넣을 수 있도록 인코더를 매개 변수로 전달.
@Transactional
public void 회원가입(UserRequest.JoinDTO joinDTO) {
userRepository.save(joinDTO.toEntity(passwordEncoder));
}
// POST 요청
// /login 일때 호출 됨
// key 값 -> username, password
// Content-Type -> x-www-form-urlencoded
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username);
return user;
}
}
- 컨트롤러 회원가입과 관련된 메서드가 추가되었습니다.
(Repository의 save 메서드는 User 클래스를 받기 때문에 toEntity로 타입을 변환시켜 전달합니다.)
- UserDetailsService를 implements 한 뒤, loadUserByUsername 메서드를 재지정하였습니다.
loadUserByUsername 메서드 :
- 메서드 시그니처:
`public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException`
UserDetailsService
인터페이스에서 정의된 메서드를 재정의하고 있습니다.
- 입력 파라미터는
username
이며, 이는 사용자의 아이디로 사용됩니다.
- 반환 타입은
UserDetails
인터페이스를 구현한 객체입니다.
UsernameNotFoundException
예외를 던질 수 있습니다. 이는 사용자를 찾지 못했을 때 발생하는 예외입니다.
- 사용자 검색:
User user = userRepository.findByUsername(username);
userRepository
객체를 사용하여 데이터베이스에서 주어진 username
을 가진 사용자를 검색합니다.UserRepository
는 사용자 정보를 데이터베이스에서 조회하는 역할을 합니다.findByUsername
메서드는 username
을 기준으로 사용자 정보를 검색하여 User
객체를 반환합니다.반환 값은
UserDetails
인터페이스를 구현한 것으로, Spring Security에서 인증 작업을 처리하는 데 사용됩니다.요약
loadUserByUsername
메서드는 사용자의username
을 기준으로 데이터베이스에서 사용자를 검색하고,UserDetails
인터페이스를 구현하는 객체를 반환합니다.
- 이 메서드는 Spring Security의 인증 과정에서 사용되며, 주어진 사용자 이름에 해당하는 사용자를 로드하여 인증 처리에 필요한 정보를 제공합니다.
- 만약 사용자를 찾지 못하면
UsernameNotFoundException
예외를 던져 예외 상황을 처리할 수 있습니다.
Controller(Board)
@RequiredArgsConstructor
@Controller
public class BoardController {
@GetMapping("/")
public String index(@AuthenticationPrincipal User user) {
System.out.println(user.getUsername());
return "index";
}
@GetMapping("/board/save-form")
public String saveForm() {
return "board/save-form";
}
}
게시글 관련 컨트롤러 내용입니다.
/ 경로를 요청하면 콘솔에 아이디(username)를 출력하고 index페이지로 이동합니다.
매개변수
@AuthenticationPrincipal User user
:@AuthenticationPrincipal
어노테이션은 Spring Security와 통합되어 현재 인증된 사용자의 정보를 가져옵니다. 여기서는User
객체를 사용하여 인증된 사용자의 정보를 주입합니다.
따라서 인증된 유저의 정보만 들고오기 때문에 로그인을 하지 않고 해당 컨트롤러 메서드에 접근하게 되면 null 예외가 발생할 수 있습니다.(null 의 getUsername() 메서드 출력을 할 수 없기 때문)
Test
1. 회원가입

회원가입을 할 수 있는 폼을 만들어두지 않았기 때문에 Postman으로 테스트를 진행하였습니다.
요청 : POST
경로 : /join
바디데이터 : (아이디 : love, 비밀번호 : 1234, 이메일 : asdf)
2. 로그인

회원가입한 아이디와 비밀번호를 입력한 뒤 로그인을 진행합니다.
위 이미지는 경로를 ‘/s/ ~~’ 와 같이 입력했을 때 나타난 페이지입니다.
(/s/로 시작하는 경로는 모두 인증을 필요로 하게 설정하였기 때문)
로그인에 성공하게 되면 메인 페이지로 이동하게 됩니다.
BoardController의 index 메서드로 테스트
해당 메서드는 로그인이 되었다면 예외 발생 없이 콘솔에 로그인 된 유저의 username을 출력합니다.
또한 인증되지 않은 유저가 해당 경로에 접근하게 되면 null 예외를 발생시키도록 작성한 상태입니다.
로그인 한 뒤, index 메서드에 접근했을 때 :
콘솔 출력 - 정상

페이지 이동 - 정상

로그인 하지 않고 index 메서드에 접근했을 때 :

null 예외가 발생하는 것을 확인 - 정상
참고 사항 :
1. 기존 더미데이터로 로그인 불가

현재 더미데이터로 아이디와 비밀번호를 ssar, 1234 와 같이 미리 넣어두었으나, 사용할 수 없습니다.
- login의 영역을 Security가 대신하게 되었고, 전달 받은 비밀번호를 Hash코드로 변환하여 DB의 값과 비교하기 때문에 더이상 위와 같은 정보로 로그인 할 수 없습니다.
- 굳이 더미데이터를 활용하려고 한다면 기존에 미리 작성해둔 비밀번호를 hash로 변환한 값을 찾아, 해당 값을 더미데이터의 비밀번호로 넣어둔다면 가능합니다.
2. CSRF
CSRF의 원리
- 유효한 세션 쿠키:
- CSRF 공격은 사용자가 이미 타겟 웹사이트에 로그인된 상태에서, 해당 사이트와 유효한 세션 쿠키를 갖고 있을 때 발생합니다.
- 피싱 또는 악성 사이트 방문:
- 공격자는 사용자를 피싱 이메일이나 악성 사이트로 유도하여, 해당 사이트에서 악의적인 스크립트를 실행합니다.
- 악의적인 요청 전송:
- 사용자가 악성 사이트를 방문하면, 해당 사이트의 스크립트가 사용자의 브라우저를 통해 타겟 웹사이트로 요청을 전송합니다.
- 브라우저는 타겟 웹사이트에 유효한 세션 쿠키를 자동으로 포함하여 요청을 보냅니다.
- 원치 않는 작업 수행:
- 타겟 웹사이트는 요청이 사용자로부터 온 것으로 간주하고, 요청된 작업을 수행합니다. 예를 들어, 사용자의 계정 정보 변경이나 게시물 작성 등이 가능합니다.
CSRF 방지 방법
- CSRF 토큰 사용:
- 서버는 각 요청에 대해 고유한 CSRF 토큰을 생성하여 클라이언트에 전달하고, 클라이언트는 폼 데이터를 전송할 때 이 토큰을 함께 포함시킵니다.
- 서버는 토큰의 유효성을 검증하여 CSRF 공격을 방지합니다.
- 참조 헤더 검증:
- 서버는 요청의 참조 헤더(
Referer
)를 확인하여, 요청이 올바른 출처에서 발생했는지 검증할 수 있습니다.
- SameSite 쿠키 속성 사용:
- SameSite 쿠키 속성을 설정하여, 쿠키가 동일한 사이트에서 발생한 요청에서만 전송되도록 할 수 있습니다. 이를 통해 다른 사이트에서 발생한 요청에서는 쿠키가 전송되지 않도록 합니다.
3. BCryptPasswordEncoder를 사용한 이유?
BCryptPasswordEncoder의 장점
- 강력한 해시 함수:
- BCrypt는 Blowfish 암호화 알고리즘을 기반으로 한 해시 함수입니다. 해시 함수는 입력 데이터를 고정된 크기의 해시 값으로 변환하는 역할을 합니다.
- BCrypt는 입력된 비밀번호에 대해 고유한 솔트(salt)를 추가하여 해시 값을 생성합니다. 솔트는 같은 비밀번호라도 매번 다른 해시 값을 생성하도록 도와줍니다.
- 솔트 자동 생성:
- BCrypt는 비밀번호를 해시할 때 자동으로 랜덤 솔트를 생성하여 해시에 추가합니다. 이 솔트는 해시된 비밀번호에 포함되어 저장되므로, 나중에 검증할 때도 사용할 수 있습니다.
- 솔트 덕분에 같은 비밀번호라도 해시 값이 항상 다르게 나와 공격자가 해시 값을 통해 원래 비밀번호를 유추하는 것을 어렵게 만듭니다.
- 강력한 보안:
- BCrypt는 느리게 작동하도록 설계되었습니다. 해시 생성이 느리면 공격자가 많은 비밀번호를 시도해 보는 작업이 어렵고 시간이 많이 걸립니다.
- 이것은 비밀번호 추측 공격(브루트 포스 공격)이나 무차별 대입 공격으로부터 비밀번호를 보호하는 데 큰 장점이 됩니다.
- 적응성:
- BCrypt는 CPU 속도가 빨라지더라도 보안 강도를 유지할 수 있도록 설정할 수 있습니다. 해시 작업을 반복하는 횟수를 조절하여 해시 계산 시간을 늘릴 수 있습니다.
- 이는 시간이 지나면서도 보안 강도를 유지하도록 도와줍니다.
다른 인코더와의 비교
- MD5:
- 빠르고 간단하지만, 보안성이 낮습니다. MD5는 암호화된 해시 값에서 원래 값을 유추하기 쉽기 때문에, 비밀번호 암호화에는 적합하지 않습니다.
- SHA-1, SHA-256:
- SHA(Secure Hash Algorithm) 계열은 MD5보다는 보안성이 높지만, 여전히 단방향 해시 알고리즘으로서 솔트를 수동으로 추가해야 하고, 빠른 해시 속도로 인해 브루트 포스 공격에 취약할 수 있습니다.
Share article