본문 바로가기
web/SpringBoot

Spring Boot 3.1.x 으로 RestFul API 서버 만들기(3)

by 뽀리님 2023. 8. 31.

 

저번시간에 이어 JWT를 이용하여 로그인 API를  만들어보고자 한다.

우선 내가 생각한 인증 프로세스는 대략 이렇다.

참조 https://webfirewood.tistory.com/115

최초 로그인시에는 사용자의 아이디와 패스워드를 받은 후 검증을 통해 JWT 객체를 반환하고, 이후 또 다른 API 요청시 발급받은 JWT 함께 보낸다.

 

 

1. 먼저 POST /login 을 생성한다.

/**
 * 로그인수행
 * @return
 */
@PostMapping("/login")
public ResponseEntity<TokenDto> loginSuccess(@RequestBody AuthDto authDto) {
    TokenDto token = userService.loginProcess(authDto.getLoginId(), authDto.getPassword());
    return ResponseEntity.ok(token);
}

파라미터로 인증용 객체를 생성한후, 요청 ID 와 PW를 받았다.

 

 

이전 포스팅에 설명된 Spring Security 관련 흐름을 보면 이렇다.

1. 사용자의 요청 정보 전송
2. AuthenticationFilter가 요청을 받아서 UsernamePasswordAuthenticationToken(인증용 객체)을 생성한다.
3~4. AuthenticationManager가 인증을 처리할 수 있는 AuthenticationProvider 에게 전달한다.
5~6. DB에 있는 사용자 정보와 요청받은 정보를 비교 후 loadUserByUsername() 메소드 수행
7~8. 성공시 Authentication 객체리턴
9. Authentication 객체를 SecurityContextHolder에 담은 이후 각 결과별 Handler 처리

 

여기서 입력받은 ID/PW 로 요청을 받아 UsernamePasswordAuthenticationToken(ID,PW) 를 통해 인증용 객체를 생성 후

AuthenticationProvider() 인증용 객체를 전달한다.

AuthenticationProvider 인증 성공, 실패, 결정할 없음을 나타낼 있고, 나머지 AuthenticationProvider 결정을 있도록 전달한다.

따라서, password 기반 인증 경우 ID/PW 유효한지 내부에서 Validaion Check가 가능하다.

 

 

2. ID, PW 인증 기반이므로 기본적으로 시큐리티에서 제공하는 passwordEncoder 를 Bean 등록해준다.

@Bean
public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
}

 

여러가지  패스워드 암호화방식이 있겠지만 나는 보편적으로 쓰이는 BCryptPasswordEncoder를 이용하여 해싱 처리를 했다.

 

3. AuthenticationProvider 를 커스텀한  CustomAuthenticationProvider 를 생성한다.

public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;
    private final PasswordEncoder passwordEncoder;

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

        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        if (!passwordEncoder.matches(password, userDetails.getPassword())) {
            throw new BadCredentialsException("패스워드가 일치하지 않습니다.");
        }
        Authentication authenticatedUser =  new UsernamePasswordAuthenticationToken(userDetails, password, userDetails.getAuthorities());
        SecurityContextHolder.getContext().setAuthentication(authenticatedUser);

        return authenticatedUser;
    }

 

입력받은 ID 와 PW 유효성을 체크 후 인증정보를 SecurityContextHolder 에 저장한다.

(※ 참고로 ID를 체크하고 싶다면 UserDetailsService를 Custom 하면 된다.)

 

 

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
    return (web) -> web.ignoring().requestMatchers(AUTH_WHITELIST);
}

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
    return httpSecurity
            .authorizeHttpRequests(  // 요청에 대한 인가 규칙설정 
                    authorize -> authorize
                                .anyRequest()
                                .authenticated()
            )
            .csrf(AbstractHttpConfigurer::disable) // CSRF 토큰 보호 해제
            .httpBasic(AbstractHttpConfigurer::disable) // HTTP 기본 인증 비활성화
            .formLogin(AbstractHttpConfigurer::disable) // 폼 기반 로그인 비활성화
            .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class) // 인증필터추가
            .build();
}

 

Security와 filter 에 적용시키지 않을 리소스를 WebSecurityCustomizer를 Bean 등록한다.

httpSecurity.authorizeHttpRequests 에 requestMatchers.permitAll() 로 처리해도 상관이 없으나,  그렇게 하면 또 필터에 shouldNotFilter를 오버라이드해서 써야 하므로 귀찮으니 난 걍 저걸 쓰겠다.

REST API 방식을 사용할 때는 쿠키를 사용해서 인증하는 방식을 사용하지 않기에 CSRF 설정은 껐다.

 

 

 

4. Service 에 로직 추가

@Transactional
public TokenDto loginProcess(String id, String password) {
    UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(id, password);
    Authentication authentication = customAuthenticationProvider.authenticate(authenticationToken);
    TokenDto token = jwtTokenProvider.generateToken(authentication);
    userRepository.updateLoginInfo(token.getRefreshToken(),id);
    return token;
}

 

인증객체 생성 후 커스텀한 Provider로 인증을 수행한다. 검증된 정보로 토큰을 생성 후 DB에 리프레시토큰 및 접속시간을 업데이트 시킨다.

 

 

5. Test

 

 

 

테스트해보니 잘된다!!

 

이 후 인증이 필요한 API는 발급받은 JWT 를 Header 에 담아서 통신하면 되겠다.

 

기본적으로 필요한것들만 정리하였고, 추가로 필요한 로직은 저기서 입맛에 맞게 추가하면 된다.