JWT TOKEN이란?
JWT(Json Web Token)는 json 형식의 웹 토큰입니다.
예전에는 주로 세션 기반으로 로그인 인증을 많이 사용 하였는데, 최근에 프로젝트는 MSA(마이크로 서비스 아키텍처)로 구현을 하기 시작 하였습니다.
도커 기반으로 이미지를 생성하고 쿠버네티스에 배포를 하여 동시 접속자가 몰리면 스케일 아웃 및 스케일 인을 자동으로
배포 및 관리하기 위해서는 기존의 세션 방식으로는 불가능 해 졌습니다.
안드로이드 앱이나, 아이폰에 어플리케이션은 세션 방식으로 통신을 할 수 없습니다.
큰 대형 업체 구글이나,네이버, 다음 등 로그인이 세션이 아닌 Auth2.0 방식으로 로그인 인증을 하고 있습니다. Auth2.0이 토큰 방식이라고 보면 맞습니다.
토큰은 액세스키와 리프레쉬 키를 쌍으로 발행 합니다.
한번 발행한 키는 유효시간안에는 취소 하고 그런 기능이 없습니다.
그 시간안에는 유효하기 때문에 보통 액세스 키는 시간을 짥게 해서 발행을 하고 유효 시간이 끝나면 리프레쉬 토큰을 이용하여 액세스 키를 다시 발급합니다.
그래서 사용자는 아무 불편없이 사용을 할 수 있습니다.
보안이 중요한 경우에는 액세스키를 1시간 또는 그 미만으로 짧게 발행하고, 액세스키를 무효화 할 수 없기 때문에 혹시 모를 탈취에 대비 하기 위해서 발행한 액세스키를 메모리 db에 관리를 합니다.
리프레쉬키도 같이 관리를 합니다.
그래서 사용자가 로그아웃을 한다던지 키를 탈취 당했으면 db에 저장된 액세스 키와 리플에쉬 키의 상태값으로 바꾸어서 사용 못하게 막는 방법으로 보안을 처리 합니다.
토큰은 자바로 발핼 할수도 있고 go언어, 파이쎤, node.js 등 모든 프로그램언어로 발행 할수 있습니다.
토큰 발행하는 비밀키만 동일하면 프로그램 언어와 상관없이 동일하게 복호화 할 수 있습니다.
보통 로그인 인증용으로 토큰을 많이 사용하는데 간단하게 자바로 토큰 생성및 복호화 하는 방법을 알아 보겠습니다.
생성및 복호화를 라이브러리로 만들어서 한개의 jar 파일로 관리 할 수 있게 만들어 봅니다.
이클립스 자바 프로젝트 생성
이클립스를 기동 시킵니다.
자바 버전은 JDK1.8 기준입니다.
New Java Project를 해서 Projec name을 적당하게 입력하여 프로젝트를 생성합니다.
패키지 파일을 하나 만듭니다.
파일 이름은 JwtMain.java
package sample.jwt;
public class JwtMain {
public static void main(String[] args) {
System.out.println("jwtMain");
//1. 토큰생성
String id = "test"; //아이디
String key = "key"; //key
String menuNo = "menu001";
//1. JWT 토큰 생성 - id
String token = JwtUtil.createToken(id,key,menuNo);
System.out.println("token : " + token);
//2.JWT 토큰 복호화
User user = JwtUtil.getUser(token);
// user.print();
System.out.println("id : " + user.getId());
System.out.println("key : " + user.getKey());
System.out.println("menuNo : " + user.getMenuNo());
}
}
메인은 토큰 생성 후에 복호화가 잘되는지 확인용도 입니다.
User 라는 로그인 사용자 클래스를 만들었습니다. 그냥 하는것 보다는 클래스 파일을 만들어서 하게 되면 보안에 아무래도 조금이라도 더 유리합니다.
User 객체를 만들어서 통신에 사용하기 위해서는 그냥 간단하게 직렬화 및 역직렬화를 합니다. 직렬화는 그냥 간단하게 객체를 텍스트로 변환 한다고 생각하시면 됩니다. 통신을 위해서는 보통 객체를 변환을 해야 합니다. 용어가 마샬링 언마샬링 하면서 어려운 용어가 많던데 나는 그냥 단순해서 그런 어려운 용어는 모르겠고 간단하게 직렬화 한줄로 쭉 세워서 순서대로 통신을 하기 위해서 객체를 변환해서 줄 세운다고 생각하시면 되고 역질력화는 반대로 텍스트를 다시 오브젝트 객체로 변환하는 거라고 생각 하시면 됩니다.
JSON Web Token 라이브러리 가져오기
자바에서 JWT를 사용하기 위해서는 maven 저장소에서 라이브러리를 들고 와야 합니다.
https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt/0.9.1
이 주소에 가서 라이브러리를 카피해서 프로젝트에 lib 폴더를 만들고 다운로드 합니다.
json web token은 의존성이 있어서 관련 라이브러리도 같이 다운 받아야 합니다. 필요한 라이브러리를 다 받아서
나중에 한개의 jar 파일로 만들어서 필요한 곳에 배포하여 사용 하면 사용자 라이브러리가 되는것 입니다.
1. jjwt-0.9.1.jar
2. jackson-annotations-2.5.0.jar
3. jackson-core-2.9.5.jar
4. jackson-databind-2.5.1.jar
jjwt-0.9.1.jar 파일에서 사용하는 의존성 라이브러리가 3개가 필요합니다. jackson-xx 붙어 있는 라이브러리를 메이븐 저장소에서 검색하여 다운 받습니다.
메이븐 프록제트가 아니고 그냥 자바 프로젝트라서 메이븐이나 그래들을 사용하지 않습니다.
저 4개의 라이브러리를 같이 포함하는 우리만의 필요한 라이브러리를 만듭니다.
jwt token 프로젝트 폴더 구조입니다.
사용자 정보를 담을스 있는 User 객체를 만듭니다.
User.java
package invako.jwt;
import java.io.Serializable;
//User권한 클래스 (직렬화 인터페이스 상속)
public class User implements Serializable {
private static final long serialVersionUID = 1L;
// 맴버 변수
private String id;
private String key;
private String menuNo;
// setter
public void setId(String id) {
this.id = id;
}
public void setKey(String key) {
this.key = key;
}
public void setMenuNo(String menuNo) {
this.menuNo = menuNo;
}
//getter
public String getId() {
return this.id;
}
public String getKey() {
return this.key;
}
public String getMenuNo() {
return this.menuNo;
}
// 맴버 변수 출력 함수
public void print() {
// 콘솔 출력
System.out.println("id = " + this.id + " key = " + this.key + " menuNo = " + this.menuNo);
}
}
Json Token 을 만들어 주는 JwtUtil.java 파일을 만든다.
package invako.jwt;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.Base64;
import java.util.Date;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.SignatureException;
public class JwtUtil {
// 암호화하기 위한 키
private static String SECRET_KEY = "SECRET_KEY";
// JWT 만료 시간 1분
private static long tokenValidMilisecond = 1000L * 60 ;
public static final String AUTHORIZATION_HEADER = "Authorization";
// 토큰 생성 함수
public static String createToken(String id, String key, String menuNo) {
// User 인스턴스 생성
User user = new User();
user.setId(id);
user.setKey(key);
user.setMenuNo(menuNo);
// JwtUtil 인스턴스 생성
JwtUtil jwt = new JwtUtil();
// User 인스턴스를 직렬화한다.
String code = jwt.convertSerializable(user);
// 직렬화된 코드에 보안을 위해 임의적인 문자나 숫자를 추가한다. 9XP 코드를 추가한다.
code = "9XP" + code;
// JWT 토큰 생성
String token = jwt.createToken(code);
return token;
}
// 토큰 생성 함수
public String createToken(String code) {
// Claims을 생성
Claims claims = Jwts.claims().setId(code);
// 현재 시간
Date now = new Date();
// JWT 토큰을 만드는데, Payload 정보와 생성시간, 만료시간, 알고리즘 종료와 암호화 키를 넣어 암호화 함.
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + tokenValidMilisecond))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
// 직렬화 함수
public String convertSerializable(User user) {
try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) {
try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
oos.writeObject(user);
// 직렬화 코드
byte[] data = baos.toByteArray();
// 직렬화된 것은 Base64로 암호화
return Base64.getEncoder().encodeToString(data);
}
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
// 역직렬화 함수
public static User convertData(String code) {
// Base64 복호화
byte[] data = Base64.getDecoder().decode(code);
// 역직렬화
try (ByteArrayInputStream bais = new ByteArrayInputStream(data))
{
try (ObjectInputStream ois = new ObjectInputStream(bais)) {
Object objectMember = ois.readObject();
// User 인스턴스로 캐스팅
return (User) objectMember;
}
} catch (Throwable e) {
e.printStackTrace();
return null;
}
}
// 사용자 정보를 리턴한다.
public static User getUser(String token) {
// JWT 토큰 복호화
User user = new User();
Jws<Claims> claims = JwtUtil.getClaims(token);
// JWT 토큰 검증
if (claims != null && JwtUtil.validateToken(claims)) {
// id를 취득한다.
String id = JwtUtil.getKey(claims);
// 콘솔 출력
//System.out.println("id : " + id);
// 9XX 3자리코드를 제거
id = id.substring(5);
// 역직렬화
user = JwtUtil.convertData(id);
} else {
// 토큰이 정합성이 맞지 않으면
System.out.println("error");
}
return user;
}
// String으로 된 코드를 복호화한다.
public static Jws<Claims> getClaims(String token) {
try {
// 암호화 키로 복호화한다.
// 즉 암호화 키가 다르면 에러가 발생한다.
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token);
}catch(SignatureException e) {
// System.out.println("SignatureException: " + e);
return null;
} catch(Exception e) {
// System.out.println("Exception: " + e);
return null;
}
}
// 토큰 검증 함수
public static boolean validateToken(Jws<Claims> claims) {
// 토큰 만료 시간이 현재 시간을 지났는지 검증
return !claims.getBody()
.getExpiration()
.before(new Date());
}
// 토큰을 통해 Payload의 ID를 취득
public static String getKey(Jws<Claims> claims) {
// Id 취득
return claims.getBody()
.getId();
}
// 토큰을 통해 Payload의 데이터를 취득
public static Object getClaims(Jws<Claims> claims, String key) {
// 데이터 취득
return claims.getBody()
.get(key);
}
}
코딩을 다 하였으니 토큰을 발행 하여 보자
메인이 있는 자바 파일에서 마우스 오른쪽 버튼을 클릭하여 실행을 시킨다.
아래의 그림과 같이 터미널에 오류 없이 토큰이 발행되고 복호화가 잘 되는지 확인한다.
정상적으로 잘 토큰이 생성이 되었다.
자바 라이브러리 만들기
이 파일을 가지고 자바 라이브러리를 만들어 보자
그냥 만들면 의존성 문제 때문에 관련 라이브러를 배포할때 같이 배포 해야 한다.
의존성 관련 라이브러도 하나의 파일에 같이 묶이게 라이브러리를 만들어 보자
프로젝트에서 마우스 오른쪽 버튼을 클릭하여 Export를 선택한다.
Java에서 JAR file을 선택하여 만들면 의존성 추가가 안되어서 배포할때 관련 라이브러리도 같이 배포해야 한다.
하나의 jar에 포함시켜서 한개로 만들려면 Runnable JAR file을 선택하고 Next버튼을 클릭한다.
Next 버튼 클릭
Launch configuration 에 메인 클래스를 선택하여 준다.
Export destination 부분에서 어디에 어떤이름으로 저장될지 이름을 입력한다.
Finish 버튼을 클릭하여 jar 파일을 만든다.
톰캣서버에서 토큰 테스트
만들어진 jar 파일을 이용하여 톰캣 웹서버에서 토큰이 잘 발행 되고 복호화가 잘 되는지 테스트 해 보자
톰캣 홈페이지에 가서 톰캣서버를 다운로드 받는다.
https://tomcat.apache.org/download-90.cgi
다운받은 파일을 적당한 디렉토리에 압축을 푼다.
압축을 푼후 톰캣서버에서 웹기본 배포 폴더인 webapps/ROOT 폴더로 WEB-INF 폴더에 web.xml파일만 놔두고 다 지운다. WEB-INF 폴더 아래에 lib 폴더를 만들어서 우리가 만든 jar 파일을 복사하여 넣는다.
이런 구조로 폴더를 만든다.
lib 폴더에 생성된 JwtToken.jar 파일을 복사하여 넣는다.
ROOT 바로 아래에 자바스크립트 라이브러리 파일을 넣을수 있게 js 폴더도 하나 만든다.
js 폴더에는 유명한 jquery 라이브러리를 다운받아서 넣는다. cdn으로 사용해도 되고 편한대로 하면 된다.
안써도 상관은 없지만 그냥 습관적으로 제이쿼리를 사용한다.
jquery-1.11.0.min.js 파일을 이용하였다.
ROOT 폴더에 테스트를 하기 위해서 index.jsp 파일과 token.jsp 파일 2개를 생성한다.
1. index.jsp 파일생성
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<script type="text/javascript" src="/js/jquery-1.11.0.min.js"></script>
</head>
<body>
<a href="#" class="login_btn" id="btnExterLogin">외부로그인</a>
<!-- 외부 토큰로그인 -->
<form name="loginExternalSendForm" id="loginExternalSendForm" method="post">
<input type="hidden" name="token" value="" />
</form>
</body>
<script type="text/javascript">
$(function(){
// 토킅생성
$("#btnExterLogin").on("click",function(event){
event.preventDefault();
param = {
"id" : "test",
"key" : "key001" ,
"menuNo" : "menu-001"
};
$.ajax({
type:"POST",
// url:"http://localhost:8070/token.jsp",
url:"token.jsp",
data: param,
dataType : "json",
success:function(data){
//생성된 토큰값 확인
console.log('data',data.token);
//외부팝업 호출 토큰값을 넘겨서 토큰을 이용하여 필요한 사이트 로그인 처리
//토큰 유효시간은 30초나 1분정도로 짧게 해서 토큰키가 탈취 당해도 시간이 짧아서
//무효화 되게 짧게 설정한다.
//서버에서도 토큰값을 보고 사전에 정의된 시크릿키를 이용하여 토큰을 검증후 로그인 처리를 하여준다.
//외부에서 받는 부분을 구현하여 넘긴다.
document.loginExternalSendForm.token.value = data.token; //토큰값
document.loginExternalSendForm.action = "http://127.0.0.1:8070/token.jsp";
document.loginExternalSendForm.target = "popup";
document.loginExternalSendForm.submit();
},
error:function(e){
alert("외부사이트 호출 실패1" + e);
console.log(e);
}
});
});
});
</script>
</html>
2. token.jsp 파일 생성
<%@ page language="java" contentType="text/html; charset=UTF-8" %>
<%@ page import="invako.jwt.JwtUtil" %>
<%
request.setCharacterEncoding("utf-8");
String id = request.getParameter("id");
String key = request.getParameter("key");
String menuNo = request.getParameter("menuNo");
/* 파라미터 값을 이용하여 토큰을 생성후 리턴한다. */
String token = JwtUtil.createToken(id,key,menuNo);
%>
{
"token" : "<%=token%>"
}
클라이언트 쪽을 다 만들었으니 톰캣을 기동하여 토큰값 발행이 잘 되는지 확인한다.
클라이언트쪽이 잘 된다.
받는쪽을 구현하여 잘 복호화가 되는지 테스트 한다.
참고로 자바버전은 jdk1.8 기준이다. 자바 버전이 다르면 해당 버전에 맞게 토큰 라이브러리를 생성해 줘야 한다.
받는쪽은 간단하게 jsp 파일 하나 만들어서 토큰값만 수신되는지 확인한다.
톰캣서버를 하나 더 띄워서 테스트를 해야 하지만 귀찮으니까 그냥 같은서버에서 수신이 잘되는지 테스트 하겠다.
ROOT 폴더 아래에 token.jsp 라고 파일을 하나 만들고 수신 토큰값이 잘 찍히는지 확인한다.
token.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%>
<%
String token = request.getParameter("token");
System.out.println("token========>" + token);
session.setAttribute("token", token);
//로그인이 필요한곳으르 리다이렉트
//response.sendRedirect("/actionLogin.do");
%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>token</title>
<script type="text/javascript">
<!-- get 방식으로 넘어왔을때 url에 표시되는 파라민터 정보를 replace 처리하여 파라미터값 안보이게 처리 */
history.replaceState ({},null,location.pathname);
</script>
</head>
<body>
넘어온 토큰값 : <%= token%>
</body>
</html>
외부로그인 버튼 클릭하여 실행하면 token.jsp 파일에서 아래와 같이 토큰값이 브라우저에 찍히면 토큰 발행 끝
'프로그램 > 자바' 카테고리의 다른 글
레이어드 아키텍처 (0) | 2023.09.10 |
---|---|
첨부파일 다운로드 & 업로드 (0) | 2023.06.07 |
Java 에서 ValidatorException 등 인증서 관련 에러 해결 (1) | 2023.05.10 |
자바 cors 우회하기 get,post방식 (0) | 2022.07.12 |
자바 랜덤 API 암호화 BASE64 KEY 생성 (0) | 2022.01.13 |
댓글