사용자 인증 및 계정 관리 | JWT 토큰 기반 로그인 API 구현

Faithful Dev·2025년 3월 15일

매장 예약 서비스

목록 보기
3/15

구현한 기능

  • JWT 토큰 기반 로그인 API 개발
  • 사용자/파트너 통합 인증 시스템 구현
  • Spring Security와 JWT 토큰을 활용한 인증 필터 구현
  • 토큰 검증 및 권한 부여 메커니즘 구현

해결한 문제점

SignatureAlgorithm 매개변수 오류

'hmacShaKeyFor(byte[])' in 'io.jsonwebtoken.security.Keys' cannot be applied to '(io.jsonwebtoken.SignatureAlgorithm)'

JJWT 라이브러리 0.11.x 버전에서 API가 변경되었는데, 이전 버전의 메서드를 사용하려 해서 오류가 발생했다.
새 버전의 API에 맞게 코드를 수정하였다.

// 변경 전
this.key = Keys.secretKeyFor(SignatureAlgorithm.HS512);

// 변경 후
this.key = Keys.hmacShaKeyFor(secretFromEnv.getBytes());

JWT 의존성 주입 및 빈 생성 오류

Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter' defined in file [...]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed

JwtAuthenticationFilter 클래스가 생성자 주입 방식으로 JwtUtil을 의존하고 있었는데, JwtUtil 빈이 제대로 생성되지 않았다.
생성자 주입 방식에서 필드 주입 방식으로 변경하여 의존성 주입 문제를 해결하였다.

// 변경 전
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    private final JwtUtil jwtUtil;
    // ...
}

// 변경 후
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    // ...
}

JWT 시크릿 키 설정 불일치 문제

org.springframework.util.PlaceholderResolutionException

application.yml에는 spring.jwt.secret으로 설정되어 있었는데, JwtUtil 클래스에서는 @Value("${jwt.secret}")으로 접근하려 해서 불일치가 발생했다.
JwtUtil 클래스의 어노테이션을 application.yml 구조에 맞게 수정하였다.

// 변경 전
@Value("${jwt.secret}")
private String secretFromEnv;

// 변경 후
@Value("${spring.jwt.secret}")
private String secretFromEnv;

JWT 인증 흐름 코드

JWT 유틸리티 클래스

@Component
public class JwtUtil {
    private static final long JWT_TOKEN_VALIDITY = 24 * 60 * 60 * 1000; // 24시간
    
    @Value("${spring.jwt.secret}")
    private String secretFromEnv;
    
    private Key key;

    @PostConstruct
    public void init() {
        this.key = Keys.hmacShaKeyFor(secretFromEnv.getBytes());
    }

    public String extractEmail(String token) {
        return extractClaim(token, Claims::getSubject);
    }

    public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
        final Claims claims = extractAllClaims(token);
        return claimsResolver.apply(claims);
    }

    public String generateToken(String email, String role) {
        Map<String, Object> claims = new HashMap<>();
        claims.put("role", role);
        return createToken(claims, email);
    }

    private String createToken(Map<String, Object> claims, String subject) {
        return Jwts.builder()
                .setClaims(claims)
                .setSubject(subject)
                .setIssuedAt(new Date(System.currentTimeMillis()))
                .setExpiration(new Date(System.currentTimeMillis() + JWT_TOKEN_VALIDITY))
                .signWith(key)
                .compact();
    }
    
    // 기타 토큰 검증 메서드들...
}

인증 서비스 클래스

@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserRepository userRepository;
    private final PartnerRepository partnerRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtUtil jwtUtil;

    @Transactional(readOnly = true)
    public AuthDto.LoginResponse login(AuthDto.LoginRequest request) {
        // 일반 사용자 먼저 확인
        User user = userRepository.findByEmail(request.getEmail()).orElse(null);
        
        if (user != null) {
            // 일반 사용자 비밀번호 검증
            if (!passwordEncoder.matches(request.getPassword(), user.getPassword())) {
                throw new CustomException(ErrorCode.INVALID_PASSWORD);
            }
            
            // 토큰 생성
            String token = jwtUtil.generateToken(user.getEmail(), user.getRole().name());
            
            return AuthDto.LoginResponse.builder()
                    .token(token)
                    .role(user.getRole().name())
                    .id(user.getId())
                    .email(user.getEmail())
                    .name(user.getName())
                    .build();
        } 
        
        // 파트너 확인
        Partner partner = partnerRepository.findByEmail(request.getEmail())
                .orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));
        
        // 비밀번호 검증 및 토큰 발급 로직...
    }
}

JWT 인증 필터

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
    @Autowired
    private JwtUtil jwtUtil;
    
    private static final String HEADER_STRING = "Authorization";
    private static final String TOKEN_PREFIX = "Bearer ";

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        String header = request.getHeader(HEADER_STRING);
        
        if (header == null || !header.startsWith(TOKEN_PREFIX)) {
            filterChain.doFilter(request, response);
            return;
        }
        
        String token = header.substring(TOKEN_PREFIX.length());
        
        try {
            String email = jwtUtil.extractEmail(token);
            
            if (email != null && SecurityContextHolder.getContext().getAuthentication() == null) {
                String role = jwtUtil.extractClaim(token, claims -> claims.get("role", String.class));
                
                UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(
                        email, null, Collections.singletonList(new SimpleGrantedAuthority(role))
                );
                
                authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
                SecurityContextHolder.getContext().setAuthentication(authenticationToken);
            }
        } catch (Exception e) {
            SecurityContextHolder.clearContext();
        }
        
        filterChain.doFilter(request, response);
    }
}

Postman 테스트


구현 예정

  • 회원정보 조회/수정 API
profile
Turning Vision into Reality.

0개의 댓글