Showing

[스프링 부트] 로그인시 인증 토큰 jwt 발급과 스프링 시큐리티 본문

JAVA, SPRING/Spring boot, React

[스프링 부트] 로그인시 인증 토큰 jwt 발급과 스프링 시큐리티

RabbitCode 2023. 6. 8. 14:47

1.  jwt란? 

Json Web Token의 약자로, 전자 서명이 된 토큰

. 을 기준으로 헤더, 페이로드, signature 로 나뉘어져 있다.

// header.payload.signature

header 헤더에는 일반적으로 typ라고 해서 해당 토큰의 타입이 들어있다. 또한 alg라고 해서 토큰을 서명하기 위해 사용된 해시 알고리즘이 들어있다.

payload는 해당 토큰의 주인, iat 즉 토큰이 발행된 시간, exp 토큰이 만료되는 시간이 들어있다.(기본 형태가 그렇다는 뜻)

 

 

 

https://jwt.io/

jwt 홈페이지를 보면 jwt 토큰은 Encoded 안에 있는 형식의 토큰을 만들어주고, DATA payload를 보면 들어갈 데이터도 넣을 수 있다. 또한 토큰의 만료 기한 같은 것들을 설정할 수 있다. 토큰이 만료되면, 더이상 인증을 진행 할 수 없고 정상적인 토큰이 오지 않으면 인증에 실패하도록 서버 로직을 작성해주면 된다.

 

 

2. 의존성 추가(라이브러리 주입)

build.gradle

자바의 jwt인 jjwt 의존성을 불러온다.

implementation group: 'io.jsonwebtoken', name: 'jjwt', version:'0.9.1'

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	implementation 'org.springframework.boot:spring-boot-starter-validation'
	implementation group: 'io.jsonwebtoken', name: 'jjwt', version:'0.9.1'
	compileOnly 'org.projectlombok:lombok'
	runtimeOnly 'mysql:mysql-connector-java'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

3. 스프링 시큐리티, 토큰 발급, 토큰 복호화

지금은 단순히 인증만을 위한 토큰을 만들어주도록 한다.

package com.example.board.security;

import org.springframework.stereotype.Service;

@Service
public class TokenProvider  {
    private static final String SECURITY_KEY = "jwtseckey!@";
}

코드 설명

package com.example.board.security;

import org.springframework.stereotype.Service;

@Service
public class TokenProvider  {
    private static final String SECURITY_KEY = "jwtseckey!@";
}

SECURITY_KEY는 jwt를 만들 적에, 사용할 키이다. 키를 기반으로 데이터를 암호화하고 복호화하는데 사용할 것이다.

 

또한 jwt를 생성하는 create 메서드를 작성해준다.

@Service
public class TokenProvider  {
    private static final String SECURITY_KEY = "jwtseckey!@";
    public String create (String userEmail){
        //jwt 를 만들어준다.
        Data exprTime = (Data) Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        //현재 시간에 플러스 1한 시간에 만료시간 지정
        return Jwts.builder()
                .signWith(SignatureAlgorithm.ES512, SECURITY_KEY)
                .setSubject(userEmail).setIssuedAt(new Date()).setExpiration(exprTime)
                .compact();
    }
}

코드 설명

@Service
public class TokenProvider  {
    private static final String SECURITY_KEY = "jwtseckey!@";
    public String create (String userEmail){
        //jwt 를 만들어준다.
        Data exprTime = (Data) Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        //현재 시간에 플러스 1한 시간에 만료시간 지정
        return Jwts.builder() jwt 생성 메서드
                .signWith(SignatureAlgorithm.ES512, SECURITY_KEY)
                .setSubject(userEmail).setIssuedAt(new Date()).setExpiration(exprTime)
                .compact();
    }
} // 나중에 작업하다가 필요한 데이터들 더 추가해주는 식으로 작성하면 된다.

 

- Instant.now() 현재 시간에 플러스 1한 시간 : (Data) Date.from(Instant.now().plus(1, ChronoUnit.HOURS))

- 주입해둔 라이브러리 jwt를 이용 :

Jwts로 빌더를 만들어준다.

signWith에다가 ES512 알고리즘을 지정해준다. 그리고 만들어둔 SECURITY_KEY를 쓴다. 

setSubject에 유저 이메일을 넣어주고, 

생성날짜는 setIssuedAt(new Date())를 통해 지정해주고,

만료기한(만료일)까지 setExpiration(exprTime)로 지정해준다.

.compact()로 jwt를 만들어서 보내도록 한다.

 

 

키를 복호화하는 메서드도 작성해준다.

    public String validate(String token){
        //Claims로 jwt를 파싱하도록 한다.
        // setSigningKey : 파싱하는 기준은 setSigningKey 메소드 이용해서 만들어둔 SECURITY_KEY를 이용한다.
        // 마지막에 데이터바디를 받아오도록 한다.
        Claims claims = Jwts.parser().setSigningKey(SECURITY_KEY).parseClaimsJws(token).getBody();
        //복호화해서 파싱된 claims는 당초 .setSubject(userEmail) 해준 것이기 때문에
        return claims.getSubject(); //지정되어 있는 Subject를 받아올 수 있다
    }

지금까지의 코드

package com.example.board.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Base64;
import java.util.Date;

