Spring Security란?
Spring Security는 Spring 기반 애플리케이션에 인증(Authentication)과 인가(Authorization) 기능을 제공하는 강력한 보안 프레임워크입니다.
핵심 특징
- 인증(Authentication)과 인가(Authorization) 처리
- 세션/토큰 기반 인증 지원
- CSRF, XSS, 세션 고정 공격 등 방어
- 필터 기반의 보안 체인(Security Filter Chain)
- 커스터마이징 가능한 설정 구조 (DSL 기반 설정)
Spring Security 작동원리
Spring Security는 Filter 기반의 체인 구조(Security Filter Chain) 로 동작한다.
사용자가 요청을 보낼 때, DispatcherServlet에 도달하기 전 보안 필터들이 요청을 검사하고 필요한 경우 인증 절차를 수행한다.

1. Http 요청 가로채서 로그인 요청을 가로채서 인증절차가 시작되는 스프링 시큐리티 필터체인 인증관문시작
2. UsernamePasswordAuthenticationToken:인증 전 사용자의 정보를 담는 인증 시도용 토큰 객체를 생성한다.
3. AuthenticationManager: AuthenticationProvider중 하나를 선택해 인증을 위임한다.
4. AuthenticationProvider: 사용자 인증을 수행하는 핵심 처리자 인증 성공시 Authentication 객체 반환
5. UserDetailsService: DB에서 사용자 정보를 조회하는 인터페이스로 loadUserByUsername(username) 메서드를 구현해서 DB나 외부 API로부터 사용자를 조회하고 UserDetails 객체로 반환한다.
6. UserDetails: 인증 대상 사용자 정보를 담는 객체로 UserDetailsService에서 조회 후 이 객체로 감싸서 반환한다.
7. SecurityContextHolder: 인증이 끝난 다음에 생성된 Authentication객체를 저장하는 저장소
[전체요청흐름]
1️.클라이언트 요청 (POST /login)
사용자 아이디와 비밀번호 전송
⠀⠀⠀⠀⠀⠀⠀⠀⬇
2️.UsernamePasswordAuthenticationFilter
로그인 요청 가로채기
→ UsernamePasswordAuthenticationToken 생성
→ AuthenticationManager.authenticate() 호출
⠀⠀⠀⠀⠀⠀⠀⠀⬇
3️.AuthenticationManager (ProviderManager)
처리 가능한 AuthenticationProvider 선택
→ provider.authenticate() 실행
⠀⠀⠀⠀⠀⠀⠀⠀⬇
4️.AuthenticationProvider (DaoAuthenticationProvider)
UserDetailsService 호출해 사용자 조회
→ PasswordEncoder로 비밀번호 검증
→ 인증 성공 시 Authentication 생성
⠀⠀⠀⠀⠀⠀⠀⠀⬇
5️.UserDetailsService
DB에서 사용자(username) 조회
→ UserDetails 객체 반환
⠀⠀⠀⠀⠀⠀⠀⠀⬇
6️.UserDetails
사용자 정보(username, password, 권한 등) 보유
→ AuthenticationProvider가 이 정보를 사용해 인증 완료
⠀⠀⠀⠀⠀⠀⠀⠀⬇
7️.SecurityContextHolder
Authentication을 SecurityContext에 저장
→ SecurityContextPersistenceFilter가 세션에 보관
⠀⠀⠀⠀⠀⠀⠀⠀⬇
✅ 이후 요청
세션에서 SecurityContext 복원
→ 로그인 상태 유지 후 Controller 접근 가능
Spring Security 설정방법
Spring Security를 적용하기 위해서는 요청에 대한 처리방식을 표현하는 SecurityConfig를 작성하고 회원검증 로직을 커스텀하기위해 UserDetailsService와 UserDetails를 구체화하는 과정이 필요하다 간단하게 말하면 다음과 같다
- 요청처리방식설정
- 회원검증구체화
[공통 Gradle 설정]
implementation 'org.springframework.boot:spring-boot-starter-security'
[PasswordEncoder 등록]
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
비밀번호의 암호화를 위해서 SpringConfig에 빈으로 등록한다
[UserDetailsService 구체화]
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final MemberRepository memberRepository;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Member member = memberRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("존재하지 않는 유저입니다."));
return new CustomUserDetails(member);
}
}
- UserDetailsService 인터페이스를 implements해서 loadUserByUsername메서드를 서비스에 맞게 구체화
[UserDetails 구체화]
@Getter
@AllArgsConstructor
public class CustomUserDetails implements UserDetails {
private final Member member;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority(member.getRole().name()));
}
@Override
public String getPassword() {
return member.getPassword();
}
@Override
public String getUsername() {
return member.getUsername();
}
}
- UserDetails인터페이스를 implements해서 loadUserByUsername메서드를 서비스에 맞게 구체화
Session
세션기반으로 구현할 경우 폼로그인을 사용해 로그인 컨트롤러를 따로 구현할 필요없이 UserDetailsService와 UserDetails를 각자의 서비스에 맞게 구현하고 SecurityConfig설정을 작성하면된다.
[Config 설정]
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// ✅ CSRF는 브라우저 기반 앱에서는 켜두는 게 안전
.csrf(csrf -> csrf.disable()) // REST API 형태면 disable 가능
// ✅ 요청별 접근 제어
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/css/**", "/js/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
// ✅ 폼 로그인 설정
.formLogin(form -> form
.loginPage("/login") // 커스텀 로그인 페이지
.loginProcessingUrl("/login") // 로그인 요청 처리 URL (POST)
.defaultSuccessUrl("/home", true) // 로그인 성공 시 리다이렉트
.failureUrl("/login?error=true") // 실패 시 이동
.permitAll()
)
// ✅ 로그아웃 설정
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout")
.invalidateHttpSession(true) // 세션 무효화
.deleteCookies("JSESSIONID") // 쿠키 삭제
.permitAll()
)
// ✅ 세션 관리 설정
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.ALWAYS) // 세션 항상 생성
.invalidSessionUrl("/login?expired=true") // 세션 만료 시 이동
.maximumSessions(1) // 중복 로그인 제한
.maxSessionsPreventsLogin(false)
);
return http.build();
}
}
JWT
JWT기반으로 Spring Security를 사용할 때는 JWT 토큰의 로직 클래스, 토큰을 검증하는 필터, UserDetailsService, UserDetailsf를 구현하여 적용하면 된다. jwt 토큰은 stateless하므로 설정파일에서 여러부분을 비활성화해야하므로 주의하자!!
[Gradle 설정]
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
jwt의 간편한 로직생성을 위해 jjwt 라이브러리 의존성을 추가한다
[Config 설정]
@Configuration
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtTokenProvider jwtTokenProvider;
private final UserDetailsService userDetailsService;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(AbstractHttpConfigurer::disable) //세션이나 쿠키를 사용하지 않으므로 비활성
.formLogin(AbstractHttpConfigurer::disable) // 컨트롤러에서 처리하므로 비활성
.httpBasic(AbstractHttpConfigurer::disable) // Authorization헤더 별도로 사용하므로 비활성
// 세션은 사용하지 않으므로 비활성
.sessionManagement(session
-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
//경로별 접근제어
.authorizeHttpRequests(auth -> auth
.requestMatchers("/auth/**").permitAll()// 인증&인가 요청은 허용
.requestMatchers("/admin/**").hasRole("ADMIN") // 관리자만 허용
.anyRequest().authenticated() //그외 인증필요
)
//토큰검증필터추가
.addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider, userDetailsService),
UsernamePasswordAuthenticationFilter.class)
.build();
}
}
[JwtTokenProvider]
@Component
public class JwtTokenProvider {
private static final String SECRET_KEY = "2256913ae74493677cb77fca6bbd7294e5115331341a7374037135ecd441afe3743f0210a50e44b9095fade92a16f5c433549ae4ed5aba3aaee3916d09f953f0";
private static final SecretKey KEY = Keys.hmacShaKeyFor(SECRET_KEY.getBytes());
private static final long VALIDITY = 1000 * 60 * 60 * 24;
public String createAccessToken(String username) {
Date now = new Date();
Date validity = new Date(now.getTime() + VALIDITY);
return Jwts.builder()
.subject(username)
.issuedAt(now)
.expiration(validity)
.signWith(KEY, Jwts.SIG.HS512)
.compact();
}
public boolean validateAccessToken(String token) {
try {
Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token);
return true;
} catch (RuntimeException e) {
throw new CUnAuthorizedException("유효하지 않은 토큰입니다.");
}
}
public String getUsername(String token){
return Jwts.parser()
.verifyWith(KEY)
.build()
.parseSignedClaims(token)
.getPayload()
.getSubject();
}
}
[JwtAuthenticationFilter]
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final UserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String jwt = resolveToken(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateAccessToken(jwt)) {
String username = tokenProvider.getUsername(jwt);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
//검증
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String prefix = "Bearer ";
String authHeader = "Authorization";
String bearerToken = request.getHeader(authHeader);
int start = prefix.length();
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(prefix)) {
return bearerToken.substring(start);
}
return null;
}
}
로그인 관련 컨트롤러나 서비스의 경우 각자의 서비스에 맞게 작성해주시면 됩니다.
'프로젝트' 카테고리의 다른 글
| [Spring Boot] 카카오 소셜로그인 구축기2 (0) | 2025.11.09 |
|---|---|
| [Spring Boot] 카카오 소셜 로그인 구축기1 (0) | 2025.11.08 |
| [Spring Boot] QueryDSL 적용기 (0) | 2025.11.07 |