스프링 시큐티리 + JWT 를 이용한 로그인 처리

2025. 8. 3. 19:32·spring & java

스프링 공식문서 - 스프링 시큐리티

 

Architecture :: Spring Security

The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like exploit protection,authentication, authorization, and more. The filters are executed in a speci

docs.spring.io

 

 

스프링 시큐리티란?

일반적인 공격에 대한 인증, 권한 부여 및 보호기능을 제공하는 프레임 워크이다.

회원가입, 로그인/로그아웃, 권한 관리를 위한 인증과 인가의 보안 기능을 제공한다. 

 

인증 (Authentication) : 사용자의 신원을 확인하는 과정

인가 (Authority) : 특정 리소스에 대한 접근 권한이 있는지 확인하는 과정

 

 

 

Spring Security의 특징

스프링 어플리케이션의 보안 설정을 담당하는 스프링의 하위 프레임 워크로, 인증과 인가 등의 과정을 Filter Chain에서 처리한다.

- spring MVC와 분리되어 관리하고 동작할 수 있다

 

Client(request) ->  WAS -> Filter -> DispatcherServlet -> Interceptor -> Controller

 

Bean으로 설정할 수 있다. 

 

 

 

spring security 아키텍쳐

 

<인증 과정>

1. 사용자 (client)가 로그인을 시도한다. 

2.  AuthenticationFilter가 사용자의 인증 정보를 받아 Authentication 객체를 생성한다.

3. 생성된 Authentication 객체는 AuthenticationManager에게 전달된다.

- 이 때 AuthenticationProvider가 UserDetailsService를 호출하여 사용자 상세 정보를 불러온다.

4. AuthenticationProvider는 loadUserByUsername 메소드를 통해 UserDetailsService에 사용자명을 전달하고, UserDetailssService는 해당 사용자명에 해당하는 UserDetails 객체를 반환한다.

5. UserDetails 객체는 Provider에서 인증을 수행한다.

6. 인증이 성공하면, ProviderManage 에 권한을 담은 토큰을 전달한다.

7. ProviderManager는 AuthenticationFilter를 거쳐 SecurityContextHolder의 SecurityContext에 저장된다.

8. 이후 시스템은 사용자가 인증된 상태임을 인증하고, 사용자의 권한에 따라 접근을 허용한다. 

 

 

Spring Security 에서는 다양한 기능을 가진 필터들을  10개 이상 기본적으로 제공한다.

이렇게 제공되는 필터들을 Security Filter Chain 이라고 말한다. 

 

 

 

[ Authentication ]

- 현재 접근하는 주체의 정보와 권한을 담는 인터페이스이다. 

 

[ SecurityContext ]

- Authentication 을 보관하는 역할을 하며 SecurityContext를 통해 Authentication 객체를 꺼내올 수 있다. 

 

[ SecurityContextHolder]

- 보안 주체의 세부 정보를 포함하여 응용프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장된다.

 

유저의 아이디와 패스워드 사용자 정보를 넣고 실제 가입 된 사용자인지 체크한 후,

인증에 성공하면 사용자의 principal과 credential 정보를 Authentication 안에 담는다. 

스프링 시큐리티에서 방금 담은 Authentication 을 SecurityContext에 보관한다.

 

[ UserDetails ]

인증에 성공하여 생성된 UserDetials 객체는 Authentication 객체를 구현한 UsernamePasswordAuthenticationToken 을 생성하기 위해 사용된다.

 

Spring의 요청 처리 순서

HTTP 요청 -> WAS -> Filter -> Servlet -> Interceptor -> Controller

 

 

JWT + Security 작동 원리 정리 ...

클라이언트에서 서버로 ID/PW로 로그인을 요청한다.

서버에서 검증 과정을 거쳐 해당 유저가 존재하면, Access Token + Refresh Token 을 발급

클라이언트는 요청 헤더에 2번에서 발급받은 Access Token 을 포함하여 API를 요청

 

 

 

JwtAuthenticationFilter는 클라이언트가 요청 시 보낸 JWT 토큰을 검증하고, 해당 사용자의 인증 정보를 SecurityContext에 등록해 주는 역할을 한다.

스프링 시큐리티는 기본적으로 세션 기반 인증을 사용하지만,

JWT 로그인은 토큰 기반 인증이기 때문에, 별도로 커스텀 필터를 만들어서 Security Filter Chain에 추가해줘야 한다.

 

JWT란?

Json Web Token 의 약자로, 당사자 간에 정보를 JSON 기반으로 Claims에 사용자 정보를 담아서 활용하는 Web Token이다.

 

 

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;

@Builder
@Data
@AllArgsConstructor
public class JwtToken {
    private String grantType;
    private String accessToken;
    private String refreshToken;
}

 

 

암호 키 설정

openssl rand -hex 32

 

hex -> base64로 변환

appication.properties에 넣어준다. 

 

package com.example.shopping_mall.config.jwt;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;

import java.io.IOException;

@RequiredArgsConstructor
public class JwtAuthenticationFilter implements Filter {

    private final JwtTokenProvider jwtTokenProvider;

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        // 1. Request Header에서 JWT 토큰 추출
        String token = resolveToken((HttpServletRequest) request);

        // 2. validateToken으로 토큰 유효성 검사
        if (token != null && jwtTokenProvider.validateToken(token)) {
            // 토큰이 유효할 경우 토큰에서 Authentication 객체를 가지고 와서 SecurityContext에 저장
            Authentication authentication = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        chain.doFilter(request, response);
    }