@Service
public class TokenProvider  {
    //JWT 생성 및 검증을 위한 키
    private static final String SECURITY_KEY = "jwtseckey!@";
    //JWT를 생성하는 메서드
    public String create (String userEmail){
        Date exprTime = Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        byte[] encodedKey = Base64.getEncoder().encode(SECURITY_KEY.getBytes());
        SecretKey secretKey = new SecretKeySpec(encodedKey, SignatureAlgorithm.HS512.getJcaName());

        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512,secretKey)
                .setSubject(userEmail)
                .setIssuedAt(new Date())
                .setExpiration(exprTime)
                .compact();
    }
    // JWT 검증
    public String validate(String token){
        // 매개변수로 받은 token을 키를 사용해서 복호화(디코딩=파싱)
        //Claims로 jwt를 파싱하도록 한다.
        // setSigningKey : 파싱하는 기준은 setSigningKey 메소드 이용해서 만들어둔 SECURITY_KEY를 이용한다.
        // 마지막에 데이터바디를 받아오도록 한다.
        Claims claims = Jwts.parser().setSigningKey(SECURITY_KEY).parseClaimsJws(token).getBody();
        //복호화해서 파싱된 claims는 당초 .setSubject(userEmail) 해준 것이기 때문에
        // 복호화된 토큰의 payload에서 제목을 가져온다
        return claims.getSubject(); //지정되어 있는 Subject를 받아올 수 있다
    }
}

* 아래 코드 설명과 실제 위의 코드는 많이 다릅니다! 위의 코드에서는 signWith 메서드에 secretKeySignatureAlgorithm.HS512를 함께 전달하여 JWT에 서명을 추가하는 방식으로 변경하였습니다. *

package com.example.board.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import lombok.Data;
import org.springframework.stereotype.Service;

import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.Date;

@Service
public class TokenProvider  {
    //JWT 생성 및 검증을 위한 키
    private static final String SECURITY_KEY = "jwtseckey!@";
    //JWT를 생성하는 메서드
    public String create (String userEmail){
        //jwt 를 만들어준다. 만료 날짜를 현재 날짜 + 1시간으로 설정
        Date exprTime = Date.from(Instant.now().plus(1, ChronoUnit.HOURS));
        //현재 시간에 플러스 1한 시간에 만료시간 지정
        // JWT를 생성
        return Jwts.builder()
                // 암호화에 사용될 알고리즘, 키
                .signWith(SignatureAlgorithm.ES512, SECURITY_KEY)
                // JWT 제목, 생성일, 만료일을 넣어준다
                .setSubject(userEmail).setIssuedAt(new Date()).setExpiration((Date) exprTime)
                // 생성
                .compact();
    }
    // JWT 검증
    public String validate(String token){
        // 매개변수로 받은 token을 키를 사용해서 복호화(디코딩=파싱)
        //Claims로 jwt를 파싱하도록 한다.
        // setSigningKey : 파싱하는 기준은 setSigningKey 메소드 이용해서 만들어둔 SECURITY_KEY를 이용한다.
        // 마지막에 데이터바디를 받아오도록 한다.
        Claims claims = Jwts.parser().setSigningKey(SECURITY_KEY).parseClaimsJws(token).getBody();
        //복호화해서 파싱된 claims는 당초 .setSubject(userEmail) 해준 것이기 때문에
        // 복호화된 토큰의 payload에서 제목을 가져온다
        return claims.getSubject(); //지정되어 있는 Subject를 받아올 수 있다
    }
}

 

4. AuthService단에 만든 토큰 지정

    public ResponseDto<SignInResponseDto> signIn(SignInDto dto){
    //해당 레포지토리에 아이디, 비밀번호 해당하는 값들이 존재하는지
        String userEmail = dto.getUserEmail();
        String userPassword = dto.getUserPassword();
        try {
            boolean existed = userRepository.existsByUserEmailAndUserPassword(userEmail, userPassword);
            if(!existed) return ResponseDto.setFailed("Sign In Information Does Not Match");
        } catch (Exception error) {
            return ResponseDto.setFailed("Database Error");
        }
        UserEntity userEntity = null;
        try {
            userEntity = userRepository.findById(userEmail).get();
        } catch (Exception error) {
            return ResponseDto.setFailed("Database Error");
        }

        userEntity.setUserPassword("");
        //토큰에는 인증할 수 있는 데이터를 넣어주어야 한다.
        //인증은 베이직 인증과 vali.. 토큰을 쓸 것인데, vali.. 토큰 중에서 jwt를 사용해서 인증처리를 할 것이다.
        String token = "";
        int exprTime = 360000;

        SignInResponseDto signInResponseDto = new SignInResponseDto(token, exprTime, userEntity);
        return ResponseDto.setSuccess("Sign In Success", signInResponseDto);
    }

token = "" 이 부분을 고쳐주면 된다.

변경점

String token = tokenProvider.create(userEmail);

5. 포스트맨 테스트

실제 데이터베이스에 있는 유저 가짜 데이터를 활용해서 포스트맨에서 토큰이 잘 발급되는지 확인해보도록 한다.

토큰이 잘 발급되었다! (참고로 테스트 서버는 8079 포트이다)

발급된 jwt를 jwt.io에 넣어 decode 할 수 있다.

{
  "sub": "dfadsf", //이메일 가짜값
  "iat": 1686208019, //시작날짜
  "exp": 1686211619 //종료날짜
}

이렇게 토큰을 탈취하면 데이터를 바로 볼 수가 있다. 지금은 키를 사용해서 얘가 맞는 애인지 틀린 애인지 검증만 가능하고, 실제 데이터는 다 보이게 된다. 실제로 토큰에 넣는 값에는 밖으로 나가면 안되는 데이터들, 개인정보 같은 것들은 jwt에 넣으면 안된다. 우리가 밖으로 나가도 된다고 생각하는 것들을 넣어주자.

 

 

지금까지, 백엔드에서 로그인해서 jwt 만들어서 반환하는 기능을 만들어보았다.