Spring - Lv.2 과제

김지현·2023년 4월 20일
0

항해99 과제

목록 보기
3/4

2023-04-18 ~ 2023-04-23


요구사항

  1. 회원가입 API
    • username ,password 전달 받은 후 DB에 중복된 username이 없다면 회원 저장
    • username : 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)
    • password : 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)
  2. 로그인 API
    • username, password 전달 받은 후 password 비교
    • 발급한 토큰을 Header에 추가하여 로그인
  3. 전체 게시글 목록 조회 API
    • 제목, 작성자명(username), 작성내용, 작성날짜 조회
    • 작성날짜 기준 내림차순 정리
  4. 게시글 작성 API
    • 토큰 검사하여 유효한 토큰일 경우에만 작성 가능
    • 제목, 작성내용 저장
  5. 선택한 게시글 조회 API
    • 선택한 게시글의 제목, 작성자명(username), 작성날짜, 작성내용 조회
  6. 선택한 게시글 수정 API
    • 토큰 검사하여 유효한 토큰일 경우에만 수정 가능
    • 제목, 작성내용 수정
  7. 선택한 게시글 삭제 API
    • 토큰 검사하여 유효한 토큰일 경우에만 삭제 가능

API 명세서

■ 회원가입

  • Method : POST
  • URL : /api/user/signup
  • Request
{
    "username":"asdf12",
    "password":"asdf1234"
}
  • Response
{
    "message": "회원가입 성공!",
    "data": null
}

■ 로그인

  • Method : POST
  • URL : /api/user/login
  • Request
{
    "username":"asdf12",
    "password":"asdf1234"
}
  • Response
{
    "message": "로그인 성공!",
    "data": null
}

폴더 구조


전체적인 패키지 및 파일

■ controller

▲ PostController

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);
    }

}

▲ UserController

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);
    }

}

■ dto

▲ RequestDto

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;

}

▲ ResponseDto

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);
    }

}

■ entity

▲ Post

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();
    }

}

▲ Timestamped

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;

}

▲ Users

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;
    }

}

■ jwt

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();
    }

}

■ repository

▲ PostRepository

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();

}

▲ UserRepository

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);

}

■ service

▲ PostService

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 + "번 게시물을 삭제할 권한이 없습니다.");
        }
    }

}

▲ UserService

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);
    }

}

역경과 고난

  1. JWT가 너무 이해 안가서 3,4일 정도 다음 레벨 과제를 진행하지 않고 이해하는 시간을 가졌다.
    JWT ( Json Web Token ) 은 말그대로 웹에서 사용되는 JSON 형식의 토큰에 대한 표준 규격인데 주로 클라이언트의 인증 또는 인가 정보를 서버와 클라이언트 간에 안전하게 주고 받기 위해 사용된다.
    Token 은 header , payload , signature 로 구분되어 표현되는데
    header : 토큰의 타입 & 해싱 알고리즘 정보,
    payload : 토큰에 담을 정보 ( claim ),
    signature : 비밀키
    가 포함된다.

  2. Servlet 도 이해가 안가는데 음.. 이건 공부가 더 필요하다.

0개의 댓글

관련 채용 정보