찍어먹어보자! 인증과 인가란 무엇인가?
인증(authentication)은 사용자의 신원을 입증하는 과정이다. 예를 들어 사용자가 사이트에 로그인을 할 때 누구인지 확인하는 과정을 인증이라고 한다.
인가(authorization)는 사이트의 특정 부분에 접근할 수 있는지 권한을 확인하는 작업이다. 즉, 일반 사용자가 관리자 페이지에 접속할 수 없고 관리자는 관리자 페이지에 접속할 수 있는 것처럼 권한을 확인하는 것을 인가라고 한다.
찍어먹어보자! Spring Security란 무엇인가?
스프링 시큐리티는 스프링 기반 애플리케이션의 보안을 담당하는 스프링 하위 프레임워크이다.
보안 관련 옵션을 많이 제공하며, 애너테이션으로 설정도 쉽다.
CSRF 공격(사용자의 권한을 가지고 특정 동작을 수행하도록 유도하는 공격)과 세션 고정 공격(사용자의 인증 정보를 탈취하거나 변조하는 공격)을 방어해주고, 요청 헤더도 보안 처리를 해주므로 개발자가 보안 관련 개발을 해야 하는 부담을 크게 줄여준다.
스프링 시큐리티는 필터 기반으로 동작하며, 스프링 시큐리트의 필터 구조를 살펴보면 아래와 같다.

아주 많다! 뭐가 많다!
순서를 먼저 살펴보자면 SecurityContextPersistenceFilter로 부터 시작해서 아래로 내려가며 FilterSecurityInterceptor까지 순서대로 필터를 거친다.
필터를 실행할 때는 빨간색 화살표로 연결된 오른쪽 박스의 클래스를 거치며 실행을 한다.
특정 필터를 제거하거나 필터 뒤에 커스텀 필터를 넣는 등의 설정도 가능하다.
여기서 눈여겨 볼 것은 UsernamePasswordAuthenticationFilter와 FilterSecurityInterceptor인데 UsernamePasswordAuthenticationFilter는 아이디와 패스워드가 넘어오면 인증 요청을 위임하는 인증 관리자 역할은 하며,
FilterSecurityInterceptor는 권한 부여 처리를 위임해 접근 제어 결정을 쉽게 하는 접근 결정 관리자 역할을 한다.
각 필터에 대해서 설명을 하자면 아래와 같다.
필터명 | 설명 |
SecurityContextPersistenceFilter | SecurityContextRepository에서 SecurityContext(접근 주체와 인증에 대한 정보를 담고 있는 객체)를 가져오거나 저장하는 역할을 한다. |
LogoutFilter | 설정된 로그아웃 URL로 오는 요청을 확인해 해당 사용자를 로그아웃 처리한다. |
UsernamePasswordAuthenticationFilter | 인증 관리자 폼 기반 로그인할 때 사용되는 필터로 아이디, 패스워드 데이터를 파싱해 인증 요청을 위임한다. 인증이 성공하면 AuthenticationSuccessHandler를, 인증에 실패하면 AuthenticationFailureHandler를 실행한다. |
DefaultLoginPageGeneratingFilter | 사용자가 로그인 페이지를 따로 지정하지 않았을 때 기본으로 설정하는 로그인 페이지 관련 필터이다. |
BasicAuthenticationFilter | 요청 헤더에 있는 아이디와 패스워드를 파싱해서 인증 요청을 위임한다. 인증이 성공하면 AuthenticationSuccessHandler를, 인증에 실패하면 AuthenticationFailureHandler를 실행한다. |
RequestCacheAwareFilter | 로그인 성공 후, 관련 있는 캐시 요청이 있는지 확인하고 캐시 요청을 처리해준다. ex) 로그인하지 않은 상태로 방문했던 페이지를 기억해두었다가 로그인 이후에 해당 페이지로 이동시켜준다. |
SecurityContextHolderAwareRequestFilter | HttpServletRequest 정보를 감싼다. 필터 체인 상의 다음 필터들에게 부가 정보를 제공되기 위해 사용한다. |
AnonymousAuthenticationFilter | 필터가 호출되는 시점까지 인증되지 않았다면 익명 사용자 전용 객체인 AnonymousAuthentication을 만들어 SecurityContext에 넣어준다. |
SessionManagementFilter | 인증된 사용자와 관련된 세션 관련 작업을 진행한다. 세션 변조 방지 전략을 설정하고, 유효하지 않은 세션에 대한 처리를 하고, 세션 생성 전략을 세우는 등의 작업을 처리한다. |
ExceptionTranslationFilter | 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달한다. |
FilterSecurityInterceptor | 접근 결정 관리자 AccessDecisionManager로 권한 부여 처리를 위임함으로써 접근 제어 결정을 쉽게 해준다. 이 과정에서는 이미 사용자가 인증되어 있으므로 유효한 사용자인지도 알 수 있다. 즉, 인가 관련 설정을 할 수 있다. |
아이디, 패스워드 기반 폼 로그인을 시도하면 스프링 시큐리티에서는 어떤 일이?!
아이디와 패스워드 기반 폼 로그인을 시도하면 스프링 시큐리티에서는 어떤 절차로 인증 처리를 하는지 살펴보자

