Spring Boot에서 JWT 인증 & Swagger 연동, 그리고 실전 삽질기 (feat. Security 5.x, jjwt 최신버전, 오류 모음)

Heejun Kim·2025년 6월 14일

Spring

목록 보기
6/6


?? 왜 삽질하는 사진 가져왔냐구요?

Spring Boot 2.7.x에서 JWT 인증을 직접 붙이고, Swagger(OpenAPI)를 연동하면서 겪은 다양한 실전 오류와 그 해결법을 모은 실전 삽질 정리했거든요 ㅎㅎㅎ

1. JWT 라이브러리?

jjwt 버전 이슈 & 의존성 추가

최신은 0.12.5(2024년 6월 기준), 그리고 구성도 약간 다르지만 안정적이면서 많은사람들이 사용하고 있는 0.11.5버전 기준으로 작성했습니다

<!-- pom.xml -->
<!-- jjwt 최신버전 의존성 -->
<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>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- json 파싱용 -->
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

jjwt는 api/impl/jackson 이렇게 3개 다 넣어야 제대로 동작하니까 다 넣어주세요

2. JWT 비밀키는?

환경변수 & base64
application.properties에 비밀키(Secret Key)는 직접 작성해도 되고,

한글, 공백, 아무 값이나 넣어도 되는데 base64 인코딩을 거쳐야 함!


jwt.secret.key=ZG9uJ3QgaGF2ZSBNeSBnaXJsZnJpZW5k
echo -n "don't have My girlfriend" | base64 등으로 만들어서 넣기

3. JWT 토큰 생성/파싱 코드

기존 코드의 문제

이왕 의존성 갖다 쓰는거 0.12.x 버전대 쓰려고 했었다
이 버전부터 .setSubject(), .signWith(key, algo) 같은 메서드가 deprecated 되고

최신은 builder 스타일로 .subject(), .claim(), .signWith(key)) 이렇게 set빼고 간략하게 적어도 된다고 해서 쓰려고 했는데 아직 정보도 부족했던 터라서 끙끙 앓다가 다시 0.11.5로 백함..

@Component
public class JwtUtil {


    //Header 키 값
    public static final String AUTHORIZATION_HEADER = "Authorization";
    // 사용자 권한 값의 KEY
    public static final String AUTHORIZATION_KEY = "auth";
    // Token 식별자
    public static final String BEARER_PREFIX = "Bearer ";
    //토큰 만료시간
    private static final long EXPIRATION_TIME = 1000 * 60 * 30; // 30분

    @Value("${jwt.secret.key}") //base64 Encode한 SecretKey
    private String secretKey;
    private Key key;
    private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

    @PostConstruct
    public void init(){
        byte[] bytes = Base64.getDecoder().decode(secretKey);
        key = Keys.hmacShaKeyFor(bytes);
    }

    public String createToken(String username, Role role) {
        Date now = new Date();
        Date expiry = new Date(now.getTime() + EXPIRATION_TIME);

    return Jwts.builder()
            .setSubject(username)
            .claim(AUTHORIZATION_KEY, role.name())
            .setIssuedAt(now)
            .setExpiration(expiry)
            .signWith(key, SignatureAlgorithm.HS256)
            .compact();
}

    private Claims getClaims(String token) {
        // parserBuilder는 0.11.0부터 지원, setSigningKey(key)로 key 직접 전달 가능
        return Jwts.parserBuilder()
                .setSigningKey(key)
                .build()
                .parseClaimsJws(token)
                .getBody();
    }

    public String extractUsername(String token) {
        return getClaims(token).getSubject();
    }

    public boolean validateToken(String token, String username) {
        return extractUsername(token).equals(username) && !isTokenExpired(token);
    }

    private boolean isTokenExpired(String token) {
        return getClaims(token).getExpiration().before(new Date());
    }
}

4. Spring Security와 Swagger 연동

1) SecurityFilterChain에서 Permit 경로 제대로 추가해야함

Swagger UI, docs 경로는 항상 permitAll로 열어둬야 함!

@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
    http.csrf().disable()
        .authorizeRequests()
        .antMatchers("/v3/api-docs/**", "/swagger-ui.html", "/swagger-ui/**", "/api/users/register", "/api/users/login").permitAll()
        .anyRequest().authenticated();
    return http.build();
}

antMatchers 순서 중요한데 맨날 기억나는 순서대로 적다가 헷갈리는 나 자신을 발견했다..
=> anyRequest() 뒤에 antMatchers 쓰면 에러남 기억하자 좀..

아 그리고 로그인이나 회원가입같은 주소를 미리 추가 안하면 403 인증 오류 뜨니 꼭꼭 확인해야한다

2) Swagger에서 Dto 예시가 이상하게 뜨는 현상

Dto 필드가 여러 개인데, Swagger UI에선 한 줄로 뜸

@Schema 어노테이션으로 명시하거나,

Getter/Setter, 생성자 누락, 필드명 대소문자 체크!

5. 회원가입 & 로그인 API 예시

Dto/Entity 패턴 추천
Entity는 setter 최소화, Dto는 builder나 생성자만 써도 됨

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class UserRegisterRequest {
    private String username;
    private String name;
    private String email;
    private String password;
}

회원가입 로직

 public void registerUser(UserRegisterRequest dto) {
        String encodedPassword = passwordEncoder.encode(dto.getPassword());
        User newUser = User.builder()
                .username(dto.getUsername())
                .name(dto.getName())
                .email(dto.getEmail())
                .password(encodedPassword)
                .role(Role.USER)
                .build();
        userRepository.save(newUser);
    }

아래는 로그인 로직, 로그인 후 토큰 생성해서 반환하는데 이후 클라이언트는 헤더에 넣어 사용하도록 해야한다 안그러면 403뜸


public String login(String username, String rawPassword) {
        Optional<User> userOpt = userRepository.findByUsername(username);
        if (userOpt.isPresent()) {
            User user = userOpt.get();
            if (passwordEncoder.matches(rawPassword, user.getPassword())) {
                return jwtUtil.createToken(user.getUsername(), user.getRole());
            }
        }
        return null;
    }

6. 오류 모음 & 해결 과정

  • Data too long for column 'password'

-> 비밀번호 컬럼 길이 50 → 100 이상으로 변경

  • Illegal base64 character

-> secret key가 base64가 아님 → 꼭 base64 인코딩해서 넣기

7. 아무튼

JWT secret key는 base64 인코딩만 하면 공백/한글 상관 없음, 단 보안 중요하니까 여러사람 울릴줄 아는 상남자 되고싶으면 대충해도 됌

토큰 해독해서 서버에서 다시 비밀번호 꺼내쓰는 일은 없음 (userId/username 정도만)

0개의 댓글