프로젝트

스프링 시큐리티를 분석해보자

ummchicken 2023. 2. 25. 19:58

Spring Security를 분석해보자.

 

부족한 부분은 계속해서 채워나갈 예정이다.

 

 


 

✔️ 들어가기 전

  • PasswordEncoder
    • Spring Security에서 비밀번호를 안전하게 저장할 수 있도록 제공하는 인터페이스이다.
    • 단방향 해쉬 알고리즘에 Salt를 추가하여 인코딩하는 방식을 제공한다.

 

  • FormLogin
    • MVC 방식에서 화면을 보여주고, 아이디와 비밀번호를 입력하는 전통적인 로그인을 말한다.

 

  • CSRF (Cross-SIte Request Forgery)
    • 사이트 간 요청 위조를 뜻한다.
    • 스프링 시큐리티에서는 @EnableWebSecurity 어노테이션을 이용해 CSRF를 방지하는 기능을 제공한다.
    • 먼저 서버에서 임의의 토큰을 발급한다.
    • 자원에 대한 변경 요청이 되돌아 올 경우, 토큰 값을 확인하여 클라이언트가 정상적인 요청을 보낸 것인지 확인한다.
    • 만일 CSRF 토큰이 유효하지 않다면(값이 다르거나 수명이 끝났으면), 4xx 상태 코드를 리턴한다.

 

 

 

 

✔️ PasswordEncoder 

먼저, PasswordEncoder를 살펴볼 겁니다.

 

 

PasswordEncode를 등록하는 코드입니다.

/**
 * passwordEncoderForEncode로는 BCryptPasswordEncoder를 사용하는 DelegatingPasswordEncoder를 사용한다.
 *
 * DB에 데이터를 저장할 때, Bcrypt로 저장을 한다면 이는 암호화되어서 저장되고, 이를 복호화 할 수 있는 방법은 없다.
 * 즉, 비밀번호의 비교는 가능하나 복호화는 불가능 하다.
 * */
@Bean
public PasswordEncoder passwordEncoder(){ //1 - PasswordEncoder 등록
    return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}

 

 

PasswordEncoder 인터페이스를 봅시다.

public interface PasswordEncoder {

	String encode(CharSequence rawPassword);

	boolean matches(CharSequence rawPassword, String encodedPassword);

	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}

}
  • 인터페이스는 encode()와 matches로 구성되어 있습니다.
  • encode : 실제로 패스워드를 암호화 할 때 사용한다.
  • matches : 사용자에게 입력받은 패스워드를 비교하고자 할 때 사용한다.

 

 

encode의 사용 예는 다음과 같습니다.

// 패스워드 암호화
public void encodePassword(PasswordEncoder passwordEncoder) {
    this.password = passwordEncoder.encode(password);
}
  • 회원가입 시 입력받은 password를 encode()로 암호화 합니다.

 

 

다음은 matches의 사용 예를 봅시다.

public boolean matchPassword(PasswordEncoder passwordEncoder, String checkPassword) {
    return passwordEncoder.matches(checkPassword, getPassword());
}
  • return은 boolean 입니다.
  • 입력받은 rawPassword와 실제 password(encode된)를 비교합니다.

 

 

 

다음은 createDelegatingPasswordEncoder를 자세히 살펴볼 겁니다.

 

 

✔️ createDelegatingPasswordEncoder

PasswordEncoder를 여러개 선언한 뒤, 상황에 맞게 골라쓸 수 있도록 지원하는 Encoder이다.

 

<캡쳐 화면>

 

<실제 코드>

public final class PasswordEncoderFactories {

	private PasswordEncoderFactories() {
	}
    
	@SuppressWarnings("deprecation")
	public static PasswordEncoder createDelegatingPasswordEncoder() {
		String encodingId = "bcrypt";
		Map<String, PasswordEncoder> encoders = new HashMap<>();
		encoders.put(encodingId, new BCryptPasswordEncoder());
		encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
		encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
		encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
		encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
		encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
		encoders.put("scrypt", new SCryptPasswordEncoder());
		encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
		encoders.put("SHA-256",
				new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
		encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
		encoders.put("argon2", new Argon2PasswordEncoder());
		return new DelegatingPasswordEncoder(encodingId, encoders);
	}

}
  • encodingId는 "bcrypt"입니다.
  • 이것은 BCryptPasswordEncoder()와 매핑됩니다.
  • 최종으로 encoding이 되는 값은 bcrypt로 해싱된 패스워드가 return이 됩니다.
  • 그 다음 DelegatingPasswordEncoder의 생성자의 인자로 넘겨줍니다.
  • 한번 encode된 패스워드는 다시 복호화를 할 수 없다고 합니다.

 

 

 

Spring에서 기본적으로 제공되는 Password의 종류는 다음과 같습니다. (출처)

