1) 회원 가입 API 과제명세
- username, password를 Client에서 전달받기
- username은
최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)
로 구성되어야 한다.- password는
최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)
로 구성되어야 한다.- DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기
참고자료
1. https://mangkyu.tistory.com/174
2. https://ko.wikipedia.org/wiki/정규_표현식
3. https://bamdule.tistory.com/35
@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {
@Id
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
public User(SignupRequestDto signupRequestDto) {
this.username = signupRequestDto.getUsername();
this.password = signupRequestDto.getPassword();
}
}
username
에 대한 중복검증을 요구하고, 사용자가 작성한 글에 대해서만 데이터변경이 가능하도록 하라는 점들로부터 username
을 PK로 정해야겠다는 결론을 내리고 @Id 어노테이션을 달아주었다.// @RestController는 @Controller에 @ResponseBody가 추가된 것. 주용도는 Json형태로 객체 데이터를 반환하는 것.
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
private final UserService userService;
@PostMapping("/signup")
public ResponseEntity<String> signup(@RequestBody @Valid SignupRequestDto signupRequestDto) {
userService.signup(signupRequestDto);
return new ResponseEntity<>("회원가입 성공", HttpStatus.CREATED);
}
}
@Getter
public class SignupRequestDto {
@Size(min = 4, max = 10,message = "username Size error")
@Pattern(regexp = "^[a-z0-9]*$",message = "username Pattern error")
private String username;
@Size(min = 8, max = 15,message = "password Size error")
@Pattern(regexp ="^[a-zA-Z0-9]*$", message = "password Pattern error")
private String password;
}
build.gradle
// @Valid를 통해 유효성검증을 하기 위해서는 의존성을 추가해주어야한다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
}
UserControler.signup의 매개변수(dto)앞에 @Valid를 붙여주고,
SignupRequestDto의 값들에 @Size, @Pattern을 사용하여 제약조건들을 걸어주었다.
@Transactional
public void signup(SignupRequestDto signupRequestDto) {
//회원 중복 확인 by username
Optional<User> overlapUser = userRepository.findByUsername(signupRequestDto.getUsername());
if(overlapUser.isPresent()) throw new OverlapUserExistException();
userRepository.save(new User(signupRequestDto));
}
2) 로그인 API 과제명세
- username, password를 Client에서 전달받기
- DB에서 username을 사용하여 저장된 회원의 유무를 확인하고 있다면 password 비교하기
- 로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고,
발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기
UserController
@PostMapping("/login")
public ResponseEntity<String> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
userService.login(loginRequestDto, response);
return new ResponseEntity<>("로그인 성공", HttpStatus.OK);
}
UserService
@Transactional
public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
User user = userRepository.findByUsername(loginRequestDto.getUsername()).orElseThrow(UserNotExistException::new);
user.validatePassword(loginRequestDto.getPassword());
response.addHeader(jwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername()));
}
로그인 API를 개발하는 과정에서 JWT와 HttpRequest, Response에 대한 배움을 많이 얻었다.
(역시 직접 써보고, 안되는 이유를 찾아가며 디버깅도 해봐야 이해가 잘된다.)
- 다른 부분보다 더 신기했던 점은, HttpResponse를 Request받는 시점에서 호출하여 값을 넣어줄수 있다는 점이다. (요청받는 시점에 응답객체도 동시에 생성되기 때문에 가능하다.)
취소선으로 표시된 부분은 삭제 /배경으로 감싸진 부분은 수정
1) 전체 게시글 목록 조회 API
작성자명(username)
, 작성 내용, 작성 날짜를 조회하기2) 게시글 작성 API
토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
제목, 작성자명(username), 작성 내용을 저장하고
3) 선택한 게시글 조회 API
작성자명(username)
, 작성 날짜, 작성 내용을 조회하기4) 선택한 게시글 수정 API
토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 수정 가능
제목, 작성 내용을 수정
하고 수정된 게시글을 Client 로 반환하기5) 선택한 게시글 삭제 API
토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 삭제 가능
// 컨트롤러의 updatePost를 예시로 가져왔다.
@PutMapping("/api/posts/{id}")
public PostResponseDto updatePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto, HttpServletRequest request){
return postService.updatePost(id, postRequestDto, request);
}
수정할 Post의 id를 @PathVariable을 통해 받고, @RequestBody를 통해 Dto에 수정할 Post 내용들을 담아서 받고, HttpServletRequest를 받아와서 header에 담긴 JWT를 확인하고자 했다.
- 친구와 코드리뷰를 하는 과정에서 Request 전체를 다 Service로 넘겨주는 것보다, 내가 필요한 부분들만 추출해서 넘겨주는게 좋다는 피드백을 받을 수 있었다.
// 서비스의 updatePost를 예시로 가져왔다.
@Transactional
public PostResponseDto updatePost(Long id, PostRequestDto postRequestDto, HttpServletRequest request) {
String token = jwtUtil.resolveToken(request);
String username;
tokenNullCheck(token);
if (jwtUtil.validateToken(token)) {
username = jwtUtil.getUserInfoFromToken(token).getSubject();
} else throw new IllegalArgumentException("Token Error");
Post post = postRepository.findById(id).orElseThrow(PostNotExistException::new);
post.validateUsername(username);
post.updatePost(postRequestDto);
postRepository.save(post);
return new PostResponseDto(post);
}
public void tokenNullCheck(String token) {
if (token == null) throw new IllegalArgumentException("토큰이 텅텅 비었어요");
}
postService.updatePost
1. 가져온 토큰을 resolve 해주고,
2. NullCheck를 해준다. (nullCheck는 여기저기서 계속 쓰이기도하고 직관적으로 어떤 작업을 하는지 알아보기 편하도록 메서드로 만들었다.
3. 유효한token
일 경우에username
을 추출해주고(검증과정과 추출과정에서 2번의 jwt verify가 진행된다는 점이 성능상으로 좋지 못하다는 점도 배웠다.)(강의에서 저렇게 알려줬다는게 킬포)
4. id를 활용해서 선택한 Post를 가져오고username
이 동일한지 검증한다.
5. 1~4가 무사히 진행됐다면? 업데이트를 진행한다.
- 과제의 다른 구현사항들을 확인하길 원하신다면 게시물 처음에 레파지토리에서 확인 가능합니다.