스프링 JWT 서비스 구현하기
본문 바로가기

웹/Spring

스프링 JWT 서비스 구현하기

728x90
반응형

안녕하세요 놀이방 사장입니다.

 

이번 포스팅은 JWT서비스를 직접 구현해보겠습니다.

JWT를 생성하고 검증하는 서비스를 구현합니다.

의존성과 토큰 제공자를 추가하고

리프레시 토큰 도메인과 토큰 필터를 구현하면 JWT 서비스를 사용할 수 있다.

 

1. 의존성 추가

build.gradle에 추가하기

implementation 'io.jsonwebtoken:jjwt:0.9.1' // JWT구현
implementation 'javax.xml.bind:jaxb-api:2.3.1' // XML문서와 Java 객체 간 매핑 자동화

runtimeOnly 'com.h2database:h2'

compileOnly 'org.projectlombok:lombok'
 

4개를 추가해줍니다.

 

2. 토큰 제공자 추가하기

JWT 토큰을 발급하기 위해서는 이슈발급자, 비밀키를 필수로 설정해야함

yml에 설정해줍니다.

jwt :
  issuer : ajufresh@gmail.com
  secret_key : study_springboot

 

 

3. 해당값들을 변수로 접근하는데 사용할 JwtProperties 클래스를 만든다.

config/jwt 패키에 JwtProperties.java 파일을 만든다.

package me.joyeonggyu.springbootdeveloper.config.jwt;

import lombok.Getter;
import lombok.Setter;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Setter
@Getter
@Component
@ConfigurationProperties("jwt") //자바 클래스에 프로피티값을 가져와서 사용하는 애너테이션
public class JwtProperties {
    private String issuer;
    private String secretKey;
}

 

 

4. 계속해서 토큰을 생성하고 올바른 토큰인지 유효성 검사를 하고 토큰에서 필요한 정보를 가져오는 클래스를 작성한다.

TokenProvider.java 파일을 생성하고 config/jwt 디렉터리에 생성

package me.joyeonggyu.springbootdeveloper.config.jwt;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.RequiredArgsConstructor;
import me.joyeonggyu.springbootdeveloper.domain.User;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Service;

import java.time.Duration;
import java.util.Collections;
import java.util.Date;
import java.util.Set;

@RequiredArgsConstructor
@Service
public class TokenProvider {
    private final JwtProperties jwtProperties;

    public String generateToken(User user, Duration expiredAt){
        Date now = new Date();
        return makeToken(new Date(now.getTime()+expiredAt.toMillis()), user);
    }

    // JWT 토큰 생성 메서드
    public String makeToken(Date expiry, User user){
        Date now = new Date();

        return Jwts.builder()
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)   // 헤더 typ : JWT
                //내용 iss : ajurefresh@gmail.com(propertise파일에서 설정한 값)
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(now)   //내용 iat : 현재 시간
                .setExpiration(expiry)  // 내용 exp : expiry 멤버 변수값
                .setSubject(user.getEmail())    // 내용 sub : 유저의 이메일
                .claim("id" , user.getId()) // 클레임 : id , 유저ID
                // 서명 : 비밀값과 함께 해시값을 HS256 방식으로 암호화
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }

    // JWT 토큰 유효성 검증 메서드
    public boolean validToken(String token){
        try{
            Jwts.parser()
                    .setSigningKey(jwtProperties.getSecretKey())    //비밀값으로 복화화
                    .parseClaimsJws(token);
            return true;
        }catch(Exception e){    //복호화 과정에서 에러가 나면 유효하지 않은 토큰
            return false;
        }
    }

    // 토큰 기반으로 인증 정보를 가져오는 메서드
    public UsernamePasswordAuthenticationToken getAuthentication(String token){
        Claims claims = getClaims(token);
        Set<SimpleGrantedAuthority> authorities = Collections.singleton(new
                SimpleGrantedAuthority("ROLE_USER"));

        return new UsernamePasswordAuthenticationToken(new org.springframework.
                security.core.userdetails.User(claims.getSubject
                (), "", authorities), token, authorities);
    }

    //토큰 기반으로 유저 ID를 가져오는 메서드
    public Long getUserId(String token){
        Claims claims = getClaims(token);
        return claims.get("id", Long.class);
    }

    public Claims getClaims(String token){
        return Jwts.parser()    //클레임 조회
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody();
    }
}
makeToken

