반응형

오늘은 RESTful API에 인증(Authentication)과 권한 관리(Authorization)를 추가하는 방법에 대해 다뤄보겠습니다. API를 안전하게 보호하는 것은 실무에서 필수적인 작업이며, 이를 구현하기 위해 Spring Security와 JWT(JSON Web Token)를 사용할 것입니다.


1. 인증과 권한 관리의 기본 흐름

  1. 인증(Authentication): 사용자의 신원을 확인합니다. (e.g., 로그인)
  2. 권한 관리(Authorization): 인증된 사용자가 요청한 자원에 접근 권한이 있는지 확인합니다.

2. JSON Web Token(JWT) 개념

JWT는 클라이언트와 서버 간에 안전하게 정보를 전송하기 위해 사용되는 JSON 기반 토큰입니다.
구성:

  • Header: 토큰 유형과 해싱 알고리즘
  • Payload: 사용자의 인증 정보 및 클레임(Claims)
  • Signature: Header와 Payload를 비밀 키로 서명한 값

3. 프로젝트 설정

필요 라이브러리 추가

build.gradle에 다음 의존성을 추가합니다.

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

4. JWT 유틸리티 클래스 작성

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.security.Keys;

import java.security.Key;
import java.util.Date;

public class JwtUtil {

    private static final Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
    private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(key)
                .compact();
    }

    public static String validateToken(String token) {
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

5. Spring Security 설정

SecurityConfig 클래스

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;

@Configuration
public class SecurityConfig {

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.csrf().disable()
            .authorizeRequests()
            .antMatchers("/api/auth/**").permitAll()
            .anyRequest().authenticated();

        return http.build();
    }

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

6. 인증 컨트롤러 작성

AuthController 클래스

import org.springframework.web.bind.annotation.*;

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

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        // 간단한 사용자 인증 (데모용)
        if ("user".equals(username) && "password".equals(password)) {
            return JwtUtil.generateToken(username);
        }
        throw new RuntimeException("Invalid username or password");
    }
}

7. 사용자 요청 보호하기

JwtFilter 클래스

import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
            throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            String token = authorizationHeader.substring(7);
            String username = JwtUtil.validateToken(token);

            if (username != null) {
                UsernamePasswordAuthenticationToken authToken =
                        new UsernamePasswordAuthenticationToken(username, null, null);
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }
        chain.doFilter(request, response);
    }
}

SecurityConfig에 필터 추가

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http, JwtFilter jwtFilter) throws Exception {
    http.csrf().disable()
        .authorizeRequests()
        .antMatchers("/api/auth/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);

    return http.build();
}

8. 테스트

  1. 로그인 요청
    POST /api/auth/login
    • 요청 본문에 username과 password를 전달합니다.
    • 성공 시 JWT 토큰을 반환합니다.
  2. 보호된 API 요청
    • Authorization 헤더에 Bearer <JWT_TOKEN>을 포함하여 보호된 API를 호출합니다.
    • 유효한 토큰이 없으면 401 Unauthorized 응답을 반환합니다.

9. 실무에서의 고려사항

  1. 사용자 인증 정보: 실제 프로젝트에서는 데이터베이스에서 사용자 정보를 조회하여 인증합니다.
  2. 토큰 재발급(Refresh Token): Access Token이 만료되었을 때 새로 발급하는 메커니즘을 추가합니다.
  3. 토큰 저장 위치: 브라우저의 localStorage 또는 secure cookie를 사용합니다.
반응형

+ Recent posts