반응형

JWT(JSON Web Token)를 활용하여 전자정부프레임워크에서 토큰 기반 인증을 구현하는 방법을 학습합니다. JWT는 클라이언트와 서버 간의 인증 및 권한 관리를 위한 효율적이고 안전한 방식입니다.


1. JWT란?

JWT는 JSON 포맷을 사용해 정보를 안전하게 전달하는 토큰입니다.

  • 구조:
    1. Header: 토큰 타입(JWT)과 서명 알고리즘(예: HMAC, RSA)
    2. Payload: 클레임(Claims)이라고 불리는 사용자 데이터
    3. Signature: 헤더와 페이로드를 암호화하여 생성

토큰 예시:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
.eyJzdWIiOiJ1c2VyMTIzIiwicm9sZSI6IlVTRVIiLCJpYXQiOjE2MjAwMDAwMDAsImV4cCI6MTYyMDAwMzYwMH0
.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

2. JWT 활용 인증 구조

  1. 클라이언트가 서버에 로그인 요청을 보냄.
  2. 서버는 로그인 정보 검증 후 JWT 발급.
  3. 클라이언트는 JWT를 저장(LocalStorage, SessionStorage 등)하고 요청 시마다 헤더에 첨부.
  4. 서버는 JWT를 검증하여 사용자 인증 수행.

3. JWT 기반 인증 구현

3-1. 의존성 추가

pom.xml에 JWT 라이브러리를 추가합니다.

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
</dependency>

3-2. JWT 유틸리티 클래스 작성

JwtUtil.java

import io.jsonwebtoken.*;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtUtil {

    private static final String SECRET_KEY = "secretKey";
    private static final long EXPIRATION_TIME = 1000 * 60 * 60; // 1시간

    // 토큰 생성
    public String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRATION_TIME))
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    // 토큰 검증
    public boolean validateToken(String token) {
        try {
            Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    // 사용자 이름 추출
    public String extractUsername(String token) {
        return Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody()
                .getSubject();
    }
}

3-3. 컨트롤러 작성

AuthController.java

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

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

    private final JwtUtil jwtUtil;

    public AuthController(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

    @PostMapping("/login")
    public String login(@RequestParam String username, @RequestParam String password) {
        // 실제로는 DB에서 사용자 인증 정보를 확인해야 합니다.
        if ("admin".equals(username) && "password".equals(password)) {
            return jwtUtil.generateToken(username);
        }
        return "인증 실패";
    }

    @GetMapping("/validate")
    public String validateToken(@RequestParam String token) {
        if (jwtUtil.validateToken(token)) {
            return "유효한 토큰";
        }
        return "유효하지 않은 토큰";
    }
}

3-4. JWT 필터 작성

JwtFilter.java

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

import javax.servlet.FilterChain;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class JwtFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    public JwtFilter(JwtUtil jwtUtil) {
        this.jwtUtil = jwtUtil;
    }

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

        String username = null;
        String jwt = null;

        if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            jwt = authorizationHeader.substring(7);
            username = jwtUtil.extractUsername(jwt);
        }

        if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
            if (jwtUtil.validateToken(jwt)) {
                UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                        username, null, new ArrayList<>());
                authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authToken);
            }
        }

        chain.doFilter(request, response);
    }
}

4. 테스트

  1. 로그인 요청:
    • URL: /auth/login
    • 파라미터: username=admin&password=password
    • 응답: JWT 토큰 반환
  2. 유효성 검증:
    • URL: /auth/validate
    • 파라미터: token=<발급받은 JWT>
    • 응답: "유효한 토큰" 또는 "유효하지 않은 토큰"
  3. 보호된 리소스 요청:
    • Authorization 헤더에 Bearer <JWT>를 포함하여 요청.

5. 마무리

JWT 기반 인증은 서버 세션에 의존하지 않으므로 확장성이 높고, 분산 시스템에서 효율적입니다. 다음에서는 API 서버와 클라이언트 간의 데이터 암호화 및 보안에 대해 학습합니다.

반응형

+ Recent posts