Skip to content

Commit 2272e57

Browse files
committed
feat: TypeHandler를 통해 OAuthToken 암호화
1 parent 75c04c3 commit 2272e57

5 files changed

Lines changed: 158 additions & 3 deletions

File tree

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.ureca.unity.global.config;
2+
3+
import org.apache.ibatis.type.BaseTypeHandler;
4+
import org.apache.ibatis.type.JdbcType;
5+
6+
import java.sql.*;
7+
8+
public class EncryptedStringTypeHandler extends BaseTypeHandler<String> {
9+
10+
private static OAuthTokenCrypto crypto;
11+
12+
public static void setCrypto(OAuthTokenCrypto c) {
13+
crypto = c;
14+
}
15+
16+
public EncryptedStringTypeHandler() {}
17+
18+
private OAuthTokenCrypto crypto() {
19+
if (crypto == null) {
20+
throw new IllegalStateException("EncryptedStringTypeHandler crypto not initialized");
21+
}
22+
return crypto;
23+
}
24+
25+
@Override
26+
public void setNonNullParameter(PreparedStatement ps, int i, String parameter, JdbcType jdbcType)
27+
throws SQLException {
28+
ps.setString(i, crypto().encrypt(parameter));
29+
}
30+
31+
@Override
32+
public String getNullableResult(ResultSet rs, String columnName) throws SQLException {
33+
String v = rs.getString(columnName);
34+
return v == null ? null : crypto().decrypt(v);
35+
}
36+
37+
@Override
38+
public String getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
39+
String v = rs.getString(columnIndex);
40+
return v == null ? null : crypto().decrypt(v);
41+
}
42+
43+
@Override
44+
public String getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
45+
String v = cs.getString(columnIndex);
46+
return v == null ? null : crypto().decrypt(v);
47+
}
48+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.ureca.unity.global.config;
2+
3+
import jakarta.annotation.PostConstruct;
4+
import lombok.RequiredArgsConstructor;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
@Configuration
8+
@RequiredArgsConstructor
9+
public class MyBatisTypeHandlerConfig {
10+
11+
private final OAuthTokenCrypto crypto;
12+
13+
@PostConstruct
14+
public void init() {
15+
EncryptedStringTypeHandler.setCrypto(crypto);
16+
}
17+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package com.ureca.unity.global.config;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.stereotype.Component;
5+
6+
import javax.crypto.Cipher;
7+
import javax.crypto.spec.GCMParameterSpec;
8+
import javax.crypto.spec.SecretKeySpec;
9+
import java.nio.charset.StandardCharsets;
10+
import java.security.SecureRandom;
11+
import java.util.Base64;
12+
13+
@Component
14+
public class OAuthTokenCrypto {
15+
16+
private static final String ALG = "AES";
17+
private static final String TRANSFORM = "AES/GCM/NoPadding";
18+
private static final int IV_LEN = 12; // GCM 권장 12 bytes
19+
private static final int TAG_BITS = 128;
20+
21+
private final byte[] keyBytes;
22+
private final SecureRandom random = new SecureRandom();
23+
24+
public OAuthTokenCrypto(@Value("${security.oauth-token.secret}") String secret) {
25+
// 최소 수정 버전: secret을 16/24/32 bytes로 맞춰 쓰는 걸 권장
26+
// 여기서는 "UTF-8 바이트" 기준으로 32바이트로 잘라/패딩 처리
27+
byte[] raw = secret.getBytes(StandardCharsets.UTF_8);
28+
this.keyBytes = new byte[32];
29+
for (int i = 0; i < this.keyBytes.length; i++) {
30+
this.keyBytes[i] = i < raw.length ? raw[i] : 0;
31+
}
32+
}
33+
34+
public String encrypt(String plain) {
35+
if (plain == null) return null;
36+
try {
37+
byte[] iv = new byte[IV_LEN];
38+
random.nextBytes(iv);
39+
40+
Cipher cipher = Cipher.getInstance(TRANSFORM);
41+
SecretKeySpec key = new SecretKeySpec(keyBytes, ALG);
42+
cipher.init(Cipher.ENCRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
43+
44+
byte[] ct = cipher.doFinal(plain.getBytes(StandardCharsets.UTF_8));
45+
46+
// 저장 포맷: base64( iv + ciphertext )
47+
byte[] out = new byte[iv.length + ct.length];
48+
System.arraycopy(iv, 0, out, 0, iv.length);
49+
System.arraycopy(ct, 0, out, iv.length, ct.length);
50+
51+
return Base64.getEncoder().encodeToString(out);
52+
} catch (Exception e) {
53+
throw new IllegalStateException("OAuth token encrypt failed", e);
54+
}
55+
}
56+
57+
public String decrypt(String enc) {
58+
if (enc == null) return null;
59+
try {
60+
byte[] in = Base64.getDecoder().decode(enc);
61+
if (in.length < IV_LEN + 1) return enc; // 방어: 평문이 들어온 경우 그냥 반환
62+
63+
byte[] iv = new byte[IV_LEN];
64+
byte[] ct = new byte[in.length - IV_LEN];
65+
66+
System.arraycopy(in, 0, iv, 0, IV_LEN);
67+
System.arraycopy(in, IV_LEN, ct, 0, ct.length);
68+
69+
Cipher cipher = Cipher.getInstance(TRANSFORM);
70+
SecretKeySpec key = new SecretKeySpec(keyBytes, ALG);
71+
cipher.init(Cipher.DECRYPT_MODE, key, new GCMParameterSpec(TAG_BITS, iv));
72+
73+
byte[] pt = cipher.doFinal(ct);
74+
return new String(pt, StandardCharsets.UTF_8);
75+
} catch (Exception e) {
76+
// 방어: 기존 데이터가 평문/다른 포맷이면 그냥 원문 반환(마이그레이션 단계에서 유용)
77+
return enc;
78+
}
79+
}
80+
}

‎src/main/resources/application.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,7 @@ cors:
4444

4545
cookie:
4646
secure: ${COOKIE_SECURE:true}
47+
48+
security:
49+
oauth-token:
50+
secret: ${OAUTH_TOKEN_SECRET}

‎src/main/resources/mapper/oauth/OAuthTokenMapper.xml‎

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,20 @@
99
<id property="id" column="oauth_token_id"/>
1010
<result property="userId" column="user_id"/>
1111
<result property="provider" column="provider"/>
12-
<result property="accessToken" column="access_token"/>
13-
<result property="refreshToken" column="refresh_token"/>
12+
<result property="accessToken" column="access_token" typeHandler="com.ureca.unity.global.config.EncryptedStringTypeHandler"/>
13+
<result property="refreshToken" column="refresh_token" typeHandler="com.ureca.unity.global.config.EncryptedStringTypeHandler"/>
1414
<result property="expiresAt" column="expires_at"/>
1515
</resultMap>
1616

1717
<insert id="insert" useGeneratedKeys="true" keyProperty="id" keyColumn="oauth_token_id">
1818
INSERT INTO oauth_tokens (user_id, provider, access_token, refresh_token, expires_at)
19-
VALUES (#{userId}, #{provider}, #{accessToken}, #{refreshToken}, #{expiresAt})
19+
VALUES (
20+
#{userId},
21+
#{provider},
22+
#{accessToken, typeHandler=com.ureca.unity.global.config.EncryptedStringTypeHandler},
23+
#{refreshToken, typeHandler=com.ureca.unity.global.config.EncryptedStringTypeHandler},
24+
#{expiresAt}
25+
)
2026
</insert>
2127

2228
<select id="findByUserIdAndProvider" resultMap="oauthTokenMap">

0 commit comments

Comments
 (0)