Spring Security - MySql Password()을PasswordEncoder로 구현하는 방법

스프링 시큐리티(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() 함수 구현하기

 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 구현체 만들기

Spring Security - 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();
  }

 글에서 사용한 코드들은 첨부파일로도 제공하니 필요하신 분은 다운로드하셔서 사용하시면 됩니다.

PasswordEncoder.zip
0.00MB

 

반응형

댓글

Designed by JB FACTORY