코이_CO2
LIVING IS DYING
코이_CO2
전체 방문자
오늘
어제
  • 분류 전체보기 (45)
    • TIL ⚓️ (4)
      • OT주차 (1)
      • 1주차_풀스택 미니 프로젝트 (0)
      • 1주차_언어 기초(Java) (0)
      • 2주차_프로그래밍 기초 (1)
      • 3주차_주특기 입문(Spring) (0)
      • 4주차_주특기 숙련(Spring) (2)
    • WIL ⚓️ (0)
      • OT주차 (0)
      • 1주차_언어 기초(Java) (0)
      • 2주차_프로그래밍 기초 (0)
      • 3주차_주특기 입문(Spring) (0)
      • 4주차_주특기 숙련(Spring) (0)
    • Java의 정석 📖 (4)
      • Chapter 1. 자바를 시작하기 전에 (3)
      • Chapter 2. 변수 (0)
      • Chapter 3. 연산자 (0)
      • Chapter 4. 조건문과 반복문 (1)
    • Programmers (7)
      • Lv. 1 (7)
    • 혼자 공부하는 자바 📖 (8)
      • Chapter 05 참조 타입 (0)
      • Chapter 06 클래스 (3)
      • Chapter 07 상속 (1)
      • Chapter 08 인터페이스 (1)
      • Chapter 09 중첩 클래스 & 인터페이스 (0)
      • Chapter 10 예외 처리 (1)
      • Chapter 12 스레드 (1)
    • Java (2)
    • Spring (1)
    • Python (2)
    • Mysql (4)
    • Machine Learning (6)
      • 추측 통계 (2)
    • Data Analysis (1)

블로그 메뉴

  • 홈
  • 태그
  • 방명록

공지사항

인기 글

태그

  • java
  • 혼자 공부하는 자바
  • 부트스트랩
  • 주특기 심화주차
  • 혼자공부하는자바
  • PYTHON
  • 하루기록
  • spring
  • 웹개발 종합반
  • programmers
  • DTO
  • 배열
  • 개발일지
  • 코딩
  • LV1
  • 스터디
  • jwt
  • 자바
  • CRUD
  • 파이썬
  • TIL/WIL
  • 게시판 프로젝트
  • HTML
  • Spring Security
  • TIL
  • sql
  • 항해99
  • 스프링
  • 개발자
  • 프로그래머스

최근 댓글

최근 글

티스토리

hELLO · Designed By 정상우.
코이_CO2

LIVING IS DYING

TIL / 항해99 12기 37일차_230214_Tue
TIL ⚓️/4주차_주특기 숙련(Spring)

TIL / 항해99 12기 37일차_230214_Tue

2023. 2. 15. 23:33

Spring 숙련 주차 Lv2 완료

게시글 CRUD + jwt

  • 회원가입 & 로그인
  • 게시글 작성
  • 게시글 전체 목록 조회
  • 게시글 선택 조회
  • 게시글 수정
  • 게시글 삭제 

환경설정

Gradle / JDK 17
Spring Boot 2.7.8

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
    'TIL ⚓️/4주차_주특기 숙련(Spring)' 카테고리의 다른 글
    • TIL / 항해99 12기 36일차_230213_Mon
    코이_CO2
    코이_CO2
    나에게 찾아오는 뻔한 매일을 언제나 값지게 여길 줄 아는 내가 되기를

    티스토리툴바