    // Request Header에서 토큰 정보 추출
    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

Security에 filter로 적용할 jwtAuthenticationFilter 작성!

 

 

package com.example.shopping_mall.config;

import com.example.shopping_mall.config.jwt.JwtAuthenticationFilter;
import com.example.shopping_mall.config.jwt.JwtTokenProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import java.util.List;

@Configuration
@EnableWebSecurity //스프링 시큐리티
public class SecurityConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() { // 비밀번호 암호화
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http, JwtTokenProvider jwtTokenProvider) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable) // 세션 저장 X
                .httpBasic(AbstractHttpConfigurer::disable)
                .cors(Customizer.withDefaults())
                .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth -> auth
                        .requestMatchers("/members/sign-in").permitAll() // 로그인 API는 인증 없이 허용
                        .requestMatchers("/members/sign-up").permitAll() // 회원가입도 인증 없이 허용하고 싶다면 추가
                        .anyRequest().authenticated() // 그 외 모든 요청은 인증 필요
                )
                .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class)
                .build();
        return http.build();
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowCredentials(true); // 쿠키, 인증정보 포함 요청 허용
        configuration.setAllowedOrigins(List.of("http://localhost:3000")); // 나중에 프론트와 연결할 때 사용
        configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
        configuration.setAllowedHeaders(List.of("*"));
        configuration.setExposedHeaders(List.of("*"));

        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); // 요청 경로에 대해 CORS 정책 적용
        source.registerCorsConfiguration("/**", configuration);
        return source;
    }

}

security를 위한 Config 작성

 

package com.example.shopping_mall.config.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import java.util.stream.Collectors;

@Component
@RequiredArgsConstructor
public class JwtTokenProvider { // JWT 생성, 검증, 인증 객체 리턴

    @Value("${jwt.secret}")
    private String secretKey;
    private Key key;

    @Value("${jwt.access-expiration-time}")
    private long accessExpirationTime;

    @Value("${jwt.refresh-expiration-time}")
    private long refreshExpirationTime;


    @PostConstruct
    public void init() {
        byte[] secretKeyBytes = Decoders.BASE64.decode(secretKey); // jjwt 사용하여 자동으로 키 생성. Base64로 인코딩 된 문자열
        key = Keys.hmacShaKeyFor(secretKeyBytes);
    }

    public JwtToken generateToken(Authentication authentication) {
        // 권한 가져오기
        String authorities = authentication.getAuthorities().stream()
                .map(GrantedAuthority::getAuthority)
                .collect(Collectors.joining(","));

        long now = new Date().getTime();

        // Access Token : 인증된 사용자의 권한 정보와 만료 시간
        Date accessTokenExpiresIn = new Date(now + accessExpirationTime); // 1시간
        String accessToken = Jwts.builder()
                .setSubject(authentication.getName())
                .claim("auth", authorities)
                .setIssuedAt(new Date(now))
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token : Access Token의 갱신을 위한 토큰
        Date refreshTokenExpiresIn = new Date(now + refreshExpirationTime); // 7일
        String refreshToken = Jwts.builder()
                .setIssuedAt(new Date(now))
                .setExpiration(refreshTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        return JwtToken.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }

    public Authentication getAuthentication(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();

        String username = claims.getSubject();
        String authorities = (String) claims.get("auth");

        List<GrantedAuthority> authorityList = Arrays.stream(authorities.split(","))
                .map(SimpleGrantedAuthority::new)
                .collect(Collectors.toList());

        UserDetails userDetails = new org.springframework.security.core.userdetails.User(username, "", authorityList);
        return new UsernamePasswordAuthenticationToken(userDetails, token, authorityList);
    }


    public boolean validateToken(String token) {
        try{
            Jwts.parserBuilder()
                    .setSigningKey(key)
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

jwt 생성, 검증, 인증 객체 리턴하는 JwtTokenProvider 작성

 

 

jwt.access-expiration-time=3600000 # 1시간
jwt.refrest-expiration-time=604800000 # 7일

로 설정하였다. 

 

 

JWT 발급에 필요한 기반 코드 ✔️

토큰 인증 필터 체인에 등록 ✔️

인증된 사용자의 권한 파싱 ✔️

SecurityContext에 인증 정보 저장 ✔️

'spring & java' 카테고리의 다른 글

Spring + Redis를 이용한 Email 인증 구현  (1) 2025.08.19
Security + OAuth 를 활용한 카카오 로그인 처리  (4) 2025.08.14
Spring Event를 이용한 비동기 이벤트 처리  (0) 2025.02.23
쿠키 & 세션 / JWT  (1) 2024.08.22
JPA 영속성 컨텍스트 / 영속성 전이  (0) 2024.08.13
'spring & java' 카테고리의 다른 글
  • Spring + Redis를 이용한 Email 인증 구현
  • Security + OAuth 를 활용한 카카오 로그인 처리
  • Spring Event를 이용한 비동기 이벤트 처리
  • 쿠키 & 세션 / JWT
zioni
zioni
  • zioni
    jiwon's dev.log
    zioni
  • 전체
    오늘
    어제
    • 분류 전체보기 (76)
      • spring & java (13)
      • Algorithm (14)
      • PS (37)
      • project (3)
      • experience (1)
      • etc (6)
      • study (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Github
  • 공지사항

  • 인기 글

  • 태그

    자바
    백준
    백준2525
    java
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.6
zioni
스프링 시큐티리 + JWT 를 이용한 로그인 처리
상단으로

티스토리툴바