스프링에서 JWT를 생성하기위해 많이쓰는 jjwt라이브러리
이 라이브러리에서 JWT를 생성할때는 builder중 signWith()라는 메서드를 이용해 암호화를 한다
0.10이전버전에서는 암호화를 할때
signWith("secretKey", SignatureAlgorithm.RS256)
형식으로
secretKey를 String형식으로 쓸 수 있었다
하지만 0.10버전 이후부터 이 방법은 Deprecated 되었다
그렇게되서 새로운 방법을 찾아야했는데
새로운 방법으로는 KeyPair를 이용해 publicKey, privateKey를 생성하고
privateKey로 JWT를 만들고 publicKey로 JWT를 인증하는 방식을 권장하였다
그렇기때문에 나름대로 KeyPair를 만들어서 Key를 프로젝트에 pem형식으로 저장시킨다음 해당 파일에서 Key를 빼오는 방식으로 만들었다
키를 생성하기 위해서는 spring security의 KeyPairGenerator
가 필요하다
public static KeyPair generateKeyPair() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
return keyPair;
}
getInstance()
를 통해 암호 알고리즘을 선택해준다
또한 initialize()
를 통해 크기를 설정해준다
나는 ssl/tls에서도 사용하고 인터넷뱅킹에서도 사용하는 RSA 2048을 택하였다
이렇게하면 간단하게 KeyPair가 생성된다
이 KeyPair에서 getPrivateKey()
와 getPublicKey()
를 이용하면 간단하게 키를 얻을 수 있다
하지만 여기에는 큰 단점이 있는데
애플리케이션을 시작할때마다 KeyPair가 바뀌게된다
이렇게되면 애플리케이션이 다시 로드되면 이전 JWT는 무용지물이 되어버린다는 소리..
이렇기때문에 이 KeyPair를 pem형식으로 프로젝트에 저장시키고
다시 애플리케이션을 실행할때 pem형식의 파일이 존재하면 해당 파일에서 key를 읽어오고
pem파일이 존재하지 않으면 새로운 pem파일을 만드는 방식을 선택했다
먼저 pem파일을 만들기위해 라이브러리를 추가해야한다
implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
public static String keyToPem(Key key, String type) throws IOException {
StringWriter stringWriter = new StringWriter();
try (JcaPEMWriter pemWriter = new JcaPEMWriter(stringWriter)) {
pemWriter.writeObject(new PemObject(type, key.getEncoded()));
}
return stringWriter.toString();
}
먼저 pem파일에는 하나의 Key밖에 저장을 못하므로 Key,그리고 어떤 타입으로 저장할지에 대한 type을 변수로 받는다
pem파일은 BEGIN RSA PRIVATE KEY .... END RSA PRIVATE KEY
이런 형식이기때문에 type을 넣어주는거같다
JcaPEMWriter를 선언하고 생성자에 stringWriter를 넣어준다
그다음 PemObject 객체를 new로 만들고 그 안에 type과 key를 getEncoded()하여 넣어준다
이렇게하면 stringWriter에 pem형식으로 만들어진 데이터가 들어가게 된다
이것을 String 으로 반환시켜주자
그리고 이 메서드를 활용해서 pem으로 저장하는 메서드를 만들어준다
public static void saveKeyPair(String privateKeyPath, String publicKeyPath, PrivateKey privateKey, PublicKey publicKey) throws IOException {
String privateKeyPem = PemUtils.keyToPem(privateKey, "RSA PRIVATE KEY");
String publicKeyPem = PemUtils.keyToPem(publicKey, "RSA PUBLIC KEY");
Files.write(Paths.get(privateKeyPath), privateKeyPem.getBytes(StandardCharsets.UTF_8));
Files.write(Paths.get(publicKeyPath), publicKeyPem.getBytes(StandardCharsets.UTF_8));
}
privateKeyPath, publicKeyPath를 받아서 파일의 경로와 이름을 받아온다
privateKey, publicKey를 받아 각각 pem형식의 String을 생성한다
이것을 Files를 이용해 파일로 만들어 저장한다
이렇게하면 pem파일이 완성된다
이제 KeyPair를 만드는 메서드, pem파일을 만드는 메서드도 만들었다
그러면 pem파일에서 키를 꺼내오는 메서드도 만들어야한다
public static KeyPair loadKeyPair(String privateKeyPath, String publicKeyPath) throws Exception {
String privateKeyPem = new String(Files.readAllBytes(Paths.get(privateKeyPath)), StandardCharsets.UTF_8);
String publicKeyPem = new String(Files.readAllBytes(Paths.get(publicKeyPath)), StandardCharsets.UTF_8);
PrivateKey privateKey = PemUtils.pemToPrivateKey(privateKeyPem);
PublicKey publicKey = PemUtils.pemToPublicKey(publicKeyPem);
return new KeyPair(publicKey, privateKey);
}
public static PrivateKey pemToPrivateKey(String privateKeyPem) throws Exception {
PemReader pemReader = new PemReader(new StringReader(privateKeyPem));
PemObject pemObject = pemReader.readPemObject();
pemReader.close();
byte[] privateKeyBytes = pemObject.getContent();
PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(privateKeyBytes);
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
return keyFactory.generatePrivate(privateKeySpec);
}
먼저 파일에서 pem키에 해당하는 내용들을 꺼내온다
이렇게 꺼내온 String형식을 PemReader
를 이용해 읽는다
PemReader
는 pem형식으로 인코딩된 내용을 읽을 수 있는 객체이다
그리고 PemObject
를 이용해 읽은 내용을 객체형식으로 저장시킨다
그리고 pemReader.close()
안하면 메모리 누수가 일어날 수 있기때문에 닫아주자
pemObject.getContent()
는 객체의 내용을 byte형식의 배열로 가져올 수 있다
그다음 PKCS8EncodedKeySpec
로 변환을 한다
PKCS8EncodedKeySpec
은 인코딩된 키를 java에서 사용할 수 있는 키로 변환시켜주는 객체이다
그리고 RSA형식의 알고리즘을 사용하는 KeyFactory
객체를 생성한다
KeyFactory
는 PKCS8EncodedKeySpec
과 같은 객체를 Key형식으로 변환시켜주는 객체이다
여기서 마지막으로 keyFactory.generatePrivate(privateKeySpec)
으로 다시 PrivateKey로 만들어준다
publicKey는 마지막 리턴에서 private가 아닌 public인것 말고 동일하다
이제 모든 기능들을 만들었으니 적용시켜보자
애플리케이션을 시작하기 전 키를 생성이나 읽어들일 수 있게 설정했다
String privateKeyPath = "private_key.pem";
String publicKeyPath = "public_key.pem";
Path privateKeyFile = Paths.get(privateKeyPath);
Path publicKeyFile = Paths.get(publicKeyPath);
try {
KeyPair keyPair;
if (Files.exists(privateKeyFile) && Files.exists(publicKeyFile)) {
// 파일에서 키 쌍을 불러오기
keyPair = KeyFileUtils.loadKeyPair(privateKeyPath, publicKeyPath);
} else {
// 새 키 쌍을 생성하고 파일에 저장
keyPair = KeyGenerator.generateKeyPair();
KeyFileUtils.saveKeyPair(privateKeyPath, publicKeyPath, keyPair.getPrivate(), keyPair.getPublic());
}
KeyStore.initialize(keyPair);
} catch (Exception e) {
e.printStackTrace();
System.err.println("키 생성및 저장에 실패하였습니다.");
System.exit(1);
}
경로없이 바로 private_key.pem이기때문에 프로젝트 루트에 생성이된다
해당 루트에 파일이 둘다 존재할경우 loadKeyPair를 이용해 파일에서 KeyPair를 읽어온다
하지만 키가 없을경우 키를 생성하고 해당 키를 pem파일로 저장한다
그리고 해당 KeyPair를 KeyStore라는 객체에 안전하게 저장시키면 완료된다
만약 둘다 실패할경우 그냥 시스템을 종료시켜버리게 해버렸다