해당 글은 SpringBoot 기본 문서를 번역해서 적은 것입니다.
오타/오역이 있을 수 있습니다!
Spring Security의 특징
- 인증, 권한 부여 및 일반적인 악용에 대한 보호를 위한 포괄적인 지원을 제공
- 사용을 단순화하기 위해 다른 라이브러리와 통합을 제공
Authentication( 인증)
- 특정 리소스에 액세스하려는 사람의 신원을 확인
- 사용자를 인증하는 일반적인 방법은 로그인
- 인증이 수행되면 우리는 신원을 알고 인증을 수행할 수 있다.
- Spring Security는 사용자 인증을 위한 기본 지원을 제공
Password
Password Storage
passwordEncoder인터페이스는 비밀번호를 안전하게 저장할 수 있도록 단방향 변환을 하는데 사용된다. 데이터베이스 인증에 사용되는 자격 증명 저장에는 유용하지 않다.
일반적으로 passwordEncoder인증 시 사용자가 제공한 비밀번호와 비교해야 하는 비밀번호를 저장하는데 사용한다.
Password Storage History
수년동안 비밀번호 보안은 계속 진화하였다.
처음에는 텍스트로 저장되었고 인증서를 통해 보안인증되었으나, 공공인증이 많아지는 탓에 악용유저들이 손쉽게 진입할 수 있었다.
그 이후 hash방식의 단방향 비밀번호 암호화가 생겼다. 악용유저들은 이에 대응하기 위하여 RainbowTable이라는 공격방식을 고안해 냈다. 각 비밀번호를 매번 작업하기보다는 한번에 조회할 수 있는 테이블을 만든것이다.
이러한 악용에 대해 대비하기 위해 salted password를 만들어냈다.
해쉬방식으로 만든 비밀번호에 임의의 바이트(salt라고도 함)를 매 유저의 비밀번호에 생성하는것
유저의 비밀번호와 솔트코드가 저장되고 , 유저가 입력한 코드와 솔트를 통해 비밀번호 인증을 수행하게 된다.
현재에는 SHA-256과 같은 암호화 해시가 더 이상 안전하지 않다.
최신 하드웨어를 사용하면 초당 수십억개의 해시 계산을 수행할 수 있기 때문이다.
이는 각 비밀번호를 개별적으로 쉽게 해독할 수 있다.
이제 개발자는 적응형 단방향 기능을 활용하여 비밀번호를 저장하는 것이 좋다.
시스템에서 비밀번호를 확인하는데 약 1초가 걸리도록 작업 요소를 조정하는 것이 좋다.
이러한 절충안은 공격자가 암호를 해독하기 어렵게 만드는 것이지만, 시스템에 과도한 부담을 주거나 사용자가 스트레스나 짜증을 호소할 만큼 비용이 많이 들지는 않는다.
시스템에따라 성능이 크게 다르기 때문에 사용자가 자신의 시스템에 맞는 작업 요소를 사용자 정의하도록 권장한다.
사용해야 하는 적응형 단방향 함수의 예로는 bcrypt, PBKDF2, scrypt 및 argon2가 있다.
적응형 단방향 기능은 의도적으로 리소스를 많이 사용하므로 모든 요청에 대해 사용자 이름과 비밀번호를 검증하면 애플리케이션 성능이 많이 저하도리 수 있다.
비밀번호 검증 속도를 높이기 위해 Spring Security가 할 수 있는 일은 없다.
보안은 검증 리소스를 집중적으로 만들어서 얻을 수 있기 때문
사용자는 장기 자격 증명(사용자 이름,비밀번호)을 단기 자격증명(세션, OAuth토큰)으로 교환하는 것이 좋다.
그러면 단기 자격증명은 보안 손실 없이 신속하게 검증될 수 있다.
비밀번호 인코더 위임
- Spring Security 5.0이전에는 기본값이 일반 텍스트 비밀번호와 필요한 PasswordEncoder의NoOpPasswordEncoder 이었다.
- 비밀번호 기록 센셕을 근간으로, 너는 아마 기본 passwordEncoder가 현재 BCryptPasswordEncoder가 될거라고 예측할 것이다.
- 하지만 현실적인 문제점이 있다.
- 많은 어플리케이션의 오래된 암호화 방식을 쉽게 변경하지 못한다.
- 모범적인 예시가 계속 바뀔것이다.
- 프레임워크에서, Spring Security는 자주 변화하지 않는다.
- 대안으로 PasswordEncoderFactories를 사용하여 쉽게 DelegatingPasswordEncoder를 구성할 수 있다.
기본 DelegatingPasswordEncoder 생성
PasswordEncoder passwordEncoder =
PasswordEncoderFactories.createDelegatingPasswordEncoder();
커스텀하여 생성
String idForEncode = "bcrypt";
Map encoders = new HashMap<>();
encoders.put(idForEncode, new BCryptPasswordEncoder());
encoders.put("noop", NoOpPasswordEncoder.getInstance());
encoders.put("pbkdf2", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5());
encoders.put("pbkdf2@SpringSecurity_v5_8", Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("scrypt", SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1());
encoders.put("scrypt@SpringSecurity_v5_8", SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("argon2", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2());
encoders.put("argon2@SpringSecurity_v5_8", Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());
encoders.put("sha256", new StandardPasswordEncoder());
PasswordEncoder passwordEncoder =
new DelegatingPasswordEncoder(idForEncode, encoders);
Password Storage Format
- 비밀번호의 일반적인 형식은 다음과 같다.
- id는 꼭 {}로 감싸져있어야 한다.
- id는 어떤 psswordEncoder가 사용되었는지 식별자이고 encodedPassword는 선택된 PasswordEncoder의 원래 암호화된 비밀번호이다.
- id와 encodedPassword가 맞으면 해당 인코더로 위임된다.
{id}encodedPassword {bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG {noop}password {pbkdf2}5d923b44a6d129f3ddf3e3c8d29412723dcbde72445e8ef6bf3b508fbf17fa4ed4d6b99ca763d8dc {scrypt}$e0801$8bWJaSu2IKSn9Z9kM+TPXfOc/9bdYSrN1oD9qfVThWEwdRTnO7re7Ei+fUZRJ68k9lTyuTeUp4of4g24hHnazw==$OAOec05+bXxvuu/1qZ6NUR+xQYvYv7BeL1QxwRpY5Pc= {sha256}97cde38028ad898ebc02e690819fa220e88c62e0699403e94fff291cfffaf8410849f27605abcbc0
예제
- idForEncode 생성자에 전달된 값은 PasswordEncoder 암호 인코딩에 사용되는 값을 결정한다.
- 앞서 구성한 DelegatingpasswordEncoder에서는 암호 인코딩 결과가{bcrypt}에 위임되고 {bcrypt}접두사가 붙는 것을 의미한다.
{bcrypt}$2a$10$dXJ3SW6G7P50lGmMkkmwe.20cQQubK3.HZWzG3YB1tlRy.fqvM/BG
Password Matching
- 일치하는지는 {id} 와 생성자에 제공된 비밀번호 저장 형식을 기반으로 한다.
- 기본적으로 매핑되지 않은 id(Null포함) 비밀번호와 id 호출결과는 IllegalArgumentException이다.
- DelegatingPasswordEncoder.setDefaultPasswordEncoderForMatches(PasswordEncoder)에 의해 사용자 정의할 수도 있다.
암호화 시작하기 예시
- 저장된 비밀번호를 해쉬화 해주지만 여전히 메모리와 컴파일된 소스 코드는 노출된다.
- 따라서 프로덕션 환경에서는 여전히 안전한것으로 간주되지 않는다.
- 프로덕션의 경우 비밀번호를 외부에서 해시해야 한다.
- 단일 유저 예시
UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("user") .build(); System.out.println(user.getPassword());
- 여러 사용자 생성시 빌더를 재사용
UserBuilder users = User.withDefaultPasswordEncoder(); UserDetails user = users .username("user") .password("password") .roles("USER") .build(); UserDetails admin = users .username("admin") .password("password") .roles("USER","ADMIN") .build();
BCryptPassword인코더
- 널리 지원되는 bcrypt알고리즘을 사용하여 해시한다.
- 비밀번호 크래킹에 대한 저항력을 높이기 위해 의도적으로 느리게 작용
- 다른 적응형 단방향 기능과 마찬가지로 시스템에서 비밀번호를 확인하는데 1초정도 걸리도록 조정해야 한다.
- 기본 구현에서 강도 10을 사용
BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(16);
String result = encoder.encode("myPassword");
assertTrue(encoder.matches("myPassword", result));
Argon2Password인코더
- 구현 에서는 Argon2
Argon2PasswordEncoder
알고리즘을 사용하여 비밀번호를 해시합니다.
- Argon2는 비밀번호 해싱 대회 에서 우승했다.
- 맞춤형 하드웨어에서 비밀번호 크래킹을 방지하기 위해 Argon2는 많은 양의 메모리가 필요한 의도적으로 느린 알고리즘이다.
- 다른 적응형 단방향 기능과 마찬가지로 시스템에서 비밀번호를 확인하는 데 약 1초가 걸리도록 조정해야 한다.
- 현재 구현에는 BouncyCastle이 필요하다.
// Create an encoder with all the defaults Argon2PasswordEncoder encoder = Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8(); String result = encoder.encode("myPassword"); assertTrue(encoder.matches("myPassword", result));
비밀번호 저장 구성
- Spring Security는 기본적으로 DelegatingPasswordEncoder를 사용한다.
- 그러나 PasswordEncoder Spring Bean으로 노출하여 이를 사용자 정의할 수 있다.
비밀번호 구성 변경
- 사용자가 비밀번호를 지정할 수 있도록 대부분의 애플리케이션에서는 해당 비밀번호를 업데이트 하는 기능도 필요하다.
- 빔리번호 변경을 위해 잘 알려진 URL은 비밀번호 관리자가 특정 애플리케이션에 대한 비밀번호 업데이트 엔드 포인트를 검색할 수 있는 메커니즘을 나타낸다.
- 이 검색 엔드포인트를 제공하도록 Spring Security에서 구성할 수 있다.
- 예를 들어, 엔드포인트가 /change-password일 때
http .passwordManagement(Customizer.withDefaults())
- 그런 다음 비밀번호 관리자가 /.well-known/change-password 로 이동하면 Spring Security가 엔드포인트를 리디렉션한다.
- 엔트도핑ㄴ트가 아닌 경우 /change-apssword를 다음과 같이 지정할 수도 있다.
http .passwordManagement((management) -> management .changePasswordPage("/update-password") )