IT/JAVA

Spring Security로 로그인 기능 구현하기 (JWT 포함)

밥알이 2025. 6. 20. 10:39

Spring Security로 로그인 기능 구현하기 (JWT 포함)

Spring Boot로 웹 애플리케이션을 개발할 때, 보안은 필수입니다. 그 중심에 있는 것이 Spring Security이며, 여기에 JWT(JSON Web Token)을 결합하면 세션리스한 인증 시스템을 손쉽게 구축할 수 있습니다.

이 글에서는 Spring Security의 인증/인가 개념을 이해하고, 실제로 JWT 기반 로그인 기능을 구현하는 실습 코드까지 차근차근 알아보겠습니다.

Spring Security의 기본 개념: 인증(Authentication)과 인가(Authorization)

1. 인증(Authentication)이란?

사용자가 누구인지 확인하는 절차입니다. 예: 아이디/비밀번호로 로그인

2. 인가(Authorization)란?

인증된 사용자가 어떤 자원(Endpoint, 기능 등)에 접근할 수 있는지를 판단하는 과정입니다.

JWT란 무엇인가?

JWT (JSON Web Token)은 인증 정보를 담은 토큰입니다. 서버에서 로그인 성공 시 토큰을 발급하고, 이후 클라이언트가 요청할 때마다 헤더에 토큰을 담아 인증하는 방식입니다.

  • 장점: 세션 저장소가 필요 없음 (stateless)
  • 단점: 토큰 탈취 시 위험, 토큰 갱신 필요

Spring Security + JWT 로그인 흐름

  1. 사용자가 ID/PW로 로그인 요청
  2. 서버에서 인증 후 JWT 토큰 발급
  3. 클라이언트는 이후 요청 시 Authorization 헤더에 토큰 포함
  4. 서버는 JWT를 검증하고, 인증된 사용자로 처리

Spring Boot 프로젝트 실습: JWT 로그인 구현

이제 실제 코드로 로그인 기능을 구현해보겠습니다.

1. build.gradle 설정


dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'
}

2. JWT 유틸 클래스


@Component
public class JwtTokenProvider {

    private final String secretKey = "mysecretkey";
    private final long validityInMilliseconds = 3600000; // 1 hour

    public String createToken(String username, List<String> roles) {
        Claims claims = Jwts.claims().setSubject(username);
        claims.put("roles", roles);

        Date now = new Date();
        Date validity = new Date(now.getTime() + validityInMilliseconds);

        return Jwts.builder()
            .setClaims(claims)
            .setIssuedAt(now)
            .setExpiration(validity)
            .signWith(SignatureAlgorithm.HS256, secretKey.getBytes())
            .compact();
    }

    public Authentication getAuthentication(String token) {
        String username = getUsername(token);
        return new UsernamePasswordAuthenticationToken(username, "", List.of());
    }

    public String getUsername(String token) {
        return Jwts.parser()
            .setSigningKey(secretKey.getBytes())
            .parseClaimsJws(token)
            .getBody()
            .getSubject();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(secretKey.getBytes()).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }
}

 

3. 로그인 컨트롤러


@RestController
@RequestMapping("/auth")
public class AuthController {

    private final AuthenticationManager authenticationManager;
    private final JwtTokenProvider jwtTokenProvider;

    public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider) {
        this.authenticationManager = authenticationManager;
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody AuthRequest authRequest) {
        Authentication authentication = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(authRequest.getUsername(), authRequest.getPassword())
        );

        String token = jwtTokenProvider.createToken(authRequest.getUsername(), List.of("ROLE_USER"));

        return ResponseEntity.ok(Map.of("token", token));
    }
}

AuthRequest DTO


public class AuthRequest {
    private String username;
    private String password;
    // getters and setters
}

4. JWT 필터 구현


public class JwtTokenFilter extends OncePerRequestFilter {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtTokenFilter(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
        throws ServletException, IOException {

        String token = resolveToken(request);

        if (token != null && jwtTokenProvider.validateToken(token)) {
            Authentication auth = jwtTokenProvider.getAuthentication(token);
            SecurityContextHolder.getContext().setAuthentication(auth);
        }

        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearer = request.getHeader("Authorization");
        return (bearer != null && bearer.startsWith("Bearer ")) ? bearer.substring(7) : null;
    }
}

5. Security 설정


@Configuration
@EnableWebSecurity
public class SecurityConfig {

    private final JwtTokenProvider jwtTokenProvider;

    public SecurityConfig(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .httpBasic().disable()
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and()
            .authorizeRequests()
            .antMatchers("/auth/**").permitAll()
            .anyRequest().authenticated()
            .and()
            .addFilterBefore(new JwtTokenFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

마무리: Spring Security + JWT로 간결하고 안전한 인증 시스템 구축

Spring Security는 복잡해 보이지만, 핵심 개념을 이해하고 JWT와 결합하면 세션리스한 인증 구조를 비교적 간단하게 구현할 수 있습니다. 이번 글에서 제공한 구조는 실제 실무에서도 활용 가능한 기본 틀이며, 여기에 Refresh Token, 사용자 권한 관리 등을 추가하면 보다 완성도 높은 인증 시스템이 됩니다.

추가로 구현해볼 수 있는 내용:

  • Refresh Token 발급 및 재발급 기능
  • 유저 DB와 연동한 사용자 인증 처리
  • 권한(ROLE_ADMIN, ROLE_USER 등) 기반 인가 처리

보안은 개발의 핵심입니다. Spring Security + JWT로 안전한 애플리케이션을 만들어보세요!