2025-06-02
스프링 시큐리티
Spring SecurityJWTOAuth2인증인가
스프링 시큐리티
Spring 생태계에서 가장 많이 사용되는 보안 프레임워크인 Spring Security를 다룹니다. 인증과 인가, Filter Chain 구조, JWT 기반 인증, OAuth2 구현을 살펴봅니다.
1. Authentication vs Authorization
Authentication (인증)
- 사용자가 누구인지 확인
- 자격 증명(username/password 또는 토큰)으로 신원 증명
- Spring Security는
AuthenticationManager와UserDetailsService사용
Authorization (인가)
- 인증된 사용자가 리소스에 접근할 수 있는지 검증
- 관리자 전용 API나 사용자 제한 페이지 접근 제어
@PreAuthorize,@Secured,antMatchers().hasRole()사용
예시 흐름: GET /api/user/profile
인증 단계:
- 클라이언트가 자격 증명 또는 JWT 토큰 전송
- Spring Security가
UserDetailsService를 호출하여 DB에서 사용자 정보 로드 Authentication객체 생성 후SecurityContext에 저장
인가 단계:
- 사용자가
ROLE_USER또는 필요한 scope를 가지고 있는지 확인 - 권한이 부족하면 403 FORBIDDEN 반환
2. Filter Chain (필터 체인) 구조
Spring Security는 Servlet Filter를 기반으로 동작하며, HTTP 요청이 컨트롤러에 도달하기 전에 보안 필터 체인을 거칩니다.
2-1. Security Filter Chain 개요
[Request] → [SecurityContextPersistenceFilter] → [UsernamePasswordAuthenticationFilter]
→ [JwtAuthenticationFilter(Custom)] → [FilterSecurityInterceptor] → [DispatcherServlet]
주요 필터
SecurityContextPersistenceFilter
- 세션에서 SecurityContext를 조회
- 인증 정보를 SecurityContextHolder에 저장
- 요청 후 세션에 컨텍스트 다시 저장
UsernamePasswordAuthenticationFilter
- 폼 기반 로그인 처리
/login으로의 POST 요청 처리- 자격 증명을 파싱하고 AuthenticationManager 호출
Custom JWT Filter
- JWT 토큰 유효성 검증
- 유효한 토큰에 대해 Authentication 객체 생성
- SecurityContextHolder에 저장
FilterSecurityInterceptor
- 컨트롤러 진입 전 최종 인가 확인
- SecurityMetadataSource와 AccessDecisionManager 사용
- URL 패턴과 역할 요구사항 적용
2-2. Filter Chain 설정
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable() // REST API에서는 비활성화
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter(jwtProvider());
}
}3. JWT 기반 인증
JWT 토큰은 인코딩된 정보(header, payload, signature)를 포함하여 세션 저장 없이 무상태 인증을 가능하게 합니다.
3-1. JWT 구조
형식: Header.Payload.Signature
- Header: 알고리즘(HS256), 토큰 타입(JWT)
- Payload: 사용자 ID, username, roles, 만료 시간(exp)
- Signature: Header와 Payload의 HMAC 서명 조합
3-2. JWT Provider 구현
@Service
public class JwtProvider {
private final String secretKey = "YOUR_SECRET_KEY";
private final long validityInMilliseconds = 3600000; // 1시간
// 토큰 생성
public String createToken(String username, List<String> roles) {
Claims claims = Jwts.claims().setSubject(username);
claims.put("roles", roles);
Date now = new Date();
Date expiry = new Date(now.getTime() + validityInMilliseconds);
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(expiry)
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
// 토큰에서 인증 정보 추출
public Authentication getAuthentication(String token) {
UserDetails userDetails = loadUserByUsername(getUsername(token));
return new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities());
}
// 토큰에서 username 추출
public String getUsername(String token) {
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token)
.getBody()
.getSubject();
}
// 토큰 유효성 검증
public boolean validateToken(String token) {
try {
Jws<Claims> claims = Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(token);
return !claims.getBody().getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}
}3-3. JWT Authentication Filter
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtProvider jwtProvider;
public JwtAuthenticationFilter(JwtProvider jwtProvider) {
this.jwtProvider = jwtProvider;
}
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null && jwtProvider.validateToken(token)) {
Authentication auth = jwtProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(auth);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if (bearer != null && bearer.startsWith("Bearer ")) {
return bearer.substring(7);
}
return null;
}
}3-4. 전체 Security 설정
@Configuration
@EnableWebSecurity
public class JwtSecurityConfig {
private final JwtProvider jwtProvider;
private final UserDetailsService userDetailsService;
public JwtSecurityConfig(JwtProvider jwtProvider,
UserDetailsService userDetailsService) {
this.jwtProvider = jwtProvider;
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/api/auth/**").permitAll()
.antMatchers(HttpMethod.GET, "/api/public/**").permitAll()
.anyRequest().authenticated()
.and()
.addFilterBefore(new JwtAuthenticationFilter(jwtProvider),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
return username -> {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role))
.collect(Collectors.toList())
);
};
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}4. OAuth2 구현
Spring Security 5.x 이상에는 내장 OAuth2 Client 지원이 포함되어 Google, Kakao, Naver, GitHub 등의 소셜 로그인 통합이 가능합니다.
4-1. Dependencies (build.gradle)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
}4-2. Application 설정 (application.yml)
spring:
security:
oauth2:
client:
registration:
google:
client-id: YOUR_GOOGLE_CLIENT_ID
client-secret: YOUR_GOOGLE_CLIENT_SECRET
scope: openid, profile, email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
client-name: Google
provider:
google:
authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
token-uri: https://www.googleapis.com/oauth2/v4/token
user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo
user-name-attribute: sub4-3. OAuth2 Security 설정
@Configuration
@EnableWebSecurity
public class OAuth2SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/login/**", "/css/**", "/js/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin().disable()
.oauth2Login()
.loginPage("/login")
.defaultSuccessUrl("/oauth2/success")
.userInfoEndpoint()
.userService(oAuth2UserService());
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService() {
return userRequest -> {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate =
new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName =
userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
Map<String, Object> attributes = oAuth2User.getAttributes();
String email = (String) attributes.get("email");
String name = (String) attributes.get("name");
User user = userRepository.findByEmail(email)
.orElseGet(() -> userRepository.save(User.builder()
.email(email)
.name(name)
.role("ROLE_USER")
.build()));
List<GrantedAuthority> authorities = List.of(new SimpleGrantedAuthority(user.getRole()));
return new DefaultOAuth2User(authorities, attributes, userNameAttributeName);
};
}
}4-4. Controller 예시
@Controller
public class OAuth2Controller {
@GetMapping("/login")
public String loginPage() {
return "login";
}
@GetMapping("/oauth2/success")
public String oauth2Success(@AuthenticationPrincipal OAuth2User oAuth2User, Model model) {
String name = oAuth2User.getAttribute("name");
String email = oAuth2User.getAttribute("email");
Collection<? extends GrantedAuthority> authorities = oAuth2User.getAuthorities();
model.addAttribute("name", name);
model.addAttribute("email", email);
model.addAttribute("authorities", authorities);
return "oauth2Success";
}
}마무리
- 인증 vs 인가: 사용자 신원 확인 후 리소스 접근 권한 검증
- Filter Chain: 순차적인 서블릿 필터가 컨트롤러 진입 전 보안 처리
- JWT: 서버 측 세션 저장 없이 무상태 토큰 기반 인증
- OAuth2: 외부 프로바이더 통합으로 소셜 로그인 간소화