본 시리즈는 작성자의 이해와 경험을 바탕으로 실습 위주의 설명을 기반으로 작성되었습니다.
실습 위주의 이해를 목표로 하기 때문에 다소 과장이 많고 생략된 부분이 많을 수 있습니다.
따라서, 이론적으로 미흡한 부분이 있을 수 있는 점에 대해 유의하시기 바랍니다.
또한, 본 시리즈는 ChatGPT의 도움을 받아 작성되었습니다.
수 차례의 질문을 통해 도출된 여러가지 다양한 방식의 코드를 종합하여
작성자의 이해와 경험을 바탕으로 가장 정석으로 생각되는 코드를 재정립하였습니다.
UserService
,UserRestController
는 저장된 사용자 정보의 접근에 관여하며,
AuthService
,AuthRestController
는 로그인 및 회원가입, 토큰의 만료기간 등에 관여합니다.
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 입니다.
@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 입니다.
UserService
,UserRestController
는 저장된 사용자 정보의 접근에 관여하며,
AuthService
,AuthRestController
는 로그인 및 회원가입, 토큰의 만료기간 등에 관여합니다.
@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
을 갱신하는 기능입니다.
@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 입니다.