Spring 숙련 주차 Lv2 완료
게시글 CRUD + jwt
- 회원가입 & 로그인
- 게시글 작성
- 게시글 전체 목록 조회
- 게시글 선택 조회
- 게시글 수정
- 게시글 삭제
환경설정
buile.grade(dependencies)
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation group: 'org.json', name: 'json', version: '20220924'
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
}
application.properties
spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=
spring.thymeleaf.cache=false
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=
Spring Application
@EnableJpaAuditing 추가
JPA Auditing을 활성화
도메인을 영속성 컨텍스트에 저장하거나 조회를 수행한 후에 update를 하는 경우,
매번 시간 데이터를 입력하여 주어야 하는데,
auditng을 이용하면 자동으로 시간을 매핑하여 데이터베이스의 테이블에 넣어주게 된다
Package 구조
controller
- PostController
- UserController
dto
- LoginRequestDto
- PostRequestDto
- PostResponseDto
- ResponseDto
- SignupRequestDto
entity
- Post
- Timestamped
- User
jwt
- JwtUtil
repository
- PostRepository
- UserRepository
service
- PostService
- UserService
JwtUtil
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
public static final String AUTHORIZATION_HEADER = "Authorization";
public static final String AUTHORIZATION_KEY = "auth";
private static final String BEARER_PREFIX = "Bearer ";
private static final long TOKEN_TIME = 60 * 60 * 1000L;
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// header 토큰을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
// 토큰 생성
public String createToken(String username) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, username)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
Dto
입력받을 Request 부분은 기능별로 만들었고
출력할 Response는 대표적으로 message와 status code를 반환하는 것,
post CRUD 기능에 대한 값을 반환하는 부분 2가지로 구성
Response
@Getter
@NoArgsConstructor
public class ResponseDto {
private String msg;
private int statusCode;
public ResponseDto(String msg, int statusCode) {
this.msg = msg;
this.statusCode = statusCode;
}
}
PostResponse
@Getter
@Setter
@NoArgsConstructor
public class PostResponseDto {
private Long id;
private String title;
private String content;
private String username;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public PostResponseDto(Post post) {
this.id = post.getId();
this.title = post.getTitle();
this.content = post.getContent();
this.username = post.getUsername();
this.createdAt = post.getCreatedAt();
this.modifiedAt = post.getModifiedAt();
}
}
User
- 로그인
- 회원가입
User
@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@OneToMany(mappedBy = "user", fetch = FetchType.EAGER)
private List<Post> post = new ArrayList<>();
public User(String username, String password) {
this.username = username;
this.password = password;
}
}
UserController
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class UserController {
private final UserService userService;
@PostMapping("/signup")
public ResponseDto signup(@RequestBody @Valid SignupRequestDto signupRequestDto) {
userService.signup(signupRequestDto);
return new ResponseDto("회원가입 성공", HttpStatus.OK.value());
}
@PostMapping("/login")
public ResponseDto login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
userService.login(loginRequestDto, response);
return new ResponseDto("로그인 성공", HttpStatus.OK.value());
}
}
UserService
@Service
@RequiredArgsConstructor
public class UserService {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
@Transactional
public void signup(SignupRequestDto signupRequestDto) {
String username = signupRequestDto.getUsername();
String password = signupRequestDto.getPassword();
// 회원 중복확인
Optional<User> found = userRepository.findByUsername(username);
if (found.isPresent()) {
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}
User user = new User(username, password);
userRepository.save(user);
}
@Transactional(readOnly = true)
public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
String username = loginRequestDto.getUsername();
String password = loginRequestDto.getPassword();
//사용자 확인
User user = userRepository.findByUsername(username).orElseThrow(
() -> new IllegalArgumentException("등록된 사용자가 없습니다.")
);
// 비밀번호 확인
if(!user.getPassword().equals(password)) {
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername()));
}
}
Post
- 게시물 작성
- 게시물 전체 조회
- 게시물 선택 조회
- 게시물 수정
- 게시물 삭제
Post
@Entity
@NoArgsConstructor
@Getter
@Setter
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@Column(nullable = false)
private String username;
@ManyToOne
@JoinColumn(name = "user_id")
private User user;
public Post(PostRequestDto requestDto, String username) {
this.title = requestDto.getTitle();
this.content = requestDto.getContent();
this.username = username;
}
public void update(PostRequestDto postRequestDto) {
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
this.username = username;
}
}
PostController
@RestController
@RequiredArgsConstructor
public class PostController {
private final PostService postService;
@PostMapping("/api/post")
public PostResponseDto createPost(@RequestBody PostRequestDto postRequestDto, HttpServletRequest request) {
return postService.createPost(postRequestDto, request);
}
@GetMapping("/api/posts")
public List<PostResponseDto> getPosts() {
return postService.getPosts();
}
@GetMapping("/api/post/{id}")
public PostResponseDto getPost(@PathVariable Long id) {
return postService.getPost(id);
}
@PutMapping("/api/post/{id}")
public PostResponseDto updatePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto, HttpServletRequest request) {
return postService.updatePost(id, postRequestDto, request);
}
@DeleteMapping("/api/post/{id}")
public ResponseDto deletePost(@PathVariable Long id, HttpServletRequest request) {
return postService.deletePost(id, request);
}
}
PostService
@Service
@RequiredArgsConstructor
public class PostService {
private final PostRepository postRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
@Transactional
public PostResponseDto createPost(PostRequestDto postRequestDto, HttpServletRequest request) {
// Request에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
Claims claims;
// 토큰이 있는 경우에만 게시물 작성 가능
if (token != null) {
if (jwtUtil.validateToken(token)) {
claims = jwtUtil.getUserInfoFromToken(token);
} else {
throw new IllegalArgumentException("토큰 에러");
}
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
Post post = postRepository.saveAndFlush(new Post(postRequestDto, user.getUsername()));
return new PostResponseDto(post);
} else {
return null;
}
}
@Transactional(readOnly = true)
public List<PostResponseDto> getPosts() {
return postRepository.findAllByOrderByModifiedAtDesc();
}
@Transactional(readOnly = true)
public PostResponseDto getPost(Long id) {
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("조회하려는 게시물이 존재하지 않습니다.")
);
return new PostResponseDto(post);
}
@Transactional
public PostResponseDto updatePost(Long id, PostRequestDto postRequestDto, HttpServletRequest request) {
User user = userInfo(request);
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("게시물이 존재하지 않습니다.")
);
if (post.getUsername().equals(user.getUsername())) {
post.update(postRequestDto);
}
else {
throw new IllegalArgumentException("등록한 게시물만 수정할 수 있습니다.");
}
return new PostResponseDto(post);
}
@Transactional
public ResponseDto deletePost(Long id, HttpServletRequest request) {
User user = userInfo(request);
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException("게시물이 존재하지 않습니다.")
);
if (post.getUsername().equals(user.getUsername())) {
postRepository.deleteById(id);
}
else {
throw new IllegalArgumentException("등록한 게시물만 삭제할 수 있습니다.");
}
return new ResponseDto("삭제 완료", HttpStatus.OK.value());
}
private User userInfo(HttpServletRequest request) {
String token = jwtUtil.resolveToken(request);
Claims claims;
if(token != null) {
if (jwtUtil.validateToken(token)) {
// 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
}
else {
throw new IllegalArgumentException("토큰 에러");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
return user;
} else {
throw new IllegalArgumentException("해당 토큰값과 일치하는 정보가 없습니다.");
}
}
}
Postman
게시글 CRUD 및 회원 가입, 로그인을 테스트
설계했던 API 명세서대로 잘 동작하는 걸 확인할 수 있다
'TIL ⚓️ > 4주차_주특기 숙련(Spring)' 카테고리의 다른 글
TIL / 항해99 12기 36일차_230213_Mon (0) | 2023.02.14 |
---|