그림을 토대로 스프링 시큐리티 폼 로그인의 인증 흐름을 설명하자면 아래와 같다.
1. 사용자가 폼에 아이디와 패스워드를 입력하면 HTTPServletRequest에 아이디와 비밀번호 정보가 전달된다. 이때 AuthenticationFilter가 넘어온 아이디와 비밀번호의 유효성 검사를 한다.
2. 유효성 검사가 끝나면 실제 구현체인 UsernamePasswordAuthenticationToken을 만들어 넘겨 준다.
3. 전달받은 인증용 객체인 UsernamePasswordAuthenticationToken을 AuthenticationManager에게 보낸다.
4. UsernamePasswordAuthenticationToken을 AuthenticationProvider에게 보낸다.
5. 사용자 아이디를 UserDetailService에 보낸다. UserDetailService는 사용자 아이디로 찾은 사용자의 정보를 UserDetails 객체로 만들어 AuthenticationProvider에게 전달한다.
6. DB에 있는 사용자 정보를 가져온다.
7. 입력 정보와 UserDetails의 정보를 비교해 실제 인증 처리를 한다.
8 ~ 10. 해당 순서까지 인증이 완료되면 SecurityContextHolder에 Authentication을 저장한다. 인증 성공 여부에 따라 AuthenticationSuccessHandler(성공), AuthenticationFailureHandler(실패) 핸들러를 실행한다.
스프링 시큐리티를 사용해 인증, 인가 기능을 간단하게 구현해보자!
1. 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' // 스프링 시큐리티 테스트를 위한 의존성 추가
}
2. 회원 엔티티 만들기
여기서 회원 엔티티를 만들 때 UserDetails 인터페이스를 상속받아 인증 객체로 사용한다.
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;
@Table(name = "users")
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
public class User implements UserDetails {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "email", nullable = false, unique = true)
private String email;
@Column(name = "password")
private String password;
@Builder
public User(String email, String password, String auth) {
this.email = email;
this.password = password;
}
// 권한 반환
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("user"));
}
@Override
public String getUsername() {
return email;
}
// 계정 만료 여부 반환
@Override
public boolean isAccountNonExpired() {
// 만료 여부 확인 로직
return true;
}
// 계정 잠금 여부 반환
@Override
public boolean isAccountNonLocked() {
// 로직
return true;
}
// 패스워드의 만료 여부 반환
@Override
public boolean isCredentialsNonExpired() {
// 로직
return true;
}
// 계정 사용 가능 여부 반환
@Override
public boolean isEnabled() {
// 로직
return true;
}
}
3. 리포지토리 만들기
import org.springframework.data.jpa.repository.JpaRepository;
import org.yaliny.blog.domain.User;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
4. 서비스 만들기
해당 서비스는 스프링 시큐리티에서 로그인을 진행할 때 사용자 정보를 가져오는 코드를 작성하기 위한 서비스이다.
UserDetailsService를 상속받아 loadUserByUsername() 메서드를 오버라이딩해서 사용자 정보를 가져오는 로직을 작성한다.
import lombok.RequiredArgsConstructor;
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 org.yaliny.blog.repository.UserRepository;
@RequiredArgsConstructor
@Service
public class UserDetailService implements UserDetailsService {
private final UserRepository userRepository;
// 사용자 이름(여기선 email)으로 사용자의 정보를 가져오는 메서드
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
return userRepository.findByEmail(email).orElseThrow(() -> new IllegalArgumentException(email));
}
}
5. 시큐리티 설정하기
실제 인증 처리를 하는 시큐리티 설정 파일을 작성할 것이다.
config 패키지를 만들어 WebSecurityConfig라고 클래스 이름을 지어주고 아래와 같이 작성하였다.
설정에 대한 설명은 코드에 주석으로 달아두었다.
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.WebSecurityCustomizer;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.yaliny.blog.service.UserDetailService;
import static org.springframework.boot.autoconfigure.security.servlet.PathRequest.toH2Console;
@RequiredArgsConstructor
@Configuration
public class WebSecurityConfig {
private final UserDetailService userService;
// 스프링 시큐리티 기능 비활성화
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers(toH2Console())
.requestMatchers("/static/**");
}
/** 특정 HTTP 요청에 대한 웹 기반 보안 구성
* requestMatchers() : 특정 요청과 일치하는 url에 대한 액세스 설정
* permitAll() : 누구나 접근이 가능하게 설정(인증/인가 없이)
* anyRequest() : 위에서 설정한 url 이외의 요청에 대해서 설정
* authenticated() : 별도의 인가는 필요하지 않지만 인증이 성공된 상태여야 접근할 수 있음
*
* loginPage() : 로그인 페이지 경로 설정
* defaultSuccessUrl() : 로그인이 완료되었을 때 이동할 경로 설정
*
* logoutSuccessUrl() : 로그아웃이 완료되었을 때 이동할 경로 설정
* invalidateHttpSession() : 로그아웃 이후에 세션을 전체 삭제할지 여부를 설정
*
* csrf 설정은 해당 공격을 방지하기 위해 활성하는게 좋지만 해당 프로젝트에서는 비활성화처리
*/
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.authorizeHttpRequests() // 인증, 인가 설정
.requestMatchers("/login", "/signup", "/user").permitAll()
.anyRequest().authenticated()
.and()
.formLogin() // 폼 기반 로그인 설정
.loginPage("/login")
.defaultSuccessUrl("/articles")
.and()
.logout() // 로그아웃 설정
.logoutSuccessUrl("/login")
.invalidateHttpSession(true)
.and()
.csrf().disable() // csrf 비활성화
.build();
}
// 인증 관리자 관련 설정 : 사용자 정보를 가져올 서비스 재정의, 인증 방법 설정
@Bean
public AuthenticationManager authenticationManager(
HttpSecurity http,
BCryptPasswordEncoder bCryptPasswordEncoder,
UserDetailService userDetailService
) throws Exception{
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userService)
.passwordEncoder(bCryptPasswordEncoder)
.and()
.build();
}
// 패스워드 인코더로 사용할 빈 등록
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
여기까지 했다면 시큐리티 설정을 마친 것이다.
이제 회원가입부터 로그인, 로그아웃 관련 코드를 작성해보겠다.
DTO
@Getter
@Setter
public class AddUserRequest {
private String email;
private String password;
}
Service
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import org.yaliny.blog.domain.User;
import org.yaliny.blog.dto.AddUserRequest;
import org.yaliny.blog.repository.UserRepository;
@RequiredArgsConstructor
@Service
public class UserService {
private final UserRepository userRepository;
private final BCryptPasswordEncoder bCryptPasswordEncoder;
public Long save(AddUserRequest request) {
return userRepository.save(User.builder()
.email(request.getEmail())
.password(bCryptPasswordEncoder.encode(request.getPassword()))
.build()).getId();
}
}
API Controller
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.yaliny.blog.dto.AddUserRequest;
import org.yaliny.blog.service.UserService;
@RequiredArgsConstructor
@Controller
public class UserApiController {
private final UserService userService;
@PostMapping("/user")
public String signup(AddUserRequest request) {
userService.save(request);
return "redirect:/login"; // 회원 가입이 완료된 이후에 로그인 페이지로 이동(리다이렉트)
}
@GetMapping("/logout")
public String logout(HttpServletRequest request, HttpServletResponse response) {
new SecurityContextLogoutHandler().logout(request, response, SecurityContextHolder.getContext().getAuthentication());
return "redirect:/login";
}
}
View Controller
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class UserViewController {
@GetMapping("/login")
public String login() {
return "login";
}
@GetMapping("/signup")
public String signup() {
return "signup";
}
}
타임리프로 만든 html 파일 코드는 올리지 않았지만, 해당 코드를 통해서 회원가입, 로그인, 로그아웃 기능이 어떻게 돌아가는지 유추할 수 있을 것이다!
이렇게 오늘은 스프링 시큐리티와 인증/인가에 대해서 간단히 찍어먹어보았다.
이걸 가지고 토이 프로젝트에 적용을 해보고 다음에는 JWT에 대해서 공부해보고 적용해보려고 한다.
자료 출처
스프링 부트3 백엔드 개발자되기 - 자바편
https://siyoon210.tistory.com/32
https://velog.io/@readnthink/Spring-Security%EC%A0%81%EC%9A%A9%EC%9D%84-%ED%95%98%EB%A0%A4%EB%A9%B4-Security-Filter%EC%9D%98-%EA%B5%AC%EC%A1%B0%EB%A5%BC-%EC%95%8C%EC%95%84%EC%95%BC-%ED%95%9C%EB%8B%A4
'TIL > Spring' 카테고리의 다른 글
[Spring] 빈 스코프 란? (0) | 2025.03.08 |
---|---|
[Spring] 빈 생명주기 콜백 이란? - 스프링 초기화, 소멸 메서드 사용하기 (0) | 2025.03.04 |
[TIL/Spring] SpringBoot JUnit을 사용한 간단한 단위테스트(feat. 단위테스트와 TDD란 다른 것) (0) | 2023.02.15 |
[TIL/Spring] 스프링 시큐리티와 OAuth 2.0으로 구글 로그인 기능 구현 (0) | 2022.10.30 |
[TIL] 비즈니스 로직을 처리하는 곳은 Service가 아니다? (0) | 2022.10.09 |