토큰을 생성하는 메소드 

인자는 만료시간, 유저정보를 받는다.

이 메서드에서는 set계열의 메서드를 통해 여러값을 지정

헤더는 typ(타입) 

내용은 iss(발급자) , iat(발급일시) , exp(만료일시), sub(토큰 제목)이 클레임에는 유저ID를 지정한다.

토큰을 만들때는 프로퍼티즈 파일에 선언해둔 비밀값과 함께 HS256방식으로 암호화한다.

 

validToken

토큰이 유효한지 검증하는 메서드이다.

프로퍼티즈 파일에 선언한 비밀값과 함꼐 토큰 복호화를 진행

복호화 과정에서 에러가 발생하면 유효하지 않은 토큰이므로 false 반환

아무 에러도 발생하지 않으면 true 반환

 

getAuthentication

토큰을 받아 인증 정보를 담은 객체 Authentication을 반환하는 메서드이다.

프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 뒤 클레임을 가져오는 private 메서드인 getClaims()를 호출해서 클레임 정보를 반환받아 사용자 이메일이 들어있는 토큰 제목 sub와 토큰 기반으로 인증 정보를 생성한다.

UsernamePasswordAuthenticationToken

여기에 첫번쨰 인자로는 User 프로젝트에서 만든 User클래스가 아닌 스프링 시큐리티에서 제공하는 객체인User클래스를 임포트해야한다.

 

getUserId

토큰 기반으로 사용자ID를 가져오는 메서드이다.

프로퍼티즈 파일에 저장한 비밀값으로 토큰을 복호화한 다음 클레임을 가져오는 private 메서드인 getClaims()를 호출해서 클레임 정보를 반환받고 클레임에서 id키로 저장된 값을 반환한다.

 

 

테스트 코드 작성하기

test디렉터리에 config.jwt패키지를 만들고 JwtFactory.java 파일을 생성

package me.joyeonggyu.springbootdeveloper.cotroller.confing.jwt;

import lombok.Getter;

import io.jsonwebtoken.Header;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Builder;
import lombok.Getter;
import me.joyeonggyu.springbootdeveloper.config.jwt.JwtProperties;

import java.time.Duration;
import java.util.Date;
import java.util.Map;

import static java.util.Collections.emptyMap;
@Getter
public class JwtFactory {
    private String subject = "test@gamil.com";
    private Date issuedAt = new Date();
    private Date expiration = new Date(new Date().getTime() + Duration.ofDays(14).toMillis());
    private Map<String, Object> claims = emptyMap();

    @Builder
    public JwtFactory(String subject, Date issuedAt, Date expiration,
                      Map<String, Object> claims){
        this.subject = subject != null ? subject : this.subject;
        this.issuedAt =issuedAt != null ? issuedAt : this.issuedAt;
        this.expiration = expiration != null ? expiration : this.expiration;
        this.claims = claims != null ? claims : this.claims;
    }

    public String createToken(JwtProperties jwtProperties){
        return Jwts.builder()
                .setSubject(subject)
                .setHeaderParam(Header.TYPE, Header.JWT_TYPE)
                .setIssuer(jwtProperties.getIssuer())
                .setIssuedAt(issuedAt)
                .setExpiration(expiration)
                .addClaims(claims)
                .signWith(SignatureAlgorithm.HS256, jwtProperties.getSecretKey())
                .compact();
    }
}

 

같은 디렉터리에 TokenProviderTest 만들

package me.joyeonggyu.springbootdeveloper.cotroller.confing.jwt;

import io.jsonwebtoken.Jwts;
import me.joyeonggyu.springbootdeveloper.config.jwt.JwtProperties;
import me.joyeonggyu.springbootdeveloper.config.jwt.TokenProvider;
import me.joyeonggyu.springbootdeveloper.domain.User;
import me.joyeonggyu.springbootdeveloper.repository.UserRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.core.userdetails.UserDetails;

import java.time.Duration;
import java.util.Date;
import java.util.Map;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

