실전: 작고 안전한 웹 애플리케이션

6.1. 프로젝트 요구 사항과 설정

  • 제품 정보와 사용자를 DB에 저장한다.

  • 암호는 bcrypt나 scrypt로 해시된다.

  • AuthenticationFilter는 요청을 가로채서 인증 책임을 AuthenticationManager에 위임한다.

  • AuthenticationManager는 AuthenticationProvider로 요청을 인증한다.

  • AuthenticationProvider는 인증 로직과 관련된 모든 사항을 구현한다.

    • AuthenticationProvider를 구현하는 AuthenticationProviderService를 구현한다.

    • UserDetailsService로 DB의 사용자 정보를 찾고 PasswordEncoder로 암호를 검증한다.

      • UserDetailsService는 사용자 관리를 담당한다.

    • JpaUserDetailsService가 데이터베이스 작업을 담당한다.

  • 성공하고 반환된 세부 정보는 AuthenticationFilter에 의해 SecurityContext에 저장된다.

6.2. 사용자 관리 구현


@Configuration
public class ProjectConfig {

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder() {
        return new SCryptPasswordEncoder(
                16384,   // CPU cost (N)
                8,     // Memory cost (r)
                1,     // Parallelization (p)
                32,    // Key length
                64     // Salt length
        );
    }
}
public class CustomUserDetails implements UserDetails {

    private final User user;

    public CustomUserDetails(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return user.getAuthorities().stream()
                .map(a -> new SimpleGrantedAuthority(a.getName()))
                .collect(Collectors.toList());
    }

    ...
}

@Service
public class JpaUserDetailsService implements UserDetailsService {

    @Autowired
    private UserRepository userRepository;

    @Override
    public CustomUserDetails loadUserByUsername(String username) {
        Supplier<UsernameNotFoundException> s =
                () -> new UsernameNotFoundException("Problem during authentication!");

        User u = userRepository.findUserByUsername(username).orElseThrow(s);

        return new CustomUserDetails(u);
    }
}
  • 인증에 사용할 값들을 구현한다.

6.3. 맞춤형 인증 논리 구현


@Service
public class AuthenticationProviderService implements AuthenticationProvider {

    @Autowired
    private JpaUserDetailsService userDetailsService;

    @Autowired
    private BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    private SCryptPasswordEncoder sCryptPasswordEncoder;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String username = authentication.getName();
        String password = authentication.getCredentials().toString();

        CustomUserDetails user = userDetailsService.loadUserByUsername(username);

        return switch (user.getUser().getAlgorithm()) {
            case BCRYPT -> checkPassword(user, password, bCryptPasswordEncoder);
            case SCRYPT -> checkPassword(user, password, sCryptPasswordEncoder);
        };

    }

    @Override
    public boolean supports(Class<?> aClass) {
        return UsernamePasswordAuthenticationToken.class.isAssignableFrom(aClass);
    }

    private Authentication checkPassword(CustomUserDetails user, String rawPassword, PasswordEncoder encoder) {
        if (encoder.matches(rawPassword, user.getPassword())) {
            return new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword(), user.getAuthorities());
        } else {
            throw new BadCredentialsException("Bad credentials");
        }
    }
}
  • 실제 인증 로직을 구현할 AuthenticationProvider를 구현한다.


@Configuration
public class ProjectConfig {

    private final AuthenticationProvider authenticationProviderService;

    public ProjectConfig(AuthenticationProvider authenticationProviderService) {
        this.authenticationProviderService = authenticationProviderService;
    }

    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public SCryptPasswordEncoder sCryptPasswordEncoder() {
        return new SCryptPasswordEncoder(
                16384,   // CPU cost (N)
                8,     // Memory cost (r)
                1,     // Parallelization (p)
                32,    // Key length
                64     // Salt length
        );
    }

    @Bean
    public AuthenticationManager authenticationManager() {
        return new ProviderManager(List.of(authenticationProviderService));
    }

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http.formLogin(form -> form.defaultSuccessUrl("/main", true))
                .authorizeHttpRequests(auth -> auth.anyRequest().authenticated());

        return http.build();
    }
}
  • 구현한 프로바이더를 설정에 등록한다.

  • formLogin으로 등록하고 기본 경로를 설정한다.

Last updated

Was this helpful?