최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)
로 구성되어야 한다.최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)
로 구성되어야 한다.package com.sparta.springboard.controller;
import com.sparta.springboard.dto.BoardRequestDto;
import com.sparta.springboard.dto.BoardResponseDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.service.BoardService;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import java.util.List;
//Controller 클래스(여기서는 BoardController)의 각 하위 메서드에 @ResponseBody 를 붙이지 않아도(@Controller 와 달리), 문자열과 JSON 등을 전송 가능
//RestController 의 주용도는 Json 형태로 객체 데이터를 반환하는 것
//@Controller + @ResponseBody 를 결합한 어노테이션
@RestController
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
//url 에서 공통되는 부분
@RequestMapping("/api")
public class BoardController {
//HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
private final BoardService boardService;
//게시글 작성
@PostMapping("/post")
//BoardResponseDto 반환 타입, createBoard 메소드 명
//@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
//HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public BoardResponseDto createBoard(@RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
//requestDto, request 에 데이터를 담아서, boardService 로 응답을 보냄
return boardService.createBoard(requestDto, request);
}
//전체 게시글 목록 조회
@GetMapping("/posts")
//BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
public List<BoardResponseDto> getListBoards() {
//() 모든 데이터를 담아서, boardService 로 응답을 보냄
return boardService.getListBoards();
}
//선택한 게시글 조회
@GetMapping("/post/{id}")
//BoardResponseDto 반환 타입, getBoards 메소드 명
//@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터 --> 전체 게시글 목록에서 id 값으로 각각의 게시글을 구별
public BoardResponseDto getBoards(@PathVariable Long id) {
//id 값을 담아서, boardService 로 응답을 보냄
return boardService.getBoard(id);
}
//선택한 게시글 수정(변경)
@PutMapping("/post/{id}")
//BoardResponseDto 반환 타입, updateBoard 메소드 명
//@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터,
//@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
//HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public BoardResponseDto updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
//id 값, requestDto, request 에 데이터를 담아서, boardService 로 응답을 보냄
return boardService.updateBoard(id, requestDto, request);
}
//선택한 게시글 삭제
@DeleteMapping("/post/{id}")
//MsgResponseDto 반환 타입, deleteBoard 메소드 명
//@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public MsgResponseDto deleteBoard(@PathVariable Long id, HttpServletRequest request) {
//id 값, request 에 데이터를 담아서, boardService 로 응답을 보냄
return boardService.deleteBoard(id, request);
}
}
package com.sparta.springboard.controller;
import com.sparta.springboard.dto.LoginRequestDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.dto.SignupRequestDto;
import com.sparta.springboard.service.UserService;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
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.ResponseBody;
//Client 요청으로부터 view 를 반환. MVC 패턴의 Controller 클래스임을 명시
@Controller
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
//url 에서 공통되는 부분
@RequestMapping("/api/auth")
public class UserController {
//HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
private final UserService userService;
//회원가입 구현
@PostMapping("/signup")
//ResponseEntity: 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음 --> 구글링 필요, MsgResponseDto 의 데이터를 반환할 것임, signup 메소드 명
//@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), SignupRequestDto: 넘어오는 데이터를 받아주는 객체
//@Valid: Controller 에서 유효성 검사를 할 곳에 붙임
public ResponseEntity<MsgResponseDto> signup(@RequestBody @Valid SignupRequestDto signupRequestDto) {
//signupRequestDto 에 데이터를 담아서, userService 로 응답을 보냄
userService.signup(signupRequestDto);
//MsgResponseDto 에서 선언한 타입(여기서는 String message, int statusCode)으로 반환하는데,
//ResponseEntity.ok(): 상태코드를 반환
return ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()));
}
//로그인 구현
@ResponseBody
@PostMapping("/login")
//ResponseEntity: 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음 --> 구글링 필요, MsgResponseDto 의 데이터를 반환할 것임, login 메소드 명
//@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), LoginRequestDto: 넘어오는 데이터를 받아주는 객체
//HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public ResponseEntity<MsgResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
//loginRequestDto, response 에 데이터를 담아서, userService 로 응답을 보냄
userService.login(loginRequestDto, response);
//MsgResponseDto 에서 선언한 타입(여기서는 String message, int statusCode)으로 반환하는데,
//ResponseEntity.ok(): 상태코드를 반환
return ResponseEntity.ok(new MsgResponseDto("로그인 완료",HttpStatus.OK.value()));
}
}
package com.sparta.springboard.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Setter
//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
public class BoardRequestDto {
//필드
private Long id;
private String title;
private String contents;
private String username;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
private String password;
private String role;
}
package com.sparta.springboard.dto;
import com.sparta.springboard.entity.Board;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
public class BoardResponseDto {
//필드
private Long id;
private String title;
private String contents;
private String username;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
//생성자
public BoardResponseDto(Board board) { //매개변수를 가지는 생성자
this.id = board.getId(); //this.id: (위에서 선언된) 필드, Board 객체의 board 매개변수로 들어온 데이터를 getId() 에 담는다(Client 에게로 보내기 위해)
this.title = board.getTitle();
this.contents = board.getContents();
this.username = board.getUsername();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
}
package com.sparta.springboard.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class LoginRequestDto {
//필드
private String username;
private String password;
}
package com.sparta.springboard.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
//모든 필드값을 파라미터로 받는 생성자 생성
@AllArgsConstructor
//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
//해당 클래스에 자동으로 빌더를 추가
@Builder
public class MsgResponseDto {
//필드
private String msg; //BoardResponseDto 에 있던 필드와 생성자를 별도로 ResponseDto 를 만들어서 분리해줄 수도 있다!
private int statusCode; //기능에 따라서 반환하려는 데이터가 다른 경우에!
//@AllArgsConstructor 덕분에, 모든 필드값을 파라미터로 받는 생성자를 굳이 생성해주지 않아도 됨
// public MsgResponseDto(String msg, int statusCode) {
// this.msg = msg;
// this.statusCode = statusCode;
// }
}
package com.sparta.springboard.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
//@NoArgsConstructor ??
@AllArgsConstructor //추가?
@Builder //추가?
public class SignupRequestDto {
//필드
private String username;
private String password;
private boolean admin = false;
private String adminToken = "";
}
package com.sparta.springboard.entity;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.sparta.springboard.dto.BoardRequestDto;
import jakarta.persistence.*;
//import javax.persistence.*; ??
import lombok.Getter;
import lombok.NoArgsConstructor;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Entity
//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
public class Board extends Timestamped {
//PK(Primary Key: DB 테이블의 유일한 값)임을 지정해줌. 특정 속성을 기본키로 설정
//@Id 어노테이션만 적게될 경우 기본키값을 직접 부여해줘야 함 --> 그래서 @GeneratedValue 를 사용
@Id
//@GeneratedValue(strategy = GenerationType.IDENTITY) 또는 SEQUENCE) 또는 TABLE) 또는 AUTO)
//각각의 기능: 기본 키 생성을 DB에 위임 (Mysql), DB 시퀀스를 사용해서 기본 키 할당 (ORACLE), 키 생성 테이블 사용 (모든 DB 사용 가능), 선택된 DB에 따라 자동으로 전략 선택
//AUTO: DB에 따라 전략을 JPA 가 자동으로 선택되므로, DB를 변경해도 코드를 수정할 필요 없다는 장점
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
//객체 필드를 테이블 컬럼과 매핑 + 여러 속성 설정 가능
//nullable: null 허용 여부
@Column(nullable = false)
private String title;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String contents;
//필드 레벨에 적용되어 해당 필드를 Jackson 이 무시할 수 있도록 함(필드 하나하나에 붙임)
@JsonIgnore //왜 붙은걸까?
@Column
private String password;
@Column(nullable = false)
private Long userId;
//생성자
//게시글 작성
public Board(BoardRequestDto requestDto, Long userId) {
this.title = requestDto.getTitle(); //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
this.password = requestDto.getPassword();
this.userId = userId; //userId: 다른 값과 일치하는지를 비교해서 본인 인증을 위해 (연관관계를 짓기 위함)?
}
//선택한 게시글 수정(변경)
public void update(BoardRequestDto requestDto) { //boardrequestDto? requestDto?
this.title = requestDto.getTitle(); //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
this.password = requestDto.getPassword();
}
}
package com.sparta.springboard.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, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
//객체의 입장에서 공통 매핑 정보가 필요할 때 사용
//공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고, 속성만 상속 받아서 사용하고 싶을 때 --> Board 가 Timestamped 를 상속받고있다!
@MappedSuperclass
//Entity 가 삽입, 삭제, 수정, 조회 등...의 작업을 하기 전/후에 어떤 작업을 하기 위해 이벤트 처리를 위해서
//예시: 데이터 삽입 전 또는 수정 전에 createdAt 과 modifiedAt 의 시간을 조작
//@EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능(시간에 대해서 자동으로 값을 넣어주는 기능)을 포함
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
//createdAt, modifiedAt 컬럼 2개를 가진다
//생성일자
@CreatedDate
private LocalDateTime createdAt;
//마지막 수정일자
@LastModifiedDate
private LocalDateTime modifiedAt;
}
package com.sparta.springboard.entity;
import jakarta.persistence.*;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.NoArgsConstructor;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
//DB 테이블 역할을 함
//JPA 를 사용해 테이블과 매핑할 클래스에 붙임
//기본 생성자 필수
//name 속성: JPA 에서 사용할 엔티티 이름 지정. 따로 지정하지 않을 시 기본값으로 클래스 이름을 그대로 Entity 이름으로 지정
@Entity(name = "users")
public class User {
//필드
//PK(Primary Key: DB 테이블의 유일한 값)임을 지정해줌. 특정 속성을 기본키로 설정
//@Id 어노테이션만 적게될 경우 기본키값을 직접 부여해줘야 함 --> 그래서 @GeneratedValue 를 사용
@Id
//@GeneratedValue(strategy = GenerationType.IDENTITY) 또는 SEQUENCE) 또는 TABLE) 또는 AUTO)
//각각의 기능: 기본 키 생성을 DB에 위임 (Mysql), DB 시퀀스를 사용해서 기본 키 할당 (ORACLE), 키 생성 테이블 사용 (모든 DB 사용 가능), 선택된 DB에 따라 자동으로 전략 선택
//AUTO: DB에 따라 전략을 JPA 가 자동으로 선택되므로, DB를 변경해도 코드를 수정할 필요 없다는 장점
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
//객체 필드를 테이블 컬럼과 매핑 + 여러 속성 설정 가능
//nullable: null 허용 여부
//unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(nullable = false, unique = true)
//문자열, 배열등의 크기 설정
@Size(min = 4,max = 10,message ="아이디는 4에서 10자 사이 입니다.")
//정규식 설정
@Pattern(regexp = "[a-z0-9]*$",message = "아이디 형식이 일치하지 않습니다.")
private String username;
@Column(nullable = false)
@Size(min = 8,max = 15,message ="비밀번호는 8에서 15자 사이 입니다.")
@Pattern(regexp = "[a-zA-Z0-9`~!@#$%^&*()_=+|{};:,.<>/?]*$",message = "비밀번호 형식이 일치하지 않습니다.")
private String password;
@Column(nullable = false)
//Enum 의 선언된 상수의 이름을 String 클래스 타입으로 변환하여 DB에 넣는다.(즉, DB 클래스 타입은 String 이다.) --> UserRoleEnum 에서 열거혐(enum) 으로 필드가 선언되어있음
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
//생성자
//필드명 앞에 this. 를 붙임으로써
//1. 객체 자기 자신을 참조한다고 밝히고,
//2. 매개변수이름과 똑같았던 필드명을 구분지어줘서 필드에 접근 가능하도록 만듦(원래는 매개변수의 우선순위가 더 높았기때문에)
public User(String username, String password, UserRoleEnum role) {
this.username = username; //this.username: (위에서 선언된) 필드, username: 매개변수
this.password = password;
this.role = role;
}
}
package com.sparta.springboard.entity;
//주의! 열거혐(enum)으로 만들어야 함
public enum UserRoleEnum {
USER, // 사용자 권한
ADMIN // 관리자 권한
}
package com.sparta.springboard.jwt;
import com.sparta.springboard.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
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: 로깅 추상화 라이브러리. 변수명이 log 로 고정된다
//log 가 왜 필요한가? 로그를 작성해두면, 어떤 동작 중인지, 어느 부분에 에러가 났는지 파악 가능
@Slf4j
//Bean Configuration 파일에 Bean 을 따로 등록하지 않아도 사용 가능해짐
//빈 등록을 위해
@Component
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
public class JwtUtil { //빈이 등록됐다는 '나뭇잎 모양' 확인 가능
//필드
//토큰 생성에 필요한 값
//Authorization: Bearer <JWT> 에서 Header 에 들어가는 KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY: 사용자 권한도 Token 안에 넣는데, 이를 가져올 때 사용하는 KEY 값
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자: Token 을 만들 때, 앞에 들어가는 부분
private static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간: 밀리세컨드 기준. 60 * 1000L는 1분. 60 * 60 * 1000L는 1시간
private static final long TOKEN_TIME = 60 * 60 * 1000L;
//@Value("${프로퍼티 키값}") : application.properties 에 정의한 내용을 가져와서 사용 가능
//수정과 관리가 용이하기 때문에 이렇게 사용
@Value("${jwt.secret.key}")
private String secretKey; //@Value() 안에 application.properties 에 넣어둔 KEY 값(jwt.secret.key=7ZWt7ZW0O...pA=)을 넣으면, 가져올 수 있음
private Key key; //Key 객체: Token 을 만들 때 넣어줄 KEY 값
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//메소드
//종속성 주입이 완료된 후 실행되어야 하는 메서드에 사용
//처음에 객체가 생성될 때, 초기화하는 함수
//사용 이유?
//1. 생성자가 호출되었을 때, 빈은 초기화되지 않았음(의존성 주입이 이루어지지 않았음)
//2. 어플리케이션이 실행될 때 bean 이 오직 한 번만 수행되게 해서 bean 이 여러 번 초기화되는 걸 방지
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey); //Base64로 인코딩되어 있는 것을, 값을 가져와서(getDecoder()) 디코드하고(decode(secretKey)), byte 배열로 반환
key = Keys.hmacShaKeyFor(bytes); //반환된 bytes 를 hmacShaKeyFor() 메서드를 사용해서 Key 객체에 넣기
}
//Header 에서 Token 가져오기
public String resolveToken(HttpServletRequest request) {
//HttpServletRequest request 객체 안에 들어간 Token 값을 가져옴
//() 안에는 어떤 KEY 를 가져올지 정할 수 있음(여기선 AUTHORIZATION_HEADER 안에 있는 KEY 의 값을 가져옴)
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
//그리고, 가져온 코드가 있는지, BEARER_PREFIX 로 시작하는지 확인
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
//substring() 메소드를 사용해서, 앞의 일곱 글자를 지워줌 --> 앞의 일곱 글자는 Token 과 관련 없는 String 값이므로
return bearerToken.substring(7);
}
return null;
}
//JWT 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX + //Token 앞에 들어가는 부분임
//실제로 만들어지는 부분
Jwts.builder()
//setSubject 라는 공간 안에 username 를 넣는다
.setSubject(username)
//claim 이라는 공간 안에 AUTHORIZATION_KEY 사용자의 권한을 넣고(이 권한을 가져올 때는 지정해둔 auth KEY 값을 사용해서 넣음)
.claim(AUTHORIZATION_KEY, role)
//이 Token 을 언제까지 유효하게 가져갈건지. date: 위의 Date date = new Date() 에서 가져온 부분
//getTime(): 현재 시간
//+ TOKEN_TIME: 우리가 지정해 둔 시간(TOKEN_TIME = 60 * 60 * 1000L)을 더한다 = 즉, 지금 기준으로 언제까지 가능한지 결정해줌
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
//Token 이 언제 만들어졌는지 넣는 부분
.setIssuedAt(date)
//secret key 를 사용해서 만든 key 객체와
//어떤 암호화 알고리즘을 사용해서 암호화할것인지 지정(SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256)
.signWith(key, signatureAlgorithm)
//반환
.compact();
}
//JWT 검증
public boolean validateToken(String token) {
try {
//Jwts: 위에서 JWT 생성 시 사용했던 것
//parserBuilder(): 검증 방법
//setSigningKey(key): Token 을 만들 때 사용한 KEY
//parseClaimsJws(token): 어떤 Token 을 검증할 것인지
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;
}
//JWT 에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기 --> 위에서 validateToken() 으로 토큰을 검증했기에 이 코드를 사용할 수 있는 것
public Claims getUserInfoFromToken(String token) {
//getBody(): Token?? 안에 들어있는 정보를 가져옴
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}
package com.sparta.springboard.repository;
import com.sparta.springboard.entity.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
import java.util.Optional;
//게시글 작성
//<연결할 엔티티(테이블) 클래스 이름, ID 필드 타입> --> Board 에서 id 필드의 타입이 Long 이기 때문
public interface BoardRepository extends JpaRepository<Board, Long> {
//전체 게시글 목록 조회
List<Board> findAllByOrderByModifiedAtDesc();
//게시글 수정, 삭제
//Optional: null 을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해
//--> 수정, 삭제에서 id 와 userid 를 비교해서 일치하지 않으면, 예외 처리를 하기때문에 여기선 적절한 사용법으로 보임
//findByIdAndUserId(Long id, Long userId): Board 의 id 와 userId 가 일치하는 Board 를 가져온다
//--> 그래야, 로그인한 해당 사용자가 작성한 게시글만 수정, 삭제되기 때문(본인 인증)
Optional<Board> findByIdAndUserId(Long id, Long userId);
}
package com.sparta.springboard.repository;
import com.sparta.springboard.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
//<연결할 엔티티(테이블) 클래스 이름, ID 필드 타입> --> User 에서 id 필드의 타입이 Long 이기 때문
public interface UserRepository extends JpaRepository<User, Long> {
// 회원 중복 확인
//Optional: null 을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해
//--> 작성, 수정, 삭제에서 해당 Username 이 DB 에 있는지 확인하고, 없으면 예외 처리를 하기때문에 여기선 적절한 사용법으로 보임
Optional<User> findByUsername(String username);
}
Entity를 DTO 로 변환해서 반환(return)
--> DTO 로 변환하는 곳은 service 외에도 선택 가능하나, 각자의 장단점이 존재함.
package com.sparta.springboard.service;
import com.sparta.springboard.dto.BoardRequestDto;
import com.sparta.springboard.dto.BoardResponseDto;
import com.sparta.springboard.dto.MsgResponseDto;
import com.sparta.springboard.entity.Board;
import com.sparta.springboard.entity.User;
import com.sparta.springboard.jwt.JwtUtil;
import com.sparta.springboard.repository.BoardRepository;
import com.sparta.springboard.repository.UserRepository;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.List;
@Service
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
public class BoardService {
//의존성 주입
//외부(다른 패키지)에서 ...?
private final BoardRepository boardRepository;
private final UserRepository userRepository;
private final JwtUtil jwtUtil;
//게시글 작성
//클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션(데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위(더 이상 쪼개질 수 없는 최소의 연산))이 되도록 보장
//사용 이유?
//1. 해당 메서드를 실행하는 도중 메서드 값을 수정/삭제하려는 시도가 들어와도, 값의 신뢰성이 보장
//2. 연산 도중 오류가 발생해도, rollback 해서 DB에 해당 결과가 반영되지 않도록 함
//예시: 결제는 다른 사람과 독립적으로 이루어지며, 과정 중에 다른 연산이 끼어들 수 없다. 오류가 생긴 경우 연산을 취소하고 원래대로 되돌린다. 성공할 경우 결과를 반영한다
@Transactional //업데이트를 할 때, DB에 반영이 되는 것을 스프링에게 알려줌??
//BoardResponseDto 반환 타입, createBoard 메소드 명
//BoardRequestDto: 넘어오는 데이터를 받아주는 객체, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public BoardResponseDto createBoard(BoardRequestDto requestDto, HttpServletRequest request) {
//request 에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
//JWT 안에 있는 정보를 담는 Claims 객체
Claims claims;
//토큰이 있는 경우에만 추가 가능
if (token != null) {
//validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
if (jwtUtil.validateToken(token)) {
//true 라면, 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
//false 라면,
} else {
//매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
throw new IllegalArgumentException("Token Error");
}
//토큰에서 가져온 사용자 정보를 사용하여 DB 조회
//claims.getSubject(): 우리가 넣어두었던 username 가져오기
//findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
//요청받은 DTO 로 DB에 저장할 객체 Board board 만들기
Board board = boardRepository.saveAndFlush(new Board(requestDto, user.getId())); //saveAndFlush 맞나?
//entity(board) 를 DTO(BoardResponseDto) 로 변환시켜서 리턴(반환)
return new BoardResponseDto(board);
//토큰이 null 이라면(Client 에게서 Token 이 넘어오지 않은 경우),
} else {
//null 을 반환
return null; //trouble shooting: 중괄호 문제로 else return null; 에 빨간 글씨 --> 복붙할때 주의하자
}
}
//전체 게시글 목록 조회
//(readOnly = true): JPA 를 사용할 경우, 변경감지 작업을 수행하지 않아 성능상의 이점
@Transactional(readOnly = true)
//BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
public List<BoardResponseDto> getListBoards() {
//boardRepository 와 연결해서, 모든 데이터들을 내림차순으로, List 타입으로 객체 Board 에 저장된 데이터들을 boardList 안에 담는다
List<Board> boardList = boardRepository.findAllByOrderByModifiedAtDesc(); //주의. boards 와 board
//boardResponseDto 를 새롭게 만든다 --> 텅 빈 상태 (빈 주머니 상태?)
List<BoardResponseDto> boardResponseDto = new ArrayList<>();
//반복문을 이용하여, boardList 에 담긴 데이터들을 객체 Board 로 모두 옮긴다
for (Board board : boardList) {
//board 를 새롭게 BoardResponseDto 로 옮겨담고, BoardResponseDto 를 boardResponseDto 안에 추가(add)한다
boardResponseDto.add(new BoardResponseDto(board));
}
//최종적으로 옮겨담아진 boardResponseDto 를 반환
return boardResponseDto;
}
//선택한 게시글 조회
@Transactional(readOnly = true)
//BoardResponseDto 반환 타입, getBoard 메소드 명, Long id: 담을 데이터
public BoardResponseDto getBoard(Long id) {
//Board: Entity 명, boardRepository 와 연결해서, id 를 찾는다
Board board = boardRepository.findById(id).orElseThrow(
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
);
//데이터가 들어간 객체 board 를 BoardResponseDto 로 반환
return new BoardResponseDto(board);
}
//선택한 게시글 수정(변경)
@Transactional
//BoardResponseDto 반환 타입, updateBoard 메소드 명
//Long id: 담을 데이터, BoardRequestDto: 넘어오는 데이터를 받아주는 객체, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto, HttpServletRequest request) {
//request 에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
//JWT 안에 있는 정보를 담는 Claims 객체
Claims claims;
//토큰이 있는 경우에만 추가 가능
if (token != null) {
//validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
if (jwtUtil.validateToken(token)) {
//true 라면, 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
//false 라면,
} else {
//매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
//claims.getSubject(): 우리가 넣어두었던 username 가져오기
//findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
//내가 가지고 온 boardid 이면서, 그 board 가 동일한 userid 를 가지고 있는지까지 확인
//즉, 현재 로그인한 user 가 선택한 board 가 맞는지 확인
Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow( //추가: 실수 //id?? id, user.getId()??
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
);
//동일하다면, update() 메서드 사용
board.update(requestDto);
//entity(board) 를 DTO(BoardResponseDto) 로 변환시켜서 리턴(반환)
return new BoardResponseDto(board); //추가: 실수
} else {
return null;
}
}
//선택한 게시글 삭제
@Transactional
//MsgResponseDto 반환 타입, deleteBoard 메소드 명
//Long id: 담을 데이터, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
public MsgResponseDto deleteBoard (Long id, HttpServletRequest request) {
//request 에서 Token 가져오기
String token = jwtUtil.resolveToken(request);
//JWT 안에 있는 정보를 담는 Claims 객체
Claims claims;
//토큰이 있는 경우에만 추가 가능
if (token != null) {
//validateToken()를 사용해서, 들어온 토큰이 위조/변조, 만료가 되지 않았는지 검증
if (jwtUtil.validateToken(token)) {
//true 라면, 토큰에서 사용자 정보 가져오기
claims = jwtUtil.getUserInfoFromToken(token);
//false 라면,
} else {
//매개변수가 의도치 않는 상황 유발시, 해당 메시지 반환
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 가져온 사용자 정보를 사용하여 DB 조회
//claims.getSubject(): 우리가 넣어두었던 username 가져오기
//findByUsername()를 사용해서, UserRepository 에서 user 정보를 가져오기
User user = userRepository.findByUsername(claims.getSubject()).orElseThrow(
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("사용자가 존재하지 않습니다.")
);
//내가 가지고 온 boardid 이면서, 그 board 가 동일한 userid 를 가지고 있는지까지 확인
//즉, 현재 로그인한 user 가 선택한 board 가 맞는지 확인
Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow( //추가: 실수 findById(id) --> findById(id, user.getId()) --> findByIdAndUserId(id, user.getId())
//매개변수가 의도치 않는 상황 유발시
() -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
);
//동일하다면, delete() 메서드 사용
boardRepository.delete(board);
return new MsgResponseDto("게시글 삭제 성공", HttpStatus.OK.value());
//이렇게 new 연산자로 생성해주면, 위에서는 따로 DelResponseDto result = new DelResponseDto(); 이런거 안 만들어줘도 됨
//return new BoardResponseDto("게시글 삭제 성공", HttpStatus.OK.value());
} else {
return new MsgResponseDto("게시글 작성자만 삭제 가능", HttpStatus.OK.value());
//return new BoardResponseDto("게시글 작성자만 삭제 가능", HttpStatus.OK.value());
}
}
}
package com.sparta.springboard.service;
import com.sparta.springboard.dto.LoginRequestDto;
import com.sparta.springboard.dto.SignupRequestDto;
import com.sparta.springboard.entity.User;
import com.sparta.springboard.entity.UserRoleEnum;
import com.sparta.springboard.jwt.JwtUtil;
import com.sparta.springboard.repository.UserRepository;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import java.util.Optional;
@Service
//다른 계층에서 파라미터를 검증하기 위해서
@Validated //추가
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
public class UserService {
//회원가입 구현
//의존성 주입(DI)
private final UserRepository userRepository;
private final JwtUtil jwtUtil; //의존성 주입(DI) --> jwtUtil.class 에서 @Component 로 빈을 등록했기때문에 가능
private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";
@Transactional
//SignupRequestDto 에서 가져온 username, password 를 확인
public void signup(@Valid SignupRequestDto signupRequestDto) { //@Valid 추가 주의!
String username = signupRequestDto.getUsername();
String password = signupRequestDto.getPassword();
// 회원 중복 확인
//userRepository 에서 username 으로 실제 유저가 있는지 없는지 확인
Optional<User> found = userRepository.findByUsername(username); //userRepository 에 구현하기
//중복된 유저가 존재한다면,
if (found.isPresent()) {
//해당 메시지 보내기
throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
}
// 사용자 ROLE(권한) 확인
UserRoleEnum role = UserRoleEnum.USER;
//SignupRequestDto 에서 admin 이 true 라면(admin 으로 로그인을 시도하려고 하는구나),
if (signupRequestDto.isAdmin()) {
//들어온 Token 값과 위의 검증을 위한 Token 값(위쪽에 AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC)이 일치하는지 확인
if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
//일치하지 않으면(매개변수가 의도치 않는 상황 유발시), 해당 메시지를 보낸다
throw new IllegalArgumentException("관리자 암호가 틀려 등록이 불가능합니다.");
}
//일치하면, user 를 admin 타입으로 바꾼다
role = UserRoleEnum.ADMIN;
}
//가져온 username, password, role(UserRoleEnum)를 넣어서 저장(save)
User user = new User(username, password, role);
userRepository.save(user);
}
//로그인 구현
@Transactional(readOnly = true)
//로그인이 되면, LoginRequestDto 로 username, password 가 넘어온다
public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
String username = loginRequestDto.getUsername();
String password = loginRequestDto.getPassword();
// 사용자 확인
//username 을 통해 확인해서, 있다면 User 객체에 담긴다
User user = userRepository.findByUsername(username).orElseThrow(
//없다면(매개변수가 의도치 않는 상황 유발시), 해당 메시지 보내기
() -> new IllegalArgumentException("등록된 사용자가 없습니다.")
);
// 비밀번호 확인
//User 객체에 들어있던 Password 와 가지고 온 Password(String password = loginRequestDto.getPassword() 에 있는) 가 일치하는지 확인
//일치하지 않는다면, 해당 메시지 보내기
if(!user.getPassword().equals(password)){
throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
}
//response 에 addHeader() 를 사용해서, Header 쪽에 값을 넣는데
//AUTHORIZATION_HEADER: KEY 값
//createToken(user.getUsername(), user.getRole()): Username(이름), Role(권한)을 넣어서 토큰을 만든다
//위의 User 객체에서 유저 정보를 가져왔기 떄문에 사용 가능한 것
//jwtUtil 를 사용하기 위해, 위에서 의존성 주입을 해줘야 함
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));
}
}
package com.sparta.springboard;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing //JPA Auditing 을 활성화
@SpringBootApplication
public class SpringboardApplication {
public static void main(String[] args) {
SpringApplication.run(SpringboardApplication.class, args);
}
}
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=
plugins {
id 'java'
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
}
group = 'com.sparta'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '17'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
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'
implementation 'org.jetbrains:annotations:20.1.0'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
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'
implementation 'org.springframework.boot:spring-boot-starter-validation' //의존성을 위해 추가
}
tasks.named('test') {
useJUnitPlatform()
}