[React + SpringBoot] JWT 인증 구현 ⑥ - Service, RestController

SihoonCho·2023년 4월 21일
0
post-thumbnail

※ 읽기에 앞서


본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.

또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.


📌 User


UserService, UserRestController는 저장된 사용자 정보의 접근에 관여하며,
AuthService, AuthRestController는 로그인 및 회원가입, 토큰의 만료기간 등에 관여합니다.


📖 UserService


UserService.java

@Service
@RequiredArgsConstructor
public class UserService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    
    /** User 조회 */
    @Transactional
    public UserResponseDto findById(Long id) {
        User user = this.userRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. user_id = " + id));
        return new UserResponseDto(user);
    }

    /** User 수정 */
    @Transactional
    public void update(Long id, UserRequestDto requestDto) {
        User user = this.userRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. user_id = " + id));
        user.update(requestDto);
    }

    /** User 삭제 */
    @Transactional
    public void delete(Long id) {
        User user = this.userRepository.findById(id).orElseThrow(
                () -> new IllegalArgumentException("해당 유저를 찾을 수 없습니다. user_id = " + id));
        this.userRepository.delete(user);
    }
}

CREATE를 제외한 기본적인 CRUD 기능을 포함하고 있는 Service 입니다.


📖 UserRestController


@RestController
@RequiredArgsConstructor
public class UserRestController {

    private final UserService userService;
    private final JwtTokenProvider jwtTokenProvider;

    /** 회원정보 조회 API */
    @GetMapping("/api/v1/user")
    public ResponseEntity<?> findUser(@RequestHeader("Authorization") String accessToken) {
    	Long id = this.jwtTokenProvider.getUserIdFromToken(accessToken.substring(7));
        UserResponseDto userResponseDto = this.userService.findById(id);
        return ResponseEntity.status(HttpStatus.OK).body(userResponseDto);
    }

    /** 회원정보 수정 API */
    @PutMapping("/api/v1/user")
    public ResponseEntity<?> updateUser(@RequestHeader("Authorization") String accessToken, 
                                        @RequestBody UserRequestDto requestDto) {
        Long id = this.jwtTokenProvider.getUserIdFromToken(accessToken.substring(7));
        this.userService.update(id, requestDto);
        return ResponseEntity.status(HttpStatus.OK).body(null);
    }

    /** 회원정보 삭제 API */
    @DeleteMapping("/api/v1/user")
    public ResponseEntity<?> deleteUser(@RequestHeader("Authorization") String accessToken) {
        Long id = this.jwtTokenProvider.getUserIdFromToken(accessToken.substring(7));
        this.userService.delete(id);
        return ResponseEntity.status(HttpStatus.OK).body(null);
    }
}

CREATE를 제외한 기본적인 CRUD 기능을 포함하고 있는 REST API 입니다.


📌 Auth


UserService, UserRestController는 저장된 사용자 정보의 접근에 관여하며,
AuthService, AuthRestController는 로그인 및 회원가입, 토큰의 만료기간 등에 관여합니다.


📖 AuthService


@Service
@RequiredArgsConstructor
public class AuthService {
    private final UserRepository userRepository;
    private final AuthRepository authRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtTokenProvider jwtTokenProvider;

