JWT 사용해 보기
JWT
(Json Web Token)
서버에 세션 정보를 저장하지 않고, 로그인 시 클라이언트에게 로그인 사용자 정보가
포함된 토큰을 발행하고, 클라이언트는 서버에 어떤 작업을 요청할 때마다 이 토큰을 같이 보내고,
서버는 이 토큰에 포함된 사용자 정보를 이용해서 authentication, authorization을 처리하는 방식이다.
처리방식
1. 사용자 로그인 시 서버는 사용자 인증을 완료하고 외부에 노출되어도 문제가 없는
인증 관련 정보(사용자 ID, 권한 등)를 JSON 형태로 만든다.
(Payload)
2. JSON형태의 Payload를 base64 인코딩을 하여 문자열을 만들고, 미리 정한 시스템의
SecretKey(전체 시스템에서 사용하는 암호)를 이용하여 서명 문자열을 생성한다.
3. Header의 정보, 인증정보(Payload), 서명 문자열을 하나의 문자열로 합친 후 클라이언트로 전송한다.
4. 서버는 인증 요청에 대한 응답으로 인코딩 된 문자열을 클라이언트로 전달한다.
5. 클라이언트는 서버로부터 받은 토큰을 클라이언트의 저장공간
(브라우저의 경우 쿠키나 브라우저내 로컬 스토리지 등)에 저장한다.
6. 클라이언트는 매번 요청 시 이 토큰을 서버로 같이 전달한다.
7. 서버는 클라이언트의 요청에서 받은 토큰 값을 이용하여 어떤 사용자의 요청인지 등을 확인한다.
내용 참고 - pipit -
JWT의 장점
- 동시 접속자가 많을 때 서버 측의 부하를 낮춘다.
- Client, Sever가 다른 도메인을 사용할 때
ex) 카카오 OAuth2.0 로그인 시 JWT Token을 사용한다.
JWT의 단점
- 구현의 복잡도 증가
- JWT 에 담는 내용이 커질수록 네트워크의 비용이 증가(클라이언트 -> 서버)
- 이미 생성된 JWT를 일부만 만료시킬 방법이 없다.
- Secret key 유출 시 JWT 조작 가능
내용 출처 - 스파르타 코딩클럽 -
JWT 구현해보기
SpringBoot생성 후 build.gradle의 dependencies에 추가
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
시크릿 키 생성과 관련하여 괜찮은 사이트를 찾았다.
생성한 시크릿 키는 application.properties에 넣어주면 된다.
jwt.secret.key = <시크릿 키>
토큰 생성에 필요한 값
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
private static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private static final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}")
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);
}
AUTHORIZATION_HEADER 가 "Authorization"으로 되어있다.
Postman으로 확인 시 KEY 값을 "Authorization"으로 하면 된다.
header에서 토큰 값을 가져온다.
// header 토큰을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
JWT 생성
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
BEARER_PREFIX 는 위에서 "Bearer"이었으므로
사용자의 이름, 권한등의 담을 정보를 가져와서
Bearer가 붙은 값으로 토큰을 생성한다.
토큰이 유효한지 알 수 있다.
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
토큰이 유효하지 않는다면 동작하지 않게 하는 코드라고 생각하면 된다.
유저의 정보가 createToken() 메서드를 거쳐 토큰이 만들어졌는데
이 토큰을 UserRepository에 저장된다면
나중에 이를 이용하여 유효한 값인지 아닌지를 판별할 수 있게 되는 것이다.
JwtUtil.java
package com.sparta.myselectshop.jwt;
import com.sparta.myselectshop.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_KEY = "auth";
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}")
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);
}
// header 토큰을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
CRUD에 JWT를 적용해 보기
환경
SpringBoot 3.0.5
JDK 17
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation group: 'org.json', name: 'json', version: '20220924'
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
aplication.properties
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=
spring.thymeleaf.cache=false
jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
1. 회원가입
2. 로그인
위의 이미지처럼 처리를 해주면 로그인을 한 상태라는 것이 가정된다.
H2 데이터 베이스에도 로그인 정보가 들어와 있다.
3. 로그인 상태로 게시글 작성하기
게시글이 아이디와 함께 잘 저장되었다.
4. 유효하지 않은 토큰으로 게시글 삭제해 보기
4번째 생성된 BOARDS를 삭제해 볼 것이다.
토큰이 유효하지 않아 예외처리가 되었다.
5. 유효한 토큰으로 게시글 삭제해 보기
토큰을 원래대로 돌리면 삭제 처리가 된다.
'자바 탐구' 카테고리의 다른 글
스프링 부트) Dependency : Spring Boot DevTools (0) | 2023.04.21 |
---|---|
인텔리제이) FAILURE : Build failed with an exception. (0) | 2023.04.21 |
정규표현식) 메타문자 - * + () 써보기 (0) | 2023.04.12 |
정규표현식) 메타문자 ^ . [] 써보기 (0) | 2023.04.11 |
자바) JVM (0) | 2023.04.09 |