본문 바로가기
Today I Learned 2024. 5. 31.

(24.05.31)[7주차] Spring Security Filter에서의 Password Encoder

과제 중, 회원가입과 로그인 기능 가운데에서 JWT를 활용할 때, 이를 인증, 인가를 위한 방법으로 Spring Security를 사용할것이니 아닌지에 대한 명확한 구분점이 없어

User의 username과 password 중 password가 평문으로 DB에 저장한 부분에 관하여 Spring Security와의 충돌에 관하여 해결한 나름의 방법을 정리했다.

 

실무에서는 지양하는 방법이지만, Spring Security의 AuthenticationManager의 작동 위치와 방법을 다시 복습하면서 점검할 수 있었다.


Spring 숙련 개인 과제 주의할 점 정리

 

Server DB의 User Entity에 Password가 평문일 때의 Spring Security의 오류 분석하기

이슈

  • 회원가입한 비밀번호가 로그인시 기입한 비밀번호가 일치함에도 Encoded password does not look like BCrypt 경고가 뜨면서 예외를 발생

원인 분석

Security Filter Chain (출처 : https://spring.io/ )

 

  • Spring Framework의 Spring Security에서 여러 필터를 사용(FilterChain)하여 AuthenticationFilter AuthorizationFilter 인가 인증 필터 등을 사용하게 되는데 이때 AuthenticationManager은 인증을 하고 SecurityContextHolder에 UserDetails를 세팅할 수 있도록 하는 역할
public class WebSecurityConfig {

    private final JwtUtil jwtUtil;
    private final UserDetailsServiceImpl userDetailsService;
    private final AuthenticationConfiguration authenticationConfiguration;
    private final UserRepository userRepository;
    private final RefreshTokenRepository refreshTokenRepository;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
...
  • Spring Security의 로그인을 위해서 api/user/login 주소로 Client가 username과 password를 담아 보내왔을 때,  AuthenticationManager이 인증(Authoriazation Filter에서 인증)을 하는 과정에서 PasswordEncoder하는데 문제가 생긴것
  • AuthenticationManager은 UserDetailsService에서 UserDetails를 받아서 Client가 보낸 username password와 비교를 거치는 역할 중 비밀번호를 PasswordEncoder를 통해서 암호화 복화를 진행해서 비교할 때 문제
    • UserDetails는 User DB에 있는 User 정보를 User Repository를 통해 Username으로 찾은 User 객체를 토대로 만들어지게 됨
    • 즉, DB에 있는 User의 Password 또는 Client에서 보낸 password 둘을 암호화/복호화 하는 과정에서 발생을 한것으로 파악!
  • 참고) WebSecurityConfig에서 Bean을 통해 Authentication Manager가 사용하게 될 PasswordEncoder를 BCryptPasswordEncoder 로 사용하는 것으로 설정 했기 때문에 비밀번호가 BCrypt 된거 같지 않다고 파악을 한것

-> 과제의 조건에 DB단에서 확인을 위해, 회원가입 시, 사용자DB에는 입력된 Password값을 암호화를 하지 않은 평문으로 저장하라는 요구와 충돌

 

해결

첫번째 시도  : PassEncoder를 안쓰거나 평문으로 비교를 할 수 있게끔 하기

 @Bean
    public PasswordEncoder passwordEncoder() {
        return NoOpPasswordEncoder.getInstance();
    }
  • NoOpPasswordEncoder 사용을 하면 암호화/복화를 진행을 안한채로 AuthenticationManager가 비교해서 인증
    • 하지만, 사용 즉시 주의 표시 -> Spring Security를 사용하는 Spring Framework 5 이전의 구버전에서만 가능하고, 지금은 보안상 deprecated 되어 사용할 수 없음

두번째 시도 : UserDetailsService에서 UserDetail을 만들 때 비밀번호만 암호화

  • WebSecurityConfig에서 지정해 놓은  PasswordEncoder을 사용해서 User Repository에서 가져온 User 객체의 평문인 비밀번호만 Encode 시켜서 새로운 User  객체를 만들어 이를 통한 UserDetails 설정
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    

@Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        // DB에 평문으로 저장된 비밀번호 암호화
        User encodedUser = new User(user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getNickname(), user.getRole());

        return new UserDetailsImpl(encodedUser);
        
...

 

  • APPLICATION FAILED TO START : The dependencies of some of the beans in the application context form a cycle  에러 발생
 // AuthenticationManager 세팅
    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
        return configuration.getAuthenticationManager();
    }

 

  • Spring  Framework에서는 빈(Bean)들 사이에 순환 참조(circular reference) = 서로 참조를 하지 못하게 default 로 설정이 되어있음
    • 바로 위 코드는 WebSecurityConfig 클래스의 Bean으로 등록이 된 AuthenticationManger 이므로, private final PasswordEncoder passwordEncoder; 처럼 UserDetailsService 구현 클래스에 주입을 받아오게 되면 순환 참조가 생겨버림 -> 필드 주입에 유의해야하는 이유

세번째 시도 : UserDetailsService에서 UserDetail을 만들 때 비밀번호만 암호화 - 새로운 Encoder객체 형성

@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("Not Found " + username));

        // DB에 평문으로 저장된 비밀번호 암호화
        User encodedUser = new User(user.getUsername(), passwordEncoder.encode(user.getPassword()), user.getNickname(), user.getRole());

        return new UserDetailsImpl(encodedUser);
    }
}
  • 정상적으로 작동: AuthenticationManager 역시 BCryptPasswordEncoder 을 잘 사용해서 인증필터에서 로그인 시도 시 Client에서 요청한 username password와 비밀번호만 암호화시킨 encodedUser의 username password를 검증
  • 단, 이는 강제로 스스로가 AuthenticationManger 의 작동방식을 찾아보고 확인해보면서 다룬 나만의 방법이지, Filter단에서 함부로 User 객체를 암호화하거나 새로운 객체를 만들거나 하는 방법은 Security의 기능에 부합하지 않은 기능
    • 더해서 결국 encodedUser가 UserDetails로 반환되기때문에, Controller - Service 단에서는 다시 JpaRepository를 통한 findyByUsername 메서드쿼리 등을 한번 더 거쳐서 DB의 user를 찾아서 사용을 해야함

-> 학습을 통한 임시적인 방편이며, 보안법상 무조건 DB에는 평문의 암호를 저장할 수 없기 때문에, 이평문 Password를 다루는 Server는 거의 없다고 봐도 무방