    /** 로그인 */
    @Transactional
    public AuthResponseDto login(AuthRequestDto requestDto) {
        // CHECK USERNAME AND PASSWORD
        User user = this.userRepository.findByUsername(requestDto.getUsername()).orElseThrow(
                () -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다. username = " + requestDto.getUsername()));
        if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다. username = " + requestDto.getUsername());
        }

        // GENERATE ACCESS_TOKEN AND REFRESH_TOKEN
        String accessToken = this.jwtTokenProvider.generateAccessToken(
                new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), user.getPassword()));
        String refreshToken = this.jwtTokenProvider.generateRefreshToken(
                new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), user.getPassword()));

        // CHECK IF AUTH ENTITY EXISTS, THEN UPDATE TOKEN
        if (this.authRepository.existsByUser(user)) {
            user.getAuth().updateAccessToken(accessToken);
            user.getAuth().updateRefreshToken(refreshToken);
            return new AuthResponseDto(user.getAuth());
        }

        // IF NOT EXISTS AUTH ENTITY, SAVE AUTH ENTITY AND TOKEN
        Auth auth = this.authRepository.save(Auth.builder()
                        .user(user)
                        .tokenType("Bearer")
                        .accessToken(accessToken)
                        .refreshToken(refreshToken)
                        .build());
        return new AuthResponseDto(auth);
    }

    /** 회원가입 */
    @Transactional
    public void signup(UserRequestDto requestDto) {
        // SAVE USER ENTITY
        requestDto.setRole(Role.ROLE_USER);
        requestDto.setPassword(passwordEncoder.encode(requestDto.getPassword()));
        this.userRepository.save(requestDto.toEntity());
    }

    /** Token 갱신 */
    @Transactional
    public String refreshToken(String refreshToken) {
        // CHECK IF REFRESH_TOKEN EXPIRATION AVAILABLE, UPDATE ACCESS_TOKEN AND RETURN
        if (this.jwtTokenProvider.validateToken(refreshToken)) {
            Auth auth = this.authRepository.findByRefreshToken(refreshToken).orElseThrow(
                    () -> new IllegalArgumentException("해당 REFRESH_TOKEN 을 찾을 수 없습니다.\nREFRESH_TOKEN = " + refreshToken));

            String newAccessToken = this.jwtTokenProvider.generateAccessToken(
                    new UsernamePasswordAuthenticationToken(
                            new CustomUserDetails(auth.getUser()), auth.getUser().getPassword()));
            auth.updateAccessToken(newAccessToken);
            return newAccessToken;
        }

        // IF NOT AVAILABLE REFRESH_TOKEN EXPIRATION, REGENERATE ACCESS_TOKEN AND REFRESH_TOKEN
        // IN THIS CASE, USER HAVE TO LOGIN AGAIN, SO REGENERATE IS NOT APPROPRIATE
        return null;
    }
}

singup()

/** 회원가입 */
@Transactional
public void signup(UserRequestDto requestDto) {
    // SAVE USER ENTITY
    requestDto.setRole(Role.ROLE_USER);
    requestDto.setPassword(passwordEncoder.encode(requestDto.getPassword()));
    this.userRepository.save(requestDto.toEntity());
}

UserService의 CREATE 기능이 signup()에 포함되었습니다.
개발자 개인의 호불호에 맞게 UserService에서 구현해도 상관없습니다.

login()

/** 로그인 */
@Transactional
public AuthResponseDto login(AuthRequestDto requestDto) {
    // CHECK USERNAME AND PASSWORD
    User user = this.userRepository.findByUsername(requestDto.getUsername()).orElseThrow(
            () -> new UsernameNotFoundException("해당 유저를 찾을 수 없습니다. username = " + requestDto.getUsername()));
    if (!passwordEncoder.matches(requestDto.getPassword(), user.getPassword())) {
        throw new IllegalArgumentException("비밀번호가 일치하지 않습니다. username = " + requestDto.getUsername());
    }

    // GENERATE ACCESS_TOKEN AND REFRESH_TOKEN
    String accessToken = this.jwtTokenProvider.generateAccessToken(
            new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), user.getPassword()));
    String refreshToken = this.jwtTokenProvider.generateRefreshToken(
            new UsernamePasswordAuthenticationToken(new CustomUserDetails(user), user.getPassword()));

    // CHECK IF AUTH ENTITY EXISTS, THEN UPDATE TOKEN
    if (this.authRepository.existsByUser(user)) {
        user.getAuth().updateAccessToken(accessToken);
        user.getAuth().updateRefreshToken(refreshToken);
        return new AuthResponseDto(user.getAuth());
    }

    // IF NOT EXISTS AUTH ENTITY, SAVE AUTH ENTITY AND TOKEN
    Auth auth = this.authRepository.save(Auth.builder()
                    .user(user)
                    .tokenType("Bearer")
                    .accessToken(accessToken)
                    .refreshToken(refreshToken)
                    .build());
    return new AuthResponseDto(auth);
}

