Topic (오늘의 주제)
Spring Security는 Spring 기반 애플리케이션을 위한 보안 프레임워크로, 인증(Authentication)과 인가(Authorization)를 제공한다. 인증은 "누구인가"를 확인하는 과정이고, 인가는 "무엇을 할 수 있는가"를 확인하는 과정이다.
Why (왜 사용하는가? 왜 중요한가?)
- 보안이 없는 애플리케이션은 누구나 접근할 수 있어 데이터 유출, 무단 접근, 시스템 파괴 등의 심각한 보안 위험이 발생합니다. 인증 없이는 사용자 신원을 확인할 수 없고, 인가 없이는 권한에 따른 접근 제어가 불가능합니다.
- Spring Security는 인증과 인가를 체계적으로 관리하여 애플리케이션의 보안을 강화합니다. 다양한 인증 방식(폼 로그인, JWT, OAuth 등)을 지원하고, 역할 기반 접근 제어(RBAC)를 통해 세밀한 권한 관리를 가능하게 합니다. 개발자가 보안 로직을 직접 구현하지 않아도 프레임워크가 자동으로 처리하여 개발 생산성을 높입니다.
- 인증과 인가의 차이, Spring Security의 인증 과정, 인가 과정, 주요 컴포넌트들, 그리고 실제 구현 방법을 이해해야 합니다.
1. 인증(Authentication)과 인가(Authorization)의 차이
인증(Authentication)이란?
인증(Authentication)은 "누구인가?"를 확인하는 과정입니다.
예시:
- 로그인: 사용자가 자신의 신원을 증명 (ID, 비밀번호)
- JWT 토큰 검증: 토큰이 유효한지 확인
- OAuth 인증: 소셜 로그인을 통한 신원 확인
핵심 질문:
- "당신은 누구입니까?"
- "이 사용자가 정말 이 사람이 맞습니까?"
인가(Authorization)이란?
인가(Authorization)는 "무엇을 할 수 있는가?"를 확인하는 과정입니다.
예시:
- 관리자만 사용자 삭제 가능
- 본인만 자신의 정보 수정 가능
- 특정 역할(ROLE)만 접근 가능한 페이지
핵심 질문:
- "이 사용자가 이 작업을 수행할 권한이 있습니까?"
- "이 사용자가 이 리소스에 접근할 수 있습니까?"
인증 vs 인가 비교
| 구분 | 인증 (Authentication) | 인가 (Authorization) |
|---|---|---|
| 질문 | "누구인가?" | "무엇을 할 수 있는가?" |
| 목적 | 사용자 신원 확인 | 권한 확인 |
| 시점 | 로그인 시 | 리소스 접근 시 |
| 예시 | 로그인, 토큰 검증 | 역할 확인, 권한 체크 |
| 결과 | 인증된 사용자 정보 | 권한 부여 여부 |
실제 시나리오
1. 사용자가 로그인 시도
→ 인증(Authentication): "당신은 누구입니까?"
→ ID/비밀번호 확인 → 인증 성공
2. 사용자가 관리자 페이지 접근 시도
→ 인가(Authorization): "당신은 관리자 권한이 있습니까?"
→ 역할 확인 → 인가 성공/실패
코드 예시:
// 인증: 사용자 신원 확인
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 인증 과정: 사용자가 맞는지 확인
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
request.getUsername(),
request.getPassword()
)
);
return ResponseEntity.ok("인증 성공");
}
// 인가: 권한 확인
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')") // 인가: 관리자만 접근 가능
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
// 관리자 권한이 있는 사용자만 실행 가능
userService.deleteUser(id);
return ResponseEntity.ok("삭제 완료");
}
2. Spring Security의 인증 과정
인증 과정 전체 흐름
1️⃣ 사용자가 로그인 요청
(ID, 비밀번호 전송)
↓
2️⃣ AuthenticationFilter가 요청 가로채기
↓
3️⃣ AuthenticationManager가 인증 처리 위임
↓
4️⃣ UserDetailsService가 사용자 정보 조회
↓
5️⃣ PasswordEncoder가 비밀번호 검증
↓
6️⃣ 인증 성공 → SecurityContext에 Authentication 저장
↓
7️⃣ 인증된 사용자 정보 반환
주요 컴포넌트
1. AuthenticationFilter
역할:
- HTTP 요청을 가로채서 인증 정보 추출
- 인증 성공/실패 처리
예시:
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// JWT 토큰 추출
String token = extractToken(request);
if (token != null && validateToken(token)) {
// 인증 정보 설정
Authentication authentication = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
}
2. AuthenticationManager
역할:
- 인증 처리를 담당하는 인터페이스
- 실제 인증은
ProviderManager가 수행
예시:
@Configuration
public class SecurityConfig {
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
}
3. UserDetailsService
역할:
- 사용자 정보를 데이터베이스에서 조회
UserDetails객체 반환
예시:
@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("사용자를 찾을 수 없습니다."));
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole())
.build();
}
}
4. PasswordEncoder
역할:
- 비밀번호 암호화 및 검증
- BCrypt, Argon2 등 다양한 알고리즘 지원
예시:
@Configuration
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
// 사용 예시
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder;
public void createUser(String username, String password) {
// 비밀번호 암호화
String encodedPassword = passwordEncoder.encode(password);
User user = new User(username, encodedPassword);
userRepository.save(user);
}
public boolean validatePassword(String rawPassword, String encodedPassword) {
// 비밀번호 검증
return passwordEncoder.matches(rawPassword, encodedPassword);
}
}
인증 성공 후 처리
인증이 성공하면 SecurityContext에 Authentication 객체가 저장됩니다.
// SecurityContext에 저장되는 정보
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 인증된 사용자 정보
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
3. Spring Security의 인가 과정
인가 과정 전체 흐름
1️⃣ 인증된 사용자가 리소스 접근 요청
↓
2️⃣ FilterSecurityInterceptor가 요청 가로채기
↓
3️⃣ AccessDecisionManager가 권한 확인
↓
4️⃣ 사용자의 권한과 요청된 리소스의 필요 권한 비교
↓
5️⃣ 권한 있음 → 접근 허용
권한 없음 → 접근 거부 (403 Forbidden)
주요 컴포넌트
1. FilterSecurityInterceptor
역할:
- 인증된 사용자의 요청을 가로채서 권한 확인
- 마지막 필터로 동작
2. AccessDecisionManager
역할:
- 권한 확인을 담당하는 인터페이스
AffirmativeBased,ConsensusBased,UnanimousBased구현체 제공
예시:
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN") // 인가: 관리자만
.requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") // 인가: 사용자 또는 관리자
.requestMatchers("/public/**").permitAll() // 인가: 모두 허용
.anyRequest().authenticated() // 인가: 인증된 사용자만
);
return http.build();
}
}
인가 방식
1. 역할 기반 인가 (Role-Based)
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/admin/**").hasRole("ADMIN")
.requestMatchers("/user/**").hasRole("USER")
);
return http.build();
}
}
2. 메서드 레벨 인가
@RestController
public class UserController {
@GetMapping("/users")
@PreAuthorize("hasRole('USER')") // 인가: USER 역할만
public ResponseEntity<List<User>> getUsers() {
return ResponseEntity.ok(userService.findAll());
}
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')") // 인가: ADMIN 역할만
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok("삭제 완료");
}
@PutMapping("/users/{id}")
@PreAuthorize("hasRole('USER') and #id == authentication.principal.id") // 인가: 본인만
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
userService.updateUser(id, user);
return ResponseEntity.ok("수정 완료");
}
}
3. 표현식 기반 인가
@Configuration
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
// 메서드 레벨 인가 활성화
}
@RestController
public class UserController {
@GetMapping("/users/{id}")
@PreAuthorize("hasPermission(#id, 'User', 'READ')") // 커스텀 권한 확인
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
}
4. SecurityContext와 Authentication
SecurityContext란?
SecurityContext는 현재 인증된 사용자 정보를 담고 있는 컨텍스트입니다.
특징:
ThreadLocal을 사용하여 스레드별로 독립적- 요청이 끝나면 자동으로 정리됨
예시:
// SecurityContext에서 인증 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
String username = authentication.getName();
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
}
Authentication 객체
Authentication은 인증 정보를 담고 있는 객체입니다.
주요 속성:
principal: 사용자 정보 (UserDetails)credentials: 인증 정보 (비밀번호 등)authorities: 권한 목록
예시:
@RestController
public class UserController {
@GetMapping("/me")
public ResponseEntity<UserInfo> getCurrentUser(
@AuthenticationPrincipal UserDetails userDetails) {
// @AuthenticationPrincipal로 현재 사용자 정보 주입
UserInfo userInfo = new UserInfo(
userDetails.getUsername(),
userDetails.getAuthorities()
);
return ResponseEntity.ok(userInfo);
}
}
5. 실제 구현 예시
기본 설정
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 개발 환경에서만
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/")
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login")
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
UserDetailsService 구현
@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("사용자를 찾을 수 없습니다."));
return User.builder()
.username(user.getUsername())
.password(user.getPassword())
.roles(user.getRole().name())
.build();
}
}
컨트롤러에서 인증/인가 사용
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
// 인증: 로그인
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest request) {
// 인증 로직
return ResponseEntity.ok("로그인 성공");
}
// 인가: 본인만 접근 가능
@GetMapping("/{id}")
@PreAuthorize("hasRole('USER') and #id == authentication.principal.id")
public ResponseEntity<User> getUser(@PathVariable Long id) {
return ResponseEntity.ok(userService.findById(id));
}
// 인가: 관리자만 접근 가능
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.ok("삭제 완료");
}
// 현재 사용자 정보 가져오기
@GetMapping("/me")
public ResponseEntity<User> getCurrentUser(
@AuthenticationPrincipal UserDetails userDetails) {
User user = userService.findByUsername(userDetails.getUsername());
return ResponseEntity.ok(user);
}
}
6. JWT 기반 인증
JWT 인증 흐름
1️⃣ 사용자가 로그인 (ID, 비밀번호)
↓
2️⃣ 서버가 JWT 토큰 발급
↓
3️⃣ 클라이언트가 토큰을 저장 (로컬 스토리지, 쿠키 등)
↓
4️⃣ 이후 요청에 토큰 포함 (Authorization 헤더)
↓
5️⃣ 서버가 토큰 검증 및 사용자 인증
JWT 필터 구현
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenProvider jwtTokenProvider;
@Autowired
private UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String token = extractToken(request);
if (token != null && jwtTokenProvider.validateToken(token)) {
String username = jwtTokenProvider.getUsernameFromToken(token);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
Authentication authentication = new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities()
);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String extractToken(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}
SecurityConfig에 JWT 필터 추가
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS) // JWT는 무상태
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
7. 인증과 인가의 관계
인증이 선행되어야 인가가 가능
인증 (Authentication)
↓
사용자 신원 확인 완료
↓
인가 (Authorization)
↓
권한 확인 및 접근 제어
예시:
// 1. 인증 없이는 인가 불가능
@GetMapping("/users/{id}")
@PreAuthorize("hasRole('USER')") // 인증된 사용자만 접근 가능
public ResponseEntity<User> getUser(@PathVariable Long id) {
// 인증되지 않은 사용자는 여기 도달할 수 없음
return ResponseEntity.ok(userService.findById(id));
}
// 2. 인증 후 인가 확인
@DeleteMapping("/users/{id}")
@PreAuthorize("hasRole('ADMIN')") // 인증 + 관리자 권한 필요
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
// 1단계: 인증 확인 (로그인 여부)
// 2단계: 인가 확인 (관리자 권한 여부)
userService.deleteUser(id);
return ResponseEntity.ok("삭제 완료");
}
요약
- 인증(Authentication)은 "누구인가?"를 확인하는 과정으로, 사용자 신원을 증명합니다.
- 인가(Authorization)는 "무엇을 할 수 있는가?"를 확인하는 과정으로, 권한에 따른 접근을 제어합니다.
- Spring Security 인증 과정: AuthenticationFilter → AuthenticationManager → UserDetailsService → PasswordEncoder → SecurityContext 저장
- Spring Security 인가 과정: FilterSecurityInterceptor → AccessDecisionManager → 권한 확인 → 접근 허용/거부
- 주요 컴포넌트: AuthenticationManager, UserDetailsService, PasswordEncoder, AccessDecisionManager
- SecurityContext: 현재 인증된 사용자 정보를 담고 있는 컨텍스트 (ThreadLocal 사용)
- 인증 방식: 폼 로그인, JWT, OAuth 등 다양한 방식 지원
- 인가 방식: 역할 기반(Role-Based), 메서드 레벨, 표현식 기반 인가
- 인증이 선행되어야 인가가 가능: 인증된 사용자만 권한 확인이 가능합니다.
참고 자료
'Spring' 카테고리의 다른 글
| Spring_22) 사이가 멀수록 행복한 Rest 설계 (0) | 2026.01.02 |
|---|---|
| Spring_21) 그거 만료된 토큰인데요? (1) | 2026.01.01 |
| Spring_19) 내가 작성하지 않은 어노테이션, 함부로 지우면 안 되는 이유 (0) | 2025.12.30 |
| Spring_18) 에러가 아닙니다. 예외입니다. (1) | 2025.12.30 |
| Spring_17) 어노테이션 없는 스프링은 팥 없는 단팥빵 (0) | 2025.12.26 |