인증이란 resource에 접근하려는 사람이 누구인지 판별하는 과정을 의미합니다.
username과 password를 이용한 인증방식이 일반적입니다. 사용자가 username과 password를 입력하면 일련의 과정을 거쳐 사용자 정보가 저장된 DB에서 사용자를 조회함으로서 인증을 하게 됩니다. 사용자 정보 DB에 해당 정보가 있다면 인증에 성공하고 없다면 실패하게 됩니다.
인증에 성공하게 되면 인증정보를 토대로 권한부여(인가, authorization)까지도 수행할 수 있게 됩니다. 그러니 첫 단계인 인증이 잘 이루어져야 그 이후의 절차들도 견고하게 수행이 될 수 있을 것 같습니다.
그런데 이런 인증이 일어나려면 먼저 인증할 사용자 정보를 생성하고 사용자 정보 DB에 데이터를 저장해야 됩니다.
내가 자주쓰는 아이디와 비밀번호를 아무 웹 사이트 로그인 정보로 입력한다고 해도 회원가입이 안되어 있으면 로그인 즉, 인증에 성공할 수가 없죠.
그리고 비밀번호의 경우는 중요한 정보이기 때문에 DB에 저장할 때 raw한 상태로 저장하면 매우 위험합니다. 언제 탈취당할 지 모르기 때문에 잘 대비해야 합니다. 탈취로 인한 개인정보 유출등의 사고에서 피해를 최소화하려면 비밀번호를 암호화하여 저장해야 합니다.
이번 글에서는 어떻게 비밀번호를 암호화하여 저장 하는지 한 번 알아보겠습니다.
암호화 방법
암호화를 Encoding이라고 부르는데, Spring Security에서는 다양한 암호화 알고리즘을 사용할 수 있도록 지원하고 있습니다.
Spring Security 5.0 부터 제공하는 기본 암호화 인코더(Encoder)는 DelegatingPasswordEncoder입니다.
예전에는 사용할 알고리즘을 명시해서 사용해왔습니다. 하지만 DelegatingPasswordEncoder를 사용하면 사용하는 암호화 알고리즘 방식을 Map에 key(알고리즘 이름) : value(인코더 객체) 로 저장해두고 필요한 알고리즘을 상황에 맞추어 사용할 수 있습니다.
여러 형태의 인코더를 key값으로 불러와 편리하게 사용할 수 있고 추후에 새로운 인코더가 생겼을 때 Map에 추가해서 사용하면 됩니다.
위의 그림은 PasswordEncoderFactories에 기본적으로 들어있는 encoder들입니다.
마지막 라인에 return 값으로는 encodigId라는 key를 사용한다고 되어있는데, 위에 코드를 보니 encodingId = "bcrypt"이고 new BCryptPasswordEncoder 객체를 가지고 있는 key이네요. 결국 내장된 DelegatingPasswordEncoder를 이용하면 default로 BCryptPasswordEncoder를 사용하게 됩니다.
사용방법은 아래와 같습니다.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
//...
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
//...
}
저는 Config 파일에서 Bean 등록을 해서 사용해보았습니다.
PasswordEncoderFactories의.createDelegatingPasswordEncoder()의 반환값이 PasswordEncoder이기 때문에 해당 객체를 반환하는 메서드로 정의해주었습니다. 이렇게 하면 반환된 PasswordEncoder를 이용해서 BCryptPasswordEncoder를 사용할 수 있게 됩니다. createDelegatingPasswordEncoder() method내에서 이미 BCryptPasswordEncoder를 사용하는 것으로 설정되어 있기 때문에 어떤 인코더 방식인지 코드 상으로는 알 수가 없네요. (어차피 코드를 타고 들어가면 다 알 수 있는 것이라 이것도 어떠한 이점이 있는 것인가는 아직은 잘 모르겠습니다🤔)
@Controller
public class IndexController {
@Autowired
private MemberRepository memberRepository;
//Config file에서 정의한 Bean을 주입받게 됩니다.
@Autowired
private PasswordEncoder passwordEncoder;
//...
@PostMapping("/join")
public String join(Member member) {
member.setRole("ROLE_USER");
//비밀번호 암호화해서 저장하기!
String rawPassword = member.getPassword();
String encPassword = passwordEncoder.encode(rawPassword);
member.setPassword(encPassword);
//repository에 저장하기
memberRepository.save(member);
return "redirect:/login";
}
//...
}
Controller에서 객체 DI를 받고, .encode(인코딩할 비밀번호) 메서드를 이용해 암호화해주었습니다.
그리고 //repository에 저장하기 부분을 보시면 역시나 DI받은 memberRepository를 이용해 save(저장할 객체) 메서드를 이용해 DB에 저장해주었습니다. 저장하는 건 매우 쉽네요 ㅎㅎ
한 번 run 해서 의도대로 비밀번호가 암호화 되어서 잘 저장되는 지 확인해보았습니다.
h2에 저장된 데이터를 보니, PASSWORD가 bcrypt 형태로 변환되어서 저장된 것을 확인할 수 있었습니다.
DelegatingPasswordEncoder의 특징 중에 하나가 바로 괄호{} 안에 어떠한 인코딩 방식인지 명시되어 있다는 것입니다.
한번 DelegatingPasswordEncoder를 사용하지 않고 BCryptPasswordEncoder를 바로 사용했을 경우는 어떻게 저장되는지도 확인해보겠습니다.
Bean등록을 아까처럼 해주고
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
//...
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
//...
}
Controller의 코드도 변경해 줍니다.
@Controller
public class IndexController {
@Autowired
private MemberRepository memberRepository;
//Config file에서 정의한 Bean을 주입받게 됩니다.
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
//...
@PostMapping("/join")
public String join(Member member) {
member.setRole("ROLE_USER");
//비밀번호 암호화해서 저장하기!
String rawPassword = member.getPassword();
String encPassword = bCryptPasswordEncoder.encode(rawPassword);
member.setPassword(encPassword);
//repository에 저장하기
memberRepository.save(member);
return "redirect:/login";
}
//...
}
이렇게 명시적으로 인코더 객체를 이용해서 암호화를 하게되면,
아까와 같은 방식으로 member 등록을 했을 때 아래처럼 PASSWORD가 저장됩니다.
차이점이 보이시나요? {bcrypt} 부분이 없어졌습니다.
암호화에 사용된 알고리즘은 동일하지만 저장될 때 어떻게 저장되는지에는 차이가 있습니다.
위에서 보았던 사진인데, 만일 return되는 알고리즘이 "ldap"이었다면 DB에 저장될 때 {ldap}이라고 앞에 붙어서 나오게 됩니다.
어떠한 알고리즘을 사용해서 인코딩 한 것인지 구분하기기 좋은 것 같습니다.
이렇게 인증 단계에 앞서 어떻게 사용자 정보를 등록하는지, 특히나 비밀번호 암호화 방식에 초점을 두고 알아보았습니다. 사용자 정보 등록은 굉장히 쉬워서 설명했다고 하기도 뭐합니다 ㅎㅎ memberRepository.save(member)였으니 말입니다.
마지막으로 한가지 포인트는 DelegatingPasswordEncoder는 커스터마이징 해서 사용할 수도 있다는 점입니다. 꼭 factory에서 제공하는 것을 사용해야 하는 것은 아니고 사용할 encoder들만 모아둔 DelegatingPasswordEncoder를 만들어서 사용할 수가 있습니다. 그렇게 하면 위에서 언급한대로 필요에 따라서 원하는 알고리즘을 선택해서 사용하기 훨씬 편리할 것입니다.
커스터마이징 예시는 공식 문서에 잘 표현이 되어있습니다. 저도 조만간 직접 코드 작성해서 블로그에 업데이트 해보겠습니다 :)
감사합니다!
참고자료
https://docs.spring.io/spring-security/reference/features/authentication/password-storage.html
[커스텀 DelegatingPasswordEncoder 사용방법]
https://www.baeldung.com/spring-security-5-password-storage
https://www.youtube.com/watch?v=l6RYnUHaYMk
https://gompangs.tistory.com/entry/Spring-Password-Encoder
'Java Spring > Security' 카테고리의 다른 글
JWT란? JWT 생성과 사용 방법 알아보기 (0) | 2022.08.05 |
---|---|
Authorization(인가)-인가 내부 절차 & 권한 설정하기 (0) | 2022.08.01 |
Authentication(인증) (2) - 로그인 절차 & 코드구현 (0) | 2022.07.30 |
Spring Security의 작동 방식 알아보기 (0) | 2022.07.28 |
Spring Security Framework은 어떤 도구인지 대표적인 기능 알아보기 (0) | 2022.07.25 |