  • BCryptPasswordEncoder
  • DelegatingPasswordEncoder
  • SCryptPasswordEncoder
  • Pbkdf2PasswordEncoder

 

 

그럼 BCryptPasswordEncoder를 살펴봅시다.

 

 

✔️ BCryptPasswordEncoder

BCrypt라는 해시 함수를 사용한 구현체이다.

단순히 해시를 하는것 뿐만 아니라 Salt 를 넣는 작업까지 하므로,
입력값이 같음에도 불구하고 매번 다른 encoded된 값을 return 해주게 된다.

 

<캡쳐 화면>

 

<실제 코드>

@Override
public String encode(CharSequence rawPassword) {
    if (rawPassword == null) {
        throw new IllegalArgumentException("rawPassword cannot be null");
    }
    String salt = getSalt();
    return BCrypt.hashpw(rawPassword.toString(), salt);
}

private String getSalt() {
    if (this.random != null) {
        return BCrypt.gensalt(this.version.getVersion(), this.strength, this.random);
    }
    return BCrypt.gensalt(this.version.getVersion(), this.strength);
}
  • BCrypt라는 해시 함수를 사용한 구현체라고 합니다.
  • 단순히 해시를 하는 것 뿐만 아니라, Salt를 넣는 작업까지 하므로 
  • 입력값이 같음에도 불구하고 매번 다른 encoded된 값을 return 합니다.

 

 

그러므로 BCrypt를 사용할 때는 matches 함수를 잘 확인하고 사용하는 게 좋다고 합니다.

→ 입력값이 같아도, 매번 출력물이 다르기 때문에 equal로 비교하려고 하면, 일치하지 않는 상황이 있을 수 있습니다.

 

 

 

여기서 Bcrypt의 hashpw를 반환합니다.

hashpw가 뭔지 궁금해집니다.

private static String hashpw(byte passwordb[], String salt, boolean for_check) {
    
    ...
    
    int saltLength = salt.length();

    if (saltLength < 28) {
        throw new IllegalArgumentException("Invalid salt");
    }
    
    ...

    if (off == 4 && saltLength < 29) {
        throw new IllegalArgumentException("Invalid salt");
    }
    
    ...
    
}

신기하네요. 여러가지 조건들이 있습니다.

한 예로, 28 길이가 되지 않으면 IllegalArgumentException를 발생시키네요.

 

 

 

 

다음은 DelegatingPasswordEncoder을 살펴봅시다.

 

 

✔️ DelegatingPasswordEncoder

<캡쳐 화면>

 

<실제 코드>

public DelegatingPasswordEncoder(String idForEncode, Map<String, PasswordEncoder> idToPasswordEncoder,
			String idPrefix, String idSuffix) {
    if (idForEncode == null) {
        throw new IllegalArgumentException("idForEncode cannot be null");
    }
    if (idPrefix == null) {
        throw new IllegalArgumentException("prefix cannot be null");
    }
    if (idSuffix == null || idSuffix.isEmpty()) {
        throw new IllegalArgumentException("suffix cannot be empty");
    }
    if (idPrefix.contains(idSuffix)) {
        throw new IllegalArgumentException("idPrefix " + idPrefix + " cannot contain idSuffix " + idSuffix);
    }

    if (!idToPasswordEncoder.containsKey(idForEncode)) {
        throw new IllegalArgumentException(
                "idForEncode " + idForEncode + "is not found in idToPasswordEncoder " + idToPasswordEncoder);
    }
    for (String id : idToPasswordEncoder.keySet()) {
        if (id == null) {
            continue;
        }
        if (!idPrefix.isEmpty() && id.contains(idPrefix)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + idPrefix);
        }
        if (id.contains(idSuffix)) {
            throw new IllegalArgumentException("id " + id + " cannot contain " + idSuffix);
        }
    }
    this.idForEncode = idForEncode;
    this.passwordEncoderForEncode = idToPasswordEncoder.get(idForEncode);
    this.idToPasswordEncoder = new HashMap<>(idToPasswordEncoder);
    this.idPrefix = idPrefix;
    this.idSuffix = idSuffix;
}
  • passwordEncoderForEncode로는 BCrypt를 사용하는 DelegatingPasswordEncoder를 사용합니다.

 

 

 

DelegatingPasswordEncoder의 encode는 다음과 같습니다.

@Override
public String encode(CharSequence rawPassword) {
    return this.idPrefix + this.idForEncode + this.idSuffix + this.passwordEncoderForEncode.encode(rawPassword);
}

 

 

 

encode 메서드의 반환 결과는 

{bcrypt}BCryptPasswordEncode로 암호화된 문자열

 

예를 들면 

{bcrypt}$2a$10$UemKUf.cijGeJz6CJ/81auJKQVU0syWTJq2O.UGQXga9G.SCCKDR.
  • 위와 같이 맨 앞에 prefix로 {bcrypt}가 붙게 됩니다.
  • 따라서 어떤 암호화를 사용해서 encode를 했는지 구분하는 목적인 것 같다고 합니다.
  • 스프링 시큐리티는 '{암호화 방식}암호화된 비밀번호' 형식의 password가 반환됩니다.

 

 

 


 

 

일단 뜯어보긴 했습니다만, 

순서도 뒤죽박죽이고... 제대로 해석했는지도 모르겠네요.

 

나중에 계속 알게되면 더 추가하거나 수정하겠습니다.

 

 

- Spring Security 찍먹 뜯어보기 끝 -