스프링 시큐리티(Spring Security)를 사용할 때 적용되는 패스워드 암호화 부분은 PasswordEncoder를 이용하면 자유롭게 커스터마이징이 가능합니다. 예를 들어 MySQL의 암호화 기능인 Password()도 PasswordEncoder를 구현해서 사용하면 스프링 시큐리티에 적용할 수 있습니다.
MySQL의 암호화 기능 Password()
스프링 시큐리티의 PasswordEncoder를 통해 구현하기 위해 먼저 MySQL의 Password()가 정의된 내용을 확인해보겠습니다.
/*
Generate binary hash from raw text string
Used for Pre-4.1 password handling
SYNOPSIS
hash_password()
result OUT store hash in this location
password IN plain text password to build hash
password_len IN password length (password may be not null-terminated)
*/
void hash_password(ulong *result, const char *password, uint password_len) {
ulong nr = 1345345333L, add = 7, nr2 = 0x12345671L;
ulong tmp;
const char *password_end = password + password_len;
for (; password < password_end; password++) {
if (*password == ' ' || *password == '\t')
continue; /* skip space in password */
tmp = (ulong)(uchar)*password;
nr ^= (((nr & 63) + add) * tmp) + (nr << 8);
nr2 += (nr2 << 8) ^ nr;
add += tmp;
}
result[0] = nr & (((ulong)1L << 31) - 1L); /* Don't use sign bit (str2int) */
;
result[1] = nr2 & (((ulong)1L << 31) - 1L);
}
Password()가 반환하는 결과를 간략하게 정리하면 내부적으로 hash_password() 함수를 사용해서 결과 값을 만들어냅니다. 그리고 hash_password() 함수는 SHA1를 이용해서 Hashing을 적용한 값을 반환하게 되어있습니다. 해당 코드 내용은 MySQL의 Github에 공개되어 있는 내용으로 더 자세한 내용이 궁금하시다면 링크를 확인해주세요.
자바로 MySQL의 Password() 함수 구현하기
MySQL의 PASSWORD() 함수가 어떻게 정의되어있는지 확인했으니 자바를 이용해서 Password() 기능을 동일하게 구현해보겠습니다.
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
public class Password {
public String encode(String text) {
if (text == null) {
throw new IllegalArgumentException("text is null");
}
return toString(hashing(getBytes(text)));
}
private byte[] getBytes(String text) {
byte[] result = new byte[text.length()];
for (int i = 0; i < text.length(); i++) {
result[i] = (byte) (text.charAt(i) & 0xff);
}
return result;
}
public byte[] hashing(byte[] bytes) {
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
return messageDigest.digest(messageDigest.digest(bytes));
} catch (GeneralSecurityException e) {
throw new RuntimeException(e);
}
}
private String toString(byte[] bytes) {
StringBuffer stringBuffer = new StringBuffer(41);
stringBuffer.append("*");
for (int i = 0; i < bytes.length; i++) {
String hex = Integer.toHexString(bytes[i] & 0xff).toUpperCase();
if (hex.length() < 2) {
stringBuffer.append("0");
}
stringBuffer.append(hex);
}
return stringBuffer.toString();
}
public static void main(String[] args) {
String expectedValue = "*A4B6157319038724E3560894F7F932C8886EBFCF";
String result = new Password().encode("1234");
System.out.println("expectedValue: " + expectedValue);
System.out.println("result : " + result);
System.out.println(expectedValue.equals(result));
}
}
자바를 이용해 만든 Password 클래스의 기능은 크게 Hashing 처리와 문자 변환이 있습니다. Hashing 처리 같은 경우에는 자바에서 제공하는 MessageDigest를 통해 손쉽게 사용할 수 있습니다. MySQL의 PASSWORD() 결과를 사용하면 Password 클래스가 원하는 결과를 반환하는지 테스트할 수 있습니다.
MySQL Password() PasswordEncoder 구현체 만들기
PasswordEncoder 인터페이스의 구현체를 만들기 위해서는 encode(), matches()를 정의해야 합니다. 앞서 만든 Password 클래스를 이용해 스프링 시큐리티에서 제공하는 PasswordEncoder 인터페이스를 구현해보겠습니다.
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import org.springframework.security.crypto.password.PasswordEncoder;
public class MySqlPasswordEncoder implements PasswordEncoder {
private static MessageDigest DIGEST = null;
{
try {
DIGEST = MessageDigest.getInstance("SHA1");
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
}
}
@Override
public String encode(CharSequence rawPassword) {
if (rawPassword == null) {
throw new IllegalArgumentException("rawPassword cannot be null");
}
return toString(hashing(getBytes(rawPassword)));
}
private byte[] getBytes(CharSequence rowPassword) {
byte[] result = new byte[rowPassword.length()];
for (int i = 0; i < rowPassword.length(); i++) {
result[i] = (byte) (rowPassword.charAt(i) & 0xff);
}
return result;
}
private byte[] hashing(byte[] bytes) {
return DIGEST.digest(DIGEST.digest(bytes));
}
private String toString(byte[] bytes) {
StringBuffer buffer = new StringBuffer(41);
buffer.append("*");
for (int i = 0; i < bytes.length; i++) {
padding(buffer, Integer.toHexString(bytes[i] & 0xff).toUpperCase());
}
return buffer.toString();
}
private void padding(StringBuffer buffer, String hex) {
if (hex.length() < 2) {
buffer.append("0");
}
buffer.append(hex);
}
@Override
public boolean matches(CharSequence rawPassword, String encodedPassword) {
if (rawPassword == null || encodedPassword.isEmpty()) {
return false;
}
if (!encodedPassword.equals(encode(rawPassword))) {
return false;
}
return true;
}
}
PasswordEncoder 구현체를 만들었다면 스프링 시큐리티에서 사용하는 빈을 변경해야 합니다. 추가한 PasswordEncoder를 사용하지 않으면 스프링 시큐리티의 암호화 내용은 변경되지 않습니다.
@Bean
public PasswordEncoder passwordEncoder() {
return new MySqlPasswordEncoder();
}
글에서 사용한 코드들은 첨부파일로도 제공하니 필요하신 분은 다운로드하셔서 사용하시면 됩니다.
'프로그래밍 > 스프링' 카테고리의 다른 글
Spring boot - Major Version 업데이트 JPA QueryDsl 문제 해결 (2.7.3 > 3.1.3) (0) | 2023.09.11 |
---|---|
Spring Boot - 스프링 부트 Whitelabel 에러 처리 방법 (0) | 2021.10.26 |
Spring - 실무에서 사용하는 React + SpringBoot 프로젝트 만들기 with Gradle (4) | 2021.09.01 |
Spring boot - vuejs를 사용하는 스프링 부트 프로젝트 만들기 (0) | 2021.08.25 |
Spring error - Failed to configure a DataSource 에러 원인과 해결 방법 (0) | 2021.08.18 |