username
,password
전달 받은 후 DB에 중복된 username이 없다면 회원 저장username
: 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)password
: 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9){
"username":"asdf12",
"password":"asdf1234"
}
{
"message": "회원가입 성공!",
"data": null
}
{
"username":"asdf12",
"password":"asdf1234"
}
{
"message": "로그인 성공!",
"data": null
}
package com.sparta.spring_post.controller;
import com.sparta.spring_post.dto.PostRequestDto;
import com.sparta.spring_post.dto.ResponseDto;
import com.sparta.spring_post.entity.Post;
import com.sparta.spring_post.service.PostService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class PostController {
// PostService 연결
private final PostService postService;
// 목록 조회
@GetMapping("/posts")
public ResponseDto<List<Post>> getAllPosts() {
return postService.getAllPosts();
}
// 상세 조회
@GetMapping("/posts/{id}")
public ResponseDto<Post> getPost(@PathVariable Long id) {
return postService.getPost(id);
}
// 추가
@PostMapping("/post")
public ResponseDto<Post> createPost(@RequestBody PostRequestDto postRequestDto, HttpServletRequest httpServletRequest) {
return postService.createPost(postRequestDto, httpServletRequest);
}
// 수정
@PutMapping("/post/{id}")
public ResponseDto<Post> updatePost(@PathVariable Long id, @RequestBody PostRequestDto postRequestDto, HttpServletRequest httpServletRequest) {
return postService.updatePost(id, postRequestDto, httpServletRequest);
}
// 삭제
@DeleteMapping("/post/{id}")
public ResponseDto deletePost(@PathVariable Long id, HttpServletRequest httpServletRequest) {
return postService.deletePost(id, httpServletRequest);
}
}
package com.sparta.spring_post.controller;
import com.sparta.spring_post.dto.LoginRequestDto;
import com.sparta.spring_post.dto.ResponseDto;
import com.sparta.spring_post.dto.SignupRequestDto;
import com.sparta.spring_post.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/user")
public class UserController {
// UserService 연결
private final UserService userService;
// 회원가입
@PostMapping("/signup")
public ResponseDto signup(@RequestBody SignupRequestDto signupRequestDto) {
return userService.signup(signupRequestDto);
}
// 로그인
@PostMapping("/login")
public ResponseDto login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse httpServletResponse) {
return userService.login(loginRequestDto, httpServletResponse);
}
}
package com.sparta.spring_post.dto;
import lombok.Getter;
@Getter
public class SignupRequestDto {
private String username;
private String password;
}
package com.sparta.spring_post.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class LoginRequestDto {
private String username;
private String password;
}
package com.sparta.spring_post.dto;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@AllArgsConstructor(staticName = "set")
public class ResponseDto<D> {
private String message;
private D data;
public static <D> ResponseDto<D> setSuccess(String message, D data) {
return ResponseDto.set(message, data);
}
public static <D> ResponseDto<D> setFailed(String message) {
return ResponseDto.set(message, null);
}
}
package com.sparta.spring_post.entity;
import com.sparta.spring_post.dto.PostRequestDto;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@Entity
@NoArgsConstructor
public class Post extends Timestamped {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String content;
@ManyToOne
@JoinColumn(name = "user_name", nullable = false)
private Users users;
public Post(Users users, PostRequestDto postRequestDto) {
this.users = users;
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
}
public void update(PostRequestDto postRequestDto) {
this.title = postRequestDto.getTitle();
this.content = postRequestDto.getContent();
}
}
package com.sparta.spring_post.entity;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime modifiedAt;
}
package com.sparta.spring_post.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
public class Users {
@Id
@Column(name = "user_name", nullable = false, unique = true)
private String username;
@Column(nullable = false)
@JsonIgnore
private String password;
public Users(String username, String password) {
this.username = username;
this.password = password;
}
}
package com.sparta.spring_post.jwt;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import jakarta.annotation.PostConstruct;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@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)
.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();
}
}
package com.sparta.spring_post.repository;
import com.sparta.spring_post.entity.Post;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostRepository extends JpaRepository<Post, Long> {
List<Post> findAllByOrderByCreatedAtDesc();
}
package com.sparta.spring_post.repository;
import com.sparta.spring_post.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<Users, Long> {
Optional<Users> findByUsername(String username);
}
package com.sparta.spring_post.service;
import com.sparta.spring_post.dto.PostRequestDto;
import com.sparta.spring_post.dto.ResponseDto;
import com.sparta.spring_post.entity.Post;
import com.sparta.spring_post.entity.Users;
import com.sparta.spring_post.jwt.JwtUtil;
import com.sparta.spring_post.repository.PostRepository;
import com.sparta.spring_post.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@RequiredArgsConstructor
public class PostService {
// PostRepository 연결
private final PostRepository postRepository;
// UserRepository 연결
private final UserRepository userRepository;
// JwtUtil 연결
private final JwtUtil jwtUtil;
// 목록 조회
@Transactional(readOnly = true)
public ResponseDto<List<Post>> getAllPosts() {
List<Post> posts = postRepository.findAllByOrderByCreatedAtDesc();
return ResponseDto.setSuccess("게시물 목록 조회 성공!", posts);
}
// 상세 조회
@Transactional(readOnly = true)
public ResponseDto<Post> getPost(Long id) {
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException(id + "번 게시물이 존재하지 않습니다.")
);
return ResponseDto.setSuccess(id + "번 게시물 조회 성공!", post);
}
// 추가
@Transactional
public ResponseDto<Post> createPost(PostRequestDto postRequestDto, HttpServletRequest httpServletRequest) {
String token = jwtUtil.resolveToken(httpServletRequest);
if (token == null) {
return ResponseDto.setFailed("토큰이 없습니다.");
}
try {
jwtUtil.validateToken(token);
} catch (Exception e) {
return ResponseDto.setFailed("유효한 토큰이 없습니다.");
}
Users user = userRepository.findByUsername(postRequestDto.getUsername()).orElseThrow();
Post post = new Post(user, postRequestDto);
postRepository.save(post);
return ResponseDto.setSuccess("게시물 작성 성공!", post);
}
// 수정
@Transactional
public ResponseDto<Post> updatePost(Long id, PostRequestDto postRequestDto, HttpServletRequest httpServletRequest) {
String token = jwtUtil.resolveToken(httpServletRequest);
Claims claims;
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException(id + "번 게시물이 없습니다.")
);
if (token == null) {
return ResponseDto.setFailed("토큰이 없습니다.");
}
try {
jwtUtil.validateToken(token);
} catch (Exception e) {
return ResponseDto.setFailed("유효한 토큰이 없습니다.");
}
claims = jwtUtil.getUserInfoFromToken(token);
if (post.getUsers().getUsername().equals(claims.getSubject())) {
post.update(postRequestDto);
return ResponseDto.setSuccess(id + "번 게시물 수정 성공!", post);
} else {
return ResponseDto.setFailed(id + "번 게시물을 수정할 권한이 없습니다.");
}
}
// 삭제
@Transactional
public ResponseDto deletePost(Long id, HttpServletRequest httpServletRequest) {
String token = jwtUtil.resolveToken(httpServletRequest);
Claims claims;
Post post = postRepository.findById(id).orElseThrow(
() -> new IllegalArgumentException(id + "번 게시물이 없습니다.")
);
if (token == null) {
return ResponseDto.setFailed("토큰이 없습니다.");
}
try {
jwtUtil.validateToken(token);
} catch (Exception e) {
return ResponseDto.setFailed("유효한 토큰이 없습니다.");
}
claims = jwtUtil.getUserInfoFromToken(token);
if (post.getUsers().getUsername().equals(claims.getSubject())) {
postRepository.deleteById(id);
return ResponseDto.setSuccess(id + "번 게시물 삭제 성공!", null);
} else {
return ResponseDto.setFailed(id + "번 게시물을 삭제할 권한이 없습니다.");
}
}
}
package com.sparta.spring_post.service;
import com.sparta.spring_post.dto.LoginRequestDto;
import com.sparta.spring_post.dto.ResponseDto;
import com.sparta.spring_post.dto.SignupRequestDto;
import com.sparta.spring_post.entity.Users;
import com.sparta.spring_post.jwt.JwtUtil;
import com.sparta.spring_post.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
import java.util.regex.Pattern;
@Service
@RequiredArgsConstructor
public class UserService {
// UserRepository 연결
private final UserRepository userRepository;
// JwtUtil 연결
private final JwtUtil jwtUtil;
@Transactional
public ResponseDto signup(SignupRequestDto signupRequestDto) {
String username = signupRequestDto.getUsername();
String password = signupRequestDto.getPassword();
// 아이디 형식 확인
if (!Pattern.matches("^[a-z0-9]{4,10}$", username)) {
return ResponseDto.setFailed("형식에 맞지 않는 아이디 입니다.");
}
// 비밀번호 형식 확인
if (!Pattern.matches("^[a-zA-Z0-9]{8,15}$", password)) {
return ResponseDto.setFailed("형식에 맞지 않는 비밀번호 입니다.");
}
// 회원 중복 확인
Optional<Users> found = userRepository.findByUsername(username);
if (found.isPresent()) {
return ResponseDto.setFailed("중복된 사용자입니다.");
}
Users users = new Users(username, password);
userRepository.save(users);
return ResponseDto.setSuccess("회원가입 성공!", null);
}
@Transactional(readOnly = true)
public ResponseDto login(LoginRequestDto loginRequestDto, HttpServletResponse httpServletResponse) {
String username = loginRequestDto.getUsername();
String password = loginRequestDto.getPassword();
// 아이디 확인
Users users = userRepository.findByUsername(username).orElseThrow(
() -> new IllegalArgumentException("존재하지 않는 아이디입니다.")
);
// 비밀번호 확인
if (!users.getPassword().equals(password)) {
return ResponseDto.setFailed("일치하지 않는 비밀번호 입니다.");
}
httpServletResponse.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(users.getUsername()));
return ResponseDto.setSuccess("로그인 성공!", null);
}
}
JWT가 너무 이해 안가서 3,4일 정도 다음 레벨 과제를 진행하지 않고 이해하는 시간을 가졌다.
JWT ( Json Web Token ) 은 말그대로 웹에서 사용되는 JSON 형식의 토큰에 대한 표준 규격인데 주로 클라이언트의 인증 또는 인가 정보를 서버와 클라이언트 간에 안전하게 주고 받기 위해 사용된다.
Token 은 header
, payload
, signature
로 구분되어 표현되는데
header
: 토큰의 타입 & 해싱 알고리즘 정보,
payload
: 토큰에 담을 정보 ( claim ),
signature
: 비밀키
가 포함된다.
Servlet 도 이해가 안가는데 음.. 이건 공부가 더 필요하다.