@SpringBootTest
public class TokenProviderTest {
    @Autowired
    private TokenProvider tokenProvider;
    @Autowired
    private UserRepository userRepository;
    @Autowired
    private JwtProperties jwtProperties;

    //generateToken() 검증 테스트
    @DisplayName("generateToken() 검증 테스트 : 유저 정보와 만료기간을 전달해 토큰을 만들 수 있다.")
    @Test
    void generateToken(){
        // given
        User testUser = userRepository.save(User.builder()
                .email("user@gmail.com")
                .password("test")
                .build());
        // when
        String token = tokenProvider.generateToken(testUser, Duration.ofDays(14));

        //then
        Long userId = Jwts.parser()
                .setSigningKey(jwtProperties.getSecretKey())
                .parseClaimsJws(token)
                .getBody()
                .get("id", Long.class);

        assertThat(userId).isEqualTo(testUser.getId());
    }
    @DisplayName("validToken(): 만료된 토큰인 때에 검증에 실패")
    @Test
    void validToken_invalidToken(){
        //given
        String token = JwtFactory.builder()
                .expiration(new Date(new Date().getTime() - Duration.ofDays(7).toMillis()))
                .build()
                .createToken(jwtProperties);
        // when
        boolean result = tokenProvider.validToken(token);
        //then
        assertThat(result).isFalse();
    }

    @DisplayName("getAuthentication() : 토큰 기반으로 정보를 가져온다.")
    @Test
    void getAuthentication(){
        //given
        String userEmail = "user@email.com";
        String token = JwtFactory.builder()
                .subject(userEmail)
                .build()
                .createToken(jwtProperties);

        //when
        Authentication authentication = tokenProvider.getAuthentication(token);
        //then
        assertThat(((UserDetails) authentication.getPrincipal()).getUsername()).isEqualTo(userEmail);

    }

    @DisplayName("getUserId() : 토큰으로 유저 ID를 가져올 수 있다.")
    @Test
    void getUserId(){
        //given
        Long userId = 1L;
        String token = JwtFactory.builder()
                .claims(Map.of("id", userId))
                .build()
                .createToken(jwtProperties);

        //when
        Long userIdByToken = tokenProvider.getUserId(token);

        // then
        assertThat(userIdByToken).isEqualTo(userId);
    }
}

 

코드 설명

generateToken

given : 토큰에 유저정보를 추가하기 위해 테스트 유저를 만든다.

when : 토큰 제공자의 generateToken() 메서드를 호출해 토큰을 만든다.

then : jjwt 라이브러리를 사용해 복호화한다.

토큰을 만들 때 클레임으로 넣어둔 id값이 given 절에서 만든 유저id과 동일한지 확인

 

validToken_invalidToken

토큰이 유효한지 확인하는 메서드 중 검증 실패를 확인하는 메소드

given : jjwt 라이브러리를 사용해서 토큰을 생성한다. 이 때 만료시간을 1970년 1월1일부터 현재시간을 밀리초 단위로 치환한 값에 1000을 빼 , 이미 만료된 토큰으로 생성

when : 토큰 제공자의 vaildToken()메서드를 호출해 유효한 토큰인지 검증하고 반환값을 받음

then : 반환받은 값이 거짓인지 확

getAuthentication

토큰을 전달받아 인증정보를 담은 객체 Authentication을 반환하는 메서드

given : jjwt 라이브러리를 사용해서 토큰을 생성한다. 이 때 토큰의 제목 subject는  "user@gmail.com"라는 값 사용

when : 토큰 제공자의 getAuthentication()메서드를 호출해 인증 객체를 받는다.

then :반환받은 인증 객체의 유저이름을 가져와 given절에서 설정한 subject값인 " user@gmail.com " 과 같은지 확

getUserId

given : jjwt 라이브러리를 사용해서 토큰을 생성한다 클레임은 키는 id 값은 1 이라는 유저의 ID임

when : 토큰 제공자의 getUserId() 메서드를 호출해 유저 ID를 반환받는다.

then : 반환받은 유저 ID가 given절에서 설정한 유저 ID값인 1과 같은지 확인한다.

 

테스트 코드를

돌려서 확인하시면 됩니다.

반응형