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 찍먹 뜯어보기 끝 -
'프로젝트' 카테고리의 다른 글
Spring Security로 회원가입을 해보자 (1) - 회원 엔티티 설계 (0) | 2023.03.01 |
---|---|
JPA 1 : N 관계를 분석해보자 (0) | 2023.02.27 |
Session 기반 인증과 Token 기반 인증의 차이가 뭘까? (2) | 2023.02.20 |
Custom Exception 해보기 (0) | 2023.02.19 |
[JPA/Thymeleaf] 게시글 작성자만 수정 권한 가지기 (0) | 2023.02.18 |