일련의 과정은 다음과 같습니다.
1. 사용자가 입력한 아이디 존재여부 확인, 비밀번호 일치여부 확인
2. ACCESS_TOKEN, REFRESH_TOKEN을 생성, USER와 연결된 AUTH 가 존재여부 확인
3. USER와 연결된 AUTH가 있다면, ACCESS_TOKEN, REFRESH_TOKEN을 업데이트
4. USER와 연결된 AUTH가 없다면, 새 AUTH 데이터 저장
5. AuthResponseDto return

refreshToken()

/** Token 갱신 */
@Transactional
public String refreshToken(String refreshToken) {
    // CHECK IF REFRESH_TOKEN EXPIRATION AVAILABLE, UPDATE ACCESS_TOKEN AND RETURN
    if (this.jwtTokenProvider.validateToken(refreshToken)) {
        Auth auth = this.authRepository.findByRefreshToken(refreshToken).orElseThrow(
                () -> new IllegalArgumentException("해당 REFRESH_TOKEN 을 찾을 수 없습니다.\nREFRESH_TOKEN = " + refreshToken));

        String newAccessToken = this.jwtTokenProvider.generateAccessToken(
                new UsernamePasswordAuthenticationToken(
                        new CustomUserDetails(auth.getUser()), auth.getUser().getPassword()));
        auth.updateAccessToken(newAccessToken);
        return newAccessToken;
    }

    // IF NOT AVAILABLE REFRESH_TOKEN EXPIRATION, REGENERATE ACCESS_TOKEN AND REFRESH_TOKEN
    // IN THIS CASE, USER HAVE TO LOGIN AGAIN, SO REGENERATE IS NOT APPROPRIATE
    return null;
}

일련의 과정은 다음과 같습니다.
1. REFRESH_TOKEN이 유효한지 확인
2. 유효하다면, REFRESH_TOKEN을 통해 해당 AUTH 데이터 찾기
3. 신규 ACCESS_TOKEN을 생성하고, 미리 찾은 AUTH 데이터에 업데이트
4. NEW_ACCESS_TOKEN return

즉, REFRESH_TOKEN이 유효하면, ACCESS_TOKEN을 갱신하는 기능입니다.


📖 AuthRestController


@RequiredArgsConstructor
@RestController
public class AuthRestController {
    private final AuthService authService;

    /** 로그인 API */
    @PostMapping("/api/v1/auth/login")
    public ResponseEntity<?> login(@RequestBody AuthRequestDto requestDto) {
        AuthResponseDto responseDto = this.authService.login(requestDto);
        return ResponseEntity.status(HttpStatus.OK).body(responseDto);
    }

    /** 회원가입 API */
    @PostMapping("/api/v1/auth/signup")
    public ResponseEntity<?> singUp(@RequestBody UserRequestDto requestDto) {
        this.authService.signup(requestDto);
        return ResponseEntity.status(HttpStatus.OK).body(null);
    }

    /** 토큰갱신 API */
    @GetMapping("/api/v1/auth/refresh")
    public ResponseEntity<?> refreshToken(@RequestHeader("REFRESH_TOKEN") String refreshToken) {
        String newAccessToken = this.authService.refreshToken(refreshToken);
        return ResponseEntity.status(HttpStatus.OK).body(newAccessToken);
    }
}

회원가입 및 로그인, 토큰에 관여하는 기능을 수행하는 REST API 입니다.

profile
꾸준히 노력하는 개발자

0개의 댓글