package com.sparta.springboards.config;
import com.sparta.springboards.jwt.JwtAuthFilter;
import com.sparta.springboards.jwt.JwtUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
@Configuration
@RequiredArgsConstructor
@EnableWebSecurity // 스프링 Security 지원을 가능하게 함
@EnableGlobalMethodSecurity(securedEnabled = true) // @Secured 어노테이션 활성화
public class WebSecurityConfig {
private final JwtUtil jwtUtil;
@Bean
//Spring Security 가 제공하는 적응형 단방향 함수인 bCrypt 를 사용하여 비밀번호를 암호화(Encoder) --> 적응형 단방향이 자동 적용
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
//WebSecurityCustomizer 은 SecurityFilterChain 보다 우선적으로 걸리는 설정
@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return (web) -> web.ignoring() //ignoring(): 이러한 경로도 들어온 것들은 인증 처리하는 것을 무시하겠다
.requestMatchers(PathRequest.toH2Console()) //h2-console 사용 및 resources 접근 허용 설정
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
// CSRF 설정
http.csrf().disable();
// 기본 설정인 Session 방식은 사용하지 않고 JWT 방식을 사용하기 위한 설정 → form login이 안되게 함
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
//permitAll() 를 사용해서, 이런 URL 들을 인증하지 않고 실행 할 수 있게 함
http.authorizeRequests().antMatchers("/api/auth/**").permitAll()
//그 이외의 URL 요청들을 전부 다 authentication(인증 처리)하겠다
.anyRequest().authenticated()
// JWT 인증/인가를 사용하기 위한 설정
// Custom Filter 등록하기
//addFilterBefore: 어떤 Filter 이전에 추가하겠다 --> 우리가 만든 JwtAuthFilter 를 UsernamePasswordAuthenticationFilter 이전에 실행할 수 있도록
//1. JwtAuthFilter 를 통해 인증 객체를 만들고 --> 2. context 에 추가 --> 3.인증 완료 --> UsernamePasswordAuthenticationFilter 수행 --> 인증됐으므로 다음 Filter 로 이동 --> Controller 까지도 이동
.and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);
//"거부"가 났을 때, 403 Forbidden 페이지(접근 제한 페이지) 이동 설정
http.exceptionHandling().accessDeniedPage("/api/user/forbidden");
return http.build();
}
}
package com.sparta.springboards.controller;
import com.sparta.springboards.dto.BoardRequestDto;
import com.sparta.springboards.dto.BoardResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.BoardService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import java.util.List;
//Controller 클래스(여기서는 BoardController)의 각 하위 메서드에 @ResponseBody 를 붙이지 않아도(@Controller 와 달리), 문자열과 JSON 등을 전송 가능
//RestController 의 주용도는 Json 형태로 객체 데이터를 반환하는 것
//@Controller + @ResponseBody 를 결합한 어노테이션
@RestController
@RequiredArgsConstructor //final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequestMapping("/api")
public class BoardController {
//HTTP request 를 받아서, Service 쪽으로 넘겨주고, 가져온 데이터들을 requestDto 파라미터로 보냄
//boardService 를 가져다 쓰겠다 (의존성 주입)
private final BoardService boardService;
//Client 기준에서 보내는, 받는 정보 라는 뜻 = DB 기준에서 받는, 보내는 정보
//게시글 작성
@PostMapping("/board")
//미리 말해두기: BoardResponseDto 타입으로 반환 받을 건데, createBoard() 메소드를 사용해서, 그 안에는 BoardRequestDto 객체의 requestDto 정보, UserDetailsImpl 객체의 userDetails 정보가 담겨져있는 상태로 받을꺼다!
//@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌)
//BoardRequestDto: 요청을 받을 때는 Request Body 부분에서 오는 데이터를 DTO 를 만들어서 받는다.(변환이 필요)
//이유? client 와 controller 는 DTO 로 데이터를 교환하기 때문
//@AuthenticationPrincipal: 인증 객체(Authentication)의 Principal 부분의 값을 가져온다
//UserDetailsImpl userDetails: 인증 객체를 만들 때, Principal 부분에 userDetails 를 넣었기 때문에, userDetails 를 파라미터로 받아올 수 있었음
public ResponseEntity<BoardResponseDto> createBoard(@RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘: boardService 에서 createBoard() 메소드를 사용해서, 그 안에는 requestDto, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
//return boardService. 를 쓸 수 있는 이유? private final BoardService boardService; 로 의존성 주입을 해줘서, boardService 와 연결해둔 상태이기 때문!
return ResponseEntity.ok().body(boardService.createBoard(requestDto, userDetails.getUser()));
}
//전체 게시글 목록 조회
@GetMapping("/boards")
//미리 말해두기: BoardResponseDto 타입의 데이터를 List 형식으로 반환 받을 건데, getListBoards() 메소드를 사용해서, 그 안에는 UserDetailsImpl 객체의 userDetails 정보, category 정보가 담겨져있는 상태로 받을꺼다!
//@RequestParam: 요청 매개변수에 들어있는 기본 타입 데이터를 메서드 인자로 받아올 수 있다.
//요청 매개변수: URL 요청의 localhost:8080?username=value&age=value 의 쿼리 매개변수와 HTML 태그의 Form 데이터가 해당
//되도록 String 타입 활용
public ResponseEntity<List<BoardResponseDto>> getListBoards(@AuthenticationPrincipal UserDetailsImpl userDetails, @RequestParam String category) {
//다시 돌려줘: boardService 에서 getListBoards() 메소드를 사용해서, 그 안에는 userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
//해당 메소드의 모든 데이터를 반환할 때는 () 괄호를 비워둔다?
return ResponseEntity.ok().body(boardService.getListBoards(userDetails.getUser(),category));
}
//선택한 게시글 조회
@GetMapping("/board/{id}")
//미리 말해두기
//@PathVariable: URL 경로에 변수를 넣기
//예시: localhost:8080/api/board/{12}
public ResponseEntity<BoardResponseDto> getBoards(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘
return ResponseEntity.ok().body(boardService.getBoard(id, userDetails.getUser()));
}
//선택한 게시글 수정(변경)
@PutMapping("/board/{id}")
//미리 말해두기
public ResponseEntity<BoardResponseDto> updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘
//requestDto 넣는 이유? Client 가 수정한 내용(= 요청한 내용)을 받기 위해
return ResponseEntity.ok().body(boardService.updateBoard(id, requestDto, userDetails.getUser()));
}
//선택한 게시글 삭제
@DeleteMapping("/board/{id}")
//미리 말해두기
public ResponseEntity<MsgResponseDto> deleteBoard(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘
boardService.deleteBoard(id,userDetails.getUser());
//다시 돌려줘
//return new 를 사용해서 MsgResponseDto() 메소드를 호출하는 이유?
//HttpStatus.OK.value(): 상태 코드 를 담아서 보낸다.
return ResponseEntity.ok().body(new MsgResponseDto("삭제 성공", HttpStatus.OK.value()));
}
//게시글 좋아요
@PostMapping("/board/like/{boardId}")
//미리 말해두기: MsgResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, saveBoardLike() 메소드를 사용해서, 그 안에는 boardId 정보, userDetails 정보가 담겨져있는 상태로 받을꺼다!
//ResponseEntity: HttpEntity 클래스를 상속받아 구현한 클래스. HttpStatus, HttpHeaders, HttpBody 를 포함. 결과값, 상태코드, 헤더값을 모두 프론트에 넘겨줄 수 있고, 에러코드 또한 섬세하게 설정해서 보내줄 수 있음
public ResponseEntity<MsgResponseDto> saveBoardLike(
@PathVariable Long boardId,
@AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, boardService 에서 saveBoardLike() 메소드를 사용해서, 그 안에는 boardId, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
//ResponseEntity.ok(): 상태코드를 반환
return ResponseEntity.ok().body(boardService.saveBoardLike(boardId, userDetails.getUser()));
}
}
package com.sparta.springboards.controller;
import com.sparta.springboards.dto.CommentRequestDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.CommentService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/comment")
public class CommentController {
//의존성 주입
//CommentService 와 연결
private final CommentService commentService;
//댓글 작성
@PostMapping("/{id}")
//미리 말해두기: CommentResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, createComment() 메소드를 사용해서, 그 안에는 id 정보, commentRequestDto 정보, userDetails 가 담겨져있는 상태로 받을꺼다!
public ResponseEntity<CommentResponseDto> createComment(@PathVariable Long id, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
//다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, commentService 에서 createComment() 메소드를 사용해서, 그 안에는 id, commentRequestDto, userDetails.getUser() 를 담아서, DB 로부터 return(반환) 받는다.
return ResponseEntity.ok().body(commentService.createComment(id, commentRequestDto, userDetails.getUser()));
}
//댓글 수정(변경)
@PutMapping("/{boardId}/{cmtId}")
public ResponseEntity<CommentResponseDto> updateComment(@PathVariable Long boardId, @PathVariable Long cmtId, @RequestBody CommentRequestDto commentRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseEntity.ok().body(commentService.updateComment(boardId, cmtId, commentRequestDto, userDetails.getUser()));
}
//댓글 삭제
@DeleteMapping("/{boardId}/{cmtId}")
public ResponseEntity<MsgResponseDto> deleteComment(@PathVariable Long boardId, @PathVariable Long cmtId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
commentService.deleteComment(boardId, cmtId, userDetails.getUser());
return ResponseEntity.ok(new MsgResponseDto("삭제 성공", HttpStatus.OK.value()));
}
//댓글 좋아요
@PostMapping("/like/{cmtId}")
public ResponseEntity<MsgResponseDto> commentLike(@PathVariable Long cmtId, @AuthenticationPrincipal UserDetailsImpl userDetails) {
return ResponseEntity.ok(commentService.CommentLike(cmtId, userDetails.getUser()));
}
}
package com.sparta.springboards.controller;
import com.sparta.springboards.dto.LoginRequestDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.dto.SignupRequestDto;
import com.sparta.springboards.security.UserDetailsImpl;
import com.sparta.springboards.service.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;
@RestController //Client 요청으로부터 view 를 반환. MVC 패턴의 Controller 클래스임을 명시
@RequiredArgsConstructor
@RequestMapping("/api/auth")
public class UserController {
//UserService 와 연결
private final UserService userService;
//회원가입 구현
@PostMapping("/signup")
//미리 말해두기: MsgResponseDto 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, signup() 메소드를 사용해서, 그 안에는 signupRequestDto 정보가 담겨져있는 상태로 받을꺼다!
//@Valid: Controller 에서 유효성 검사를 할 곳에 붙임
//유효성 검사: 데이터가 server 혹은 DB 로 옮겨지기 전, 개발자가 만든 조건에 부합하는지 확인, 검증하는 작업 (이미 가입된 ID 입니다. 비밀번호는 영문,숫자,특수문자가 혼합되어야 합니다 등...)
public ResponseEntity<MsgResponseDto> signup(@Valid @RequestBody SignupRequestDto signupRequestDto) {
//다시 돌려줘: userService 에서 signup() 메소드를 사용해서, 그 안에는 signupRequestDto 를 담아서, DB 로부터 return(반환) 받는다.
userService.signup(signupRequestDto);
//다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, MsgResponseDto 에서 (String msg, int statusCode)를 담아서, DB 로부터 return(반환) 받는다.
return ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()));
}
//로그인 구현
@PostMapping("/login")
//HttpServletRequest response : 누가 로그인 했는지 알기위한 토큰을 담고 있음 ?
public ResponseEntity<MsgResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
//다시 돌려줘: userService 에서 login() 메소드를 사용해서, 그 안에는 loginRequestDto 정보, response 정보를 담아서, DB 로부터 return(반환) 받는다.
userService.login(loginRequestDto, response);
//다시 돌려줘: ResponseEntity 형식으로 상태코드를 반환 하는데, MsgResponseDto 에서 (String msg, int statusCode)를 담아서, DB 로부터 return(반환) 받는다.
return ResponseEntity.ok(new MsgResponseDto("로그인 완료",HttpStatus.OK.value()));
}
//회원 탈퇴
@DeleteMapping("/signOut")
//미리 말해두기:
public ResponseEntity<MsgResponseDto> signOut(@RequestBody LoginRequestDto loginRequestDto, @AuthenticationPrincipal UserDetailsImpl userDetails){
userService.signOut(loginRequestDto,userDetails.getUser());
return ResponseEntity.ok(new MsgResponseDto("회원 탈퇴 완료", HttpStatus.OK.value()));
}
}
//@ResponseBody
//http 요청 body 를 자바 객체로 전달받을 수 있다. = String 값 자체를 반환하고 싶을때 쓰면된다.
//@Controller 의 경우, body 를 자바객체로 받기 위해서는 @ResponseBody 를 반드시 명시해주어야한다.
//BoardController 에서는 @RestController 를 썼기 때문에, @ResponseBody 를 따로 쓰지 않았음
package com.sparta.springboards.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
//접근자, 설정자: private 설정된 멤버변수에 대하여 외부 클래스에서 접근이 불가하기 때문에, 메소드 형태로 해당 클래스에 만들어 놓은것
@Getter //접근자: 매개변수는 X. 반환유형은 해당변수랑 같은 타입
@Setter //설정자: 반환유형은 X. 해당변수와 같은 타입의 데이터를 매개변수로 받아서 해당변수에 대입
@NoArgsConstructor //파라미터가 없는 기본생성자를 생성
//@AllArgsConstructor //해당 객체 내에 있는 모든 변수들을 인수로 받는 생성자를 만들어냄 --> 모든 필드값을 파라미터로 받는 생성자를 굳이 생성해주지 않아도 됨
public class BoardRequestDto {
//필드
private String title;
private String contents;
private String category;
}
package com.sparta.springboards.dto;
import com.sparta.springboards.entity.Board;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
@Getter
@NoArgsConstructor
public class BoardResponseDto {
//필드
private Long id;
private String title;
private String category;
private String contents;
private String username;
private int boardLikeCount;
private boolean boardLikeCheck;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
//생성자
public BoardResponseDto(Board board) { //매개변수를 가지는 생성자
//board 매개변수로 들어온 데이터를 getId(), getTitle()... 에 담아서, this.id, this.title... 필드에 저장 (Client 에게로 보내기 위해)
this.id = board.getId();
this.title = board.getTitle();
this.category = board.getCategory();
this.contents = board.getContents();
this.username = board.getUsername();
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
}
private List<CommentResponseDto> commentList = new ArrayList<>();
public BoardResponseDto(Board board, List<CommentResponseDto> commentList, boolean boardLikeCheck) {
//board, commentList, boardLikeCheck 매개변수로 들어온 데이터를 board 매개변수의 getId(), getTitle()... 에 담아서, this.id, this.title... 필드에 저장 (Client 에게로 보내기 위해)
this.id = board.getId();
this.title = board.getTitle();
this.category = board.getCategory();
this.contents = board.getContents();
this.username = board.getUsername();
this.boardLikeCount = board.getBoardLikeList().size(); //size(): Collection Object(ArrayList, Set etc) 에 사용
this.boardLikeCheck = boardLikeCheck;
this.createdAt = board.getCreatedAt();
this.modifiedAt = board.getModifiedAt();
this.commentList = commentList;
}
}
package com.sparta.springboards.dto;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
public class CommentRequestDto {
private String comment;
}
package com.sparta.springboards.dto;
import com.sparta.springboards.entity.Comment;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Getter
@AllArgsConstructor
@NoArgsConstructor
//댓글 표시
public class CommentResponseDto {
private Long id;
private String username;
private String comment;
private Integer CommentLike;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public CommentResponseDto (Comment comment, int cnt) {
this.id = comment.getId();
this.username = comment.getUsername();
this.comment = comment.getComment();
this.CommentLike = cnt;
this.createdAt = comment.getCreatedAt();
this.modifiedAt = comment.getModifiedAt();
}
}
package com.sparta.springboards.dto;
import lombok.Getter;
import lombok.Setter;
@Setter
@Getter
public class LoginRequestDto {
//필드
private String username;
private String password;
}
package com.sparta.springboards.dto;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@AllArgsConstructor
@NoArgsConstructor
@Builder //해당 클래스에 자동으로 빌더를 추가
public class MsgResponseDto {
//필드
private String msg; //BoardResponseDto 에 있던 필드와 생성자를 별도로 ResponseDto 를 만들어서 분리해줄 수도 있다!
private int statusCode; //어떤 경우에? 기능에 따라서 반환하려는 데이터가 다른 경우!
}
package com.sparta.springboards.dto;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter
@NoArgsConstructor
public class SecurityExceptionDto {
private int statusCode;
private String msg;
//Token 에 대한 오류가 발생했을 때, Client 에게 넘겨줄 Exception 한 결과값
public SecurityExceptionDto(int statusCode, String msg) {
this.statusCode = statusCode;
this.msg = msg;
}
}
package com.sparta.springboards.dto;
import lombok.Getter;
import lombok.Setter;
import javax.validation.constraints.Pattern;
import javax.validation.constraints.Size;
@Setter
@Getter
public class SignupRequestDto {
//필드
@Size(min = 4,max = 10,message ="아이디는 4에서 10자 사이 입니다.") //문자열, 배열등의 크기 설정
@Pattern(regexp = "[a-z0-9]*$",message = "아이디 형식이 일치하지 않습니다.") //정규식 설정 --> Dto 에 설정해주는 것이 좋다
private String username;
@Size(min = 8,max = 15,message ="비밀번호는 8에서 15자 사이 입니다.")
@Pattern(regexp = "[a-zA-Z0-9`~!@#$%^&*()_=+|{};:,.<>/?]*$",message = "비밀번호 형식이 일치하지 않습니다.")
private String password;
private boolean admin = false; //admin 인지 아닌지 확인
private String adminToken = "";
}
정규식은 entity 보단 dto에 적자.
package com.sparta.springboards.entity;
import com.sparta.springboards.dto.BoardRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@Setter
//JPA 에서 사용할 엔티티 이름 지정
//DB 테이블 역할을 함
//기본 생성자 필수
@Entity
@Table(name = "Board") //name 속성: JPA 에서 사용할 엔티티 이름 지정. 따로 지정하지 않을 시 기본값으로 클래스 이름을 그대로 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.IDENTITY)
private Long id;
//comment : board 관계 --> 다대일 양방향 관계
//cascade = CascadeType.REMOVE: 부모 엔티티를 삭제할 때, 연관된 자식 데이터에 대한 DELETE 쿼리를 실행 (자식 엔티티를 삭제하는 쿼리가 먼저 실행되고, 그 다음 부모 엔티티를 삭제하는 쿼리가 실행됨)
//즉, board 를 삭제할 때, comment 도 삭제한다. (comment 가 먼저 삭제되고, board 가 삭제됨)
//cascade: 영속성 전이
//영속성: 데이터를 생성한 프로그램이 종료되어도 사라지지 않는 데이터의 특성. 영속성을 갖지 않으면 데이터는 메모리에서만 존재. 프로그램이 종료되면 해당 데이터는 모두 사라짐.
//그래서 데이터를 파일이나 DB에 영구 저장함으로써, 데이터에 영속성을 부여.
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
//@OrderBy("필드명 [ASC | DESC] ") 형식
@OrderBy("createdAt DESC") //--> repository 에서 사용하자.
private List<Comment> comments = new ArrayList<>();
//boardLikeList : board 관계 --> 다대일 양방향 관계
@OneToMany(mappedBy = "board", cascade = CascadeType.REMOVE)
private List<BoardLike> boardLikeList = new ArrayList<>();
//column: DB 에서 가져온다
@Column(nullable = false) //nullable: null 허용 여부
private String title;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String contents;
@Column
private String category;
//boardList : user 관계 --> 다대일 관계
//FK = 연관관계의 주인(N 쪽이 주인)
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
//생성자
//게시글 작성
//requestDto, user 매개변수로 들어온 데이터를 requestDto, user 매개변수의 getId(), getUsername()... 에 담아서, this.title, this.username... 필드에 저장 (this 이므로, 여기 필드)
//Board: 생성자이므로, 클래스명과 동일함
public Board(BoardRequestDto requestDto, User user) {
this.title = requestDto.getTitle();
this.username = user.getUsername();
this.contents = requestDto.getContents();
this.category = requestDto.getCategory();
this.user = user;
}
//메소드
//선택한 게시글 수정(변경)
//update() 메소드를 사용해서, 그 안에는 boardrequestDto 매개변수로 들어온 데이터를 boardrequestDto 매개변수의 getTitle(), getContents() 에 담아서, this.title, this.contents 필드에 저장 (this 이므로, 여기 필드)
public void update(BoardRequestDto boardrequestDto) {
this.title = boardrequestDto.getTitle();
this.category = boardrequestDto.getCategory();
this.contents = boardrequestDto.getContents();
}
}
package com.sparta.springboards.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Getter
@Entity
@NoArgsConstructor
public class BoardLike {
//필드
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
//boardLikeList : board 관계 --> 다대일 양방향 관계
@ManyToOne
@JoinColumn(name = "BOARD_ID", nullable = false) //like : board ⇒ 다대일 필요한가?
private Board board; // Post 엔티티가 Like 엔티티를 List 로 가져야할 명분이 있는가? 게시글이 자신이 받을 좋아요의 갯수를 알아야 할 필요가 있을까?
// 필요하다면, 양방향 매핑
//boardLike : user 관계 --> 다대일 단방향 관계
//FK = 연관관계의 주인(N 쪽이 주인)
@ManyToOne
@JoinColumn(name = "USER_ID", nullable = false) //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.(여기선 user)
private User user;
//생성자
public BoardLike(Board board, User user) {
this.board = board;
this.user = user;
}
}
package com.sparta.springboards.entity;
import com.sparta.springboards.dto.CommentRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@Entity
@Getter
@NoArgsConstructor
public class Comment extends Timestamped {
//필드
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//comment : board 관계 --> 다대일 양방향 관계
//FK = 연관관계의 주인(N 쪽이 주인)
@ManyToOne
@JoinColumn(name = "board_id", nullable = false)
private Board board;
//commentList : user 관계 --> 다대일 양방향 관계
//FK = 연관관계의 주인(N 쪽이 주인)
@ManyToOne
@JoinColumn(name = "user_id", nullable = false)
private User user;
@Column(nullable = false)
private String username;
@Column(nullable = false)
private String comment;
//생성자
public Comment(CommentRequestDto commentRequestDto, Board board, User user) {
this.comment = commentRequestDto.getComment();
this.username = user.getUsername();
this.board = board;
this.user = user;
}
//메소드
public void update(CommentRequestDto commentRequestDto) {
this.comment = commentRequestDto.getComment();
}
}
package com.sparta.springboards.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
@AllArgsConstructor
@Getter
@Entity
@Builder
@NoArgsConstructor
public class CommentLike {
//필드
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long CommentLikeId;
//commentLike : comment 관계 --> 다대일 단방향 관계
@ManyToOne
@JoinColumn(name = "comment_id") //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.
private Comment comment;
//commentLike : user 관계 --> 다대일 단방향 관계
@ManyToOne
@JoinColumn(name = "comment_like_User_id") //테이블에서 name 속성을 따로 적어주지 않는 경우, name 은 해당 객체명이 된다.
private User user;
}
package com.sparta.springboards.entity;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import javax.persistence.Column;
import javax.persistence.EntityListeners;
import javax.persistence.MappedSuperclass;
import java.time.LocalDateTime;
@Getter
//객체의 입장에서 공통 매핑 정보가 필요할 때 사용
//공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고, 속성만 상속 받아서 사용하고 싶을 때 --> Board 가 Timestamped 를 상속받고있다!
@MappedSuperclass
//@EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능(시간에 대해서 자동으로 값을 넣어주는 기능)을 포함
//Entity 가 삽입, 삭제, 수정, 조회 등...의 작업을 하기 전/후에 어떤 작업을 하기 위해 이벤트 처리를 위해서
//예시: 데이터 삽입 전 또는 수정 전에 createdAt 과 modifiedAt 의 시간을 조작
@EntityListeners(AuditingEntityListener.class)
public class Timestamped {
//필드
//createdAt, modifiedAt 컬럼 2개를 가진다
//생성일자
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdAt;
//마지막 수정일자
@LastModifiedDate
@Column
private LocalDateTime modifiedAt;
}
package com.sparta.springboards.entity;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Getter
@NoArgsConstructor
@Entity(name = "users")
public class User {
//필드
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
//boardList : user 관계 --> 다대일 양방향 관계
@OneToMany(mappedBy = "user")
private List<Board> boardList = new ArrayList<>();
//commentList : user 관계 --> 다대일 양방향 관계
@OneToMany(mappedBy = "user")
private List<Comment> commentList = new ArrayList<>();
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false)
//Enum 의 선언된 상수의 이름을 String 클래스 타입으로 변환하여 DB에 넣는다.(즉, DB 클래스 타입은 String 이다.)
//UserRoleEnum 에서 열거혐(enum) 으로 필드가 선언되어있음을 확인 가능함!
@Enumerated(value = EnumType.STRING)
private UserRoleEnum role;
//생성자
public User(String username, String password, UserRoleEnum role) {
this.username = username;
this.password = password;
this.role = role;
}
}
package com.sparta.springboards.entity;
//주의! 열거혐(enum)으로 만들어야 함
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
UserRoleEnum(String authority) {
this.authority = authority;
}
public String getAuthority() {
return this.authority;
}
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
package com.sparta.springboards.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
//RuntimeException: JVM 의 정상 작동 中 발생할 수 있는 예외의 슈퍼클래스
//CustomException: 전역으로 사용된다.
//RuntimeException 을 상속받아서 Unchecked Exception 으로 활용
//RuntimeException 을 상속하지 않은 클래스는 Checked Exception
//RuntimeException 을 상속한 클래스는 Unchecked Exception 으로 분류
public class CustomException extends RuntimeException{
private final ErrorCode errorCode;
}
package com.sparta.springboards.exception;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;
@Getter
@AllArgsConstructor
public enum ErrorCode {
/* Custom Error Code */
// 토큰 관련 ErrorCode
INVALID_AUTH_TOKEN(HttpStatus.UNAUTHORIZED, "권한 정보가 없는 토큰입니다"),
INVALID_TOKEN(HttpStatus.BAD_REQUEST, "토큰이 유효하지 않습니다."),
// 유저 관련 ErrorCode
AUTHORIZATION(HttpStatus.BAD_REQUEST, "작성자만 수정/삭제할 수 있습니다."),
DUPLICATED_USERNAME(HttpStatus.BAD_REQUEST, "중복된 username 입니다"),
INVALID_FORMAT(HttpStatus.BAD_REQUEST, "username과 password의 형식이 올바르지 않습니다."),
NOT_MATCH_INFORMATION(HttpStatus.BAD_REQUEST, "회원정보가 일치하지 않습니다."),
NOT_FOUND_USER(HttpStatus.BAD_REQUEST, "회원을 찾을 수 없습니다."),
// 게시글 관련 ErrorCode
NOT_FOUND_BOARD(HttpStatus.BAD_REQUEST, "게시글을 찾을 수 없습니다."),
NOT_EXIST_CATEGORY(HttpStatus.BAD_REQUEST, "카테고리가 존재하지 않습니다."),
// 댓글 관련 ErrorCode
NOT_FOUND_COMMENT(HttpStatus.BAD_REQUEST, "댓글을 찾을 수 없습니다.");
private final HttpStatus httpStatus;
private final String message;
}
package com.sparta.springboards.exception;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import org.springframework.http.ResponseEntity;
import java.time.LocalDateTime;
@Getter
@Builder //모든 요소를 받는 package-private 생성자가 자동으로 생성
//@Builder 를 사용한 경우, @AllArgsConstructor 가 암묵적으로 적용된 상태임 (즉, 생성자를 굳이 만들어주지 않아도 되는 상태)
//그러나, 우리가 생성자를 직접 만들어주게 된 경우, @AllArgsConstructor 도 넣어줘야함 ?
@AllArgsConstructor
public class ErrorResponse {
//필드
private final LocalDateTime timestamp = LocalDateTime.now(); //로컬 컴퓨터의 현재 날짜와 시간 출력
private final int status;
private final String error;
private final String code;
private final String message;
//생성자
public ErrorResponse(ErrorCode errorCode) {
this.status = errorCode.getHttpStatus().value(); //.value() 쓰는 이유? 에러코드가 int 타입이다.
this.code = errorCode.name();
this.error = errorCode.getHttpStatus().name(); //.name() 쓰는 이유? 에러코드가 String 타입이다.
this.message = errorCode.getMessage();
}
//ErrorResponse 타입의 데이터를 ResponseEntity 형식으로 반환 받을 건데, toResponseEntity() 메소드를 사용해서, 그 안에는 errorCode 정보가 담겨져있는 상태로 받을꺼다!
public static ResponseEntity<ErrorResponse> toResponseEntity(ErrorCode errorCode) {
return ResponseEntity //builder 사용법?
.status(errorCode.getHttpStatus())
.body(ErrorResponse.builder()
.status(errorCode.getHttpStatus().value())
.error(errorCode.getHttpStatus().name())
.code(errorCode.name())
.message(errorCode.getMessage())
.build());
}
}
package com.sparta.springboards.exception;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.WebRequest;
import org.springframework.web.servlet.mvc.method.annotation.ResponseEntityExceptionHandler;
//로깅에 대한 추상 레이어를 제공하는 인터페이스의 모음
//장점? 추후에 필요로 의해 로깅 라이브러리를 변경할 때, 코드의 변경 없이 가능
//로깅: 로그(log, 정보를 제공하는 일련의 기록)를 생성하도록 시스템을 작성하는 활동
//log 필요 이유? 로그를 작성해두면, 어떤 동작 중인지, 어느 부분에 에러가 났는지 파악 가능
//추상 레이어 ?
@Slf4j
@RestControllerAdvice //전역적으로 에러를 처리
//ResponseEntityExceptionHandler: Spring 에서는 스프링 예외를 미리 처리해둔 추상 클래스를 제공해줌. 이 추상 클래스가 ResponseEntityExceptionHandler
public class GlobalExceptionHandler extends ResponseEntityExceptionHandler {
//@ExceptionHandler: @Controller, @RestController 가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리. 특정 Exception 을 지정해서 별도로 처리해줌
//사용법: 1. @ExceptionHandler({ 인자로 캐치하고 싶은 예외클래스1, 인자로 캐치하고 싶은 예외클래스2 }) 이런식으로 두 개 이상 등록도 가능
@ExceptionHandler(value = {CustomException.class})
//CustomException: 기본적으로 제공되는 Exception 외에 사용
protected ResponseEntity<ErrorResponse> handleCustomException(CustomException e) {
//log: 로깅 메시지를 전달
log.error("handleDataException throw Exception : {}", e.getErrorCode());
return ErrorResponse.toResponseEntity(e.getErrorCode());
}
//Bean Validation 예외 발생시
@Override
//handleMethodArgumentNotValid: Controller 에서 @Valid 를 사용했을 때, MethodArgumentNotValidException 예외가 발생
//문제 원인? 위의 @ExceptionHandler 에서 이미 MethodArgumentNotValidException 이 구현되어있는데, GlobalExceptionHandler 에서 동일한 예외 처리를 하게 되면, Ambiguous(모호성) 문제가 발생
//해결법? handleMethodArgumentNotValid 를 오버라이딩 하기
//object: 모든 클래스들의 최상위 클래스 --> 따라서, 모든 클래스는 Object 클래스에서 상속을 받음
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers,
HttpStatus status,
WebRequest request) {
ErrorCode errorCode = ErrorCode.INVALID_FORMAT;
return ResponseEntity.status(errorCode.getHttpStatus())
.body(new ErrorResponse(errorCode));
}
}
package com.sparta.springboards.jwt;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.sparta.springboards.dto.SecurityExceptionDto;
import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Slf4j
@RequiredArgsConstructor
public class JwtAuthFilter extends OncePerRequestFilter {
//의존성 주입
private final JwtUtil jwtUtil;
@Override
//request: 토큰을 가져온다
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//jwtUtil 에서 resolveToken() 메소드를 사용해서, 그 안에는 request 정보를 담아서, token 에 저장
String token = jwtUtil.resolveToken(request);
//token 이 null 이 아닌 경우(토큰이 이미 있는 경우 = 로그인, 회원가입이 돼있는 경우),
//로그인, 회원가입은 인증이 필요 없음 --> 그래서 Token 이 Header 에 들어가 있지 않음 --> 이런 처리(if) 해주지 않으면, 토큰을 검증하는 부분에서 exception 발생해버림
//인증이 필요없는 URL 는 if 문이 수행되지 않고, 다음 Filter 로 이동
if (token != null) {
//jwtUtil 에서, validateToken() 메소드를 실행해서 false 가 된다면 (= JWT 검증 부분)
if (!jwtUtil.validateToken(token)) {
//jwtExceptionHandler() 메소드를 실행하는데, 그 안에는 response, "Token Error", HttpStatus.UNAUTHORIZED.value() 정보를 담아둔다.
//jwtExceptionHandler() 메소드는 아래에.
jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
return;
}
//token 이 null 이 아닌 경우(= Token 에 문제가 없다면)(바깥 if 문), jwtUtil 에서 token 정보를 담은 getUserInfoFromToken() 메소드를 실행해라. (= JWT 에서 사용자 정보 가져오기 부분)
Claims info = jwtUtil.getUserInfoFromToken(token);
//그리고, info 매개변수에 getSubject() 정보를 담은 setAuthentication() 메소드를 실행하라.
setAuthentication(info.getSubject());
}
//filterChain 매개변수에, doFilter() 메소드를 사용해서, request,response 정보를 담아라.
filterChain.doFilter(request,response);
}
// 인증 객체 생성 및 등록
//token 이 null 이 아닌 경우(위의 if 문에서 이어짐)
public void setAuthentication(String username) {
//SecurityContextHolder: authentication 을 담고 있는 Holder
SecurityContext context = SecurityContextHolder.createEmptyContext(); //SecurityContext context 안에 인증한 객체가 들어가게 됨
Authentication authentication = jwtUtil.createAuthentication(username); // 인증 객체 생성 부분
context.setAuthentication(authentication);
SecurityContextHolder.setContext(context);
}
//위의 if(token != null) 인 경우 中 jwtUtil 에서, validateToken() 메소드를 실행해서 false 가 되는 경우
// 여기에서 Token 에 대한 오류가 발생했을 때, Exception 한 결과값을 Client 에게 넘긴다
public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
response.setStatus(statusCode);
response.setContentType("application/json");
//try-catch 문 (try 블럭 내에는 예외 발생 가능성 있는 문장이 들어간다.)
//1. 예외가 try 블럭 내에서 발생하지 않은 경우 --> catch 블럭을 거치지 않고 try 문 자체를 빠져나감
//2. 예외가 try 블럭 내에서 발생한 경우 --> 이를 처리할 수 있는 catch 블럭이 있는지 찾음
try {
//ObjectMapper 를 통해서 변환한다
String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
//write: 문자 기반 출력 스트림의 최상위 추상 클래스
response.getWriter().write(json);
//catch(Exception e): 어떤 종류의 예외가 발생하더라도 이 catch 블럭에 의해서 처리가 될 수 있음
//보통 마지막 catch 블럭에 Exception e를 선언
} catch (Exception e) {
log.error(e.getMessage());
}
}
}
package com.sparta.springboards.jwt;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.security.UserDetailsServiceImpl;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
//빈 등록을 위해 사용하는데, @Component 는 Bean Configuration 파일에 Bean 을 따로 등록하지 않아도 사용 가능하게 만들어줌
@Component
@RequiredArgsConstructor
public class JwtUtil { //빈이 등록됐다는 '나뭇잎 모양' 확인 가능
//의존성 주입
private final UserDetailsServiceImpl userDetailsService;
//토큰 생성에 필요한 값
//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;
//@PostConstruct: 의존성 주입이 이루어진 후, 초기화를 수행하는 메서드
//사용 이유?
//1. 생성자가 호출되었을 때, 빈은 초기화되지 않았음(의존성 주입이 이루어지지 않았음)
//2. 어플리케이션이 실행될 때, bean 이 오직 한 번만 수행되게 해서 bean 이 여러 번 초기화되는 걸 방지
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey); //Base64로 인코딩되어 있는 것을, 값을 가져와서(getDecoder()) 디코드하고(decode(secretKey)), byte 배열로 반환
key = Keys.hmacShaKeyFor(bytes);
}
//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();
}
// 인증 객체 생성
public Authentication createAuthentication(String username) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username); //userDetailsService 에서 User 가 있는지 없는지 확인
return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
}
}
package com.sparta.springboards.repository;
import com.sparta.springboards.entity.BoardLike;
import org.springframework.data.jpa.repository.JpaRepository;
public interface BoardLikeRepository extends JpaRepository<BoardLike, Long> {
//Optional<T>: null 값일 수도 있는 변수를 감싸주는 Wrapper class
//제너릭으로 값을 타입을 지정
//NullPointerException 이 발생하지 않게 함
//NPE 는 언제 발생? 실제 값이 아닌 null 을 가지고 있는 객체/변수를 호출 시
//조회용 함수
//단일건을 조회 --> findBy
//여러건을 조회 --> findAllBy
//Optional<BoardLike> findByBoardIdAndUserId(Long boarId, Long userId);
void deleteByBoardIdAndUserId(Long boardId, Long userId);
boolean existsByBoardIdAndUserId(Long boardId, Long userId);
}
package com.sparta.springboards.repository;
import com.sparta.springboards.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> findAllByCategoryOrderByCreatedAtDesc(String category);
List<Board> findAllByOrderByCreatedAtDesc();
//게시글 수정, 삭제
//findByIdAndUserId(Long id, Long userId): Board 의 id 와 userId 가 일치하는 Board 를 가져온다
//이유? 로그인한 해당 사용자가 작성한 게시글만 수정, 삭제되기 때문(본인 인증)
//게시글 수정, 삭제에서 optional 사용 이유? id 와 userid 를 비교해서 일치하지 않으면, 예외 처리를 하기때문
Optional<Board> findByIdAndUserId(Long id, Long userId);
}
package com.sparta.springboards.repository;
import com.sparta.springboards.entity.CommentLike;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CommentLikeRepository extends JpaRepository<CommentLike, Long> {
Optional<CommentLike> findByCommentIdAndUserId(Long cmtId, Long userid);
void deleteByCommentIdAndUserId(Long cmtId, Long userid);
int countAllByCommentId(Long cmtId);
}
package com.sparta.springboards.repository;
import com.sparta.springboards.entity.Comment;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface CommentRepository extends JpaRepository<Comment, Long> {
Optional<Comment> findByIdAndUserId(Long cmtId, Long UserId);
}
package com.sparta.springboards.repository;
import com.sparta.springboards.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface UserRepository extends JpaRepository<User, Long> {
//회원 중복 확인
Optional<User> findByUsername(String username);
void deleteByUsername(String username);
}
package com.sparta.springboards.security;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.ArrayList;
import java.util.Collection;
//회원 상세정보 (UserServiceImpl) 를 통해 "권한 (Authority)" 설정 가능
public class UserDetailsImpl implements UserDetails {
//인증이 완료된 사용자 추가
private final User user;
private final String username;
public UserDetailsImpl(User user, String username) {
this.user = user;
this.username = username;
}
//인증완료된 User 를 가져오는 Getter
public User getUser() {
return user;
}
//사용자의 권한 GrantedAuthority 로 추상화 및 반환
@Override
//Collection 이란?
//? 의 의미?
//GrantedAuthority: 현재 사용자가 가지고 있는 권한
public Collection<? extends GrantedAuthority> getAuthorities() {
UserRoleEnum role = user.getRole();
String authority = role.getAuthority();
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(authority);
Collection<GrantedAuthority> authorities = new ArrayList<>();
authorities.add(simpleGrantedAuthority);
return authorities;
}
//사용자의 ID, PWD Getter
@Override //오버라이드 vs 오버로드 차이
public String getUsername() {
return this.username;
}
@Override
public String getPassword() {
return null;
}
@Override
public boolean isAccountNonExpired() {
return false;
}
@Override
public boolean isAccountNonLocked() {
return false;
}
@Override
public boolean isCredentialsNonExpired() {
return false;
}
@Override
public boolean isEnabled() {
return false;
}
}
package com.sparta.springboards.security;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
@Override
//throws: 예외 떠넘기기
//리턴타입 매개변수명 (매개변수 선언1, ...) throws 예외클래스1, ... { }
//예외클래스1,... : 상위 예외 클래스
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
return new UserDetailsImpl(user, user.getUsername());
}
}
package com.sparta.springboards.service;
import com.sparta.springboards.dto.BoardRequestDto;
import com.sparta.springboards.dto.BoardResponseDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.entity.*;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.repository.BoardLikeRepository;
import com.sparta.springboards.repository.BoardRepository;
import com.sparta.springboards.repository.CommentLikeRepository;
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;
import static com.sparta.springboards.exception.ErrorCode.*;
@Service
@RequiredArgsConstructor
public class BoardService {
//DB 접근을 위한 의존성 주입
private final BoardRepository boardRepository;
private final BoardLikeRepository boardLikeRepository;
private final CommentLikeRepository commentLikeRepository;
//게시글 작성
//@Transactional : 클래스나 메서드에 붙여줄 경우, 해당 범위 내 메서드가 트랜잭션(데이터베이스 관리 시스템 또는 유사한 시스템에서 상호작용의 단위(더 이상 쪼개질 수 없는 최소의 연산))이 되도록 보장
//사용 이유?
//1. 해당 메서드를 실행하는 도중 메서드 값을 수정/삭제하려는 시도가 들어와도, 값의 신뢰성이 보장
//2. 연산 도중 오류가 발생해도, rollback 해서 DB에 해당 결과가 반영되지 않도록 함
//예시: 결제는 다른 사람과 독립적으로 이루어지며, 과정 중에 다른 연산이 끼어들 수 없다. 오류가 생긴 경우 연산을 취소하고 원래대로 되돌린다. 성공할 경우 결과를 반영한다
@Transactional
//미리 말해두기: BoardResponseDto 타입으로 반환 받을 건데, createBoard() 메소드를 사용해서, 그 안에는 BoardRequestDto 객체의 requestDto 정보, User 객체의 user 정보가 담겨져있는 상태로 받을꺼다!
public BoardResponseDto createBoard(BoardRequestDto requestDto, User user) {
//만약, requestDto 에서 category 가 "TIL" 또는 "Spring" 또는 "Java" 이라면,
if(requestDto.getCategory().equals("TIL") || requestDto.getCategory().equals("Spring") || requestDto.getCategory().equals("Java")) {
//requestDto, user 정보를 Board 객체에 담아서 boardRepository 에 board 라는 매개변수로 저장한다.
Board board = boardRepository.save(new Board(requestDto, user));
//다시 돌려줘: BoardResponseDto() 메소드에 board 정보를 담아서, DB 로부터 return(반환) 받는다.
return new BoardResponseDto(board);
//아니라면, 예외처리
} else {
throw new CustomException(NOT_EXIST_CATEGORY);
}
}
//전체 게시글 목록 조회
//(readOnly = true): JPA 를 사용할 경우, 변경감지 작업을 수행하지 않아 성능상의 이점
@Transactional(readOnly = true)
//BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
public List<BoardResponseDto> getListBoards(User user, String category) {
//만약, category 가 "TIL" 또는 "Spring" 또는 "Java" 이라면,
if (category.equals("TIL") || category.equals("Spring") || category.equals("Java")) {
//category 를 findAllByCategoryOrderByCreatedAtDesc 정렬해서, boardList 에 담는다.
List<Board> boardList = boardRepository.findAllByCategoryOrderByCreatedAtDesc(category);
//for 문을 돌린 결과를 담을 List 를 미리 만들어둔다.
List<BoardResponseDto> boardResponseDto = new ArrayList<>();
//for 문을 이용하여, boardList 에 담긴 데이터들을 객체 Board 로 모두 옮긴다
for (Board board : boardList) {
//for 문을 돌린 결과를 담을 List 를 미리 만들어둔다.
List<CommentResponseDto> commentList = new ArrayList<>();
//for 문을 이용하여, board.getComments() 에 담긴 데이터들을 객체 Comment 로 모두 옮긴다
for (Comment comment : board.getComments()) {
//commentList 에 comment 정보, comment.getId() 정보를 담아서 추가(add)한다.
commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
}
//boardResponseDto 에 board 정보, commentList 정보, checkBoardLike 정보를 담아서 추가(add)한다.
//checkBoardLike: 해당 회원의 해당 게시글 좋아요 여부. 게시글 좋아요는 해당 게시글의 아이디 정보(board.getId()), 유저 정보(user) 를 담고있다.
boardResponseDto.add(new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user))));
}
return boardResponseDto;
} else {
//boardRepository 와 연결해서, 모든 데이터들을 내림차순으로, List 타입으로 객체 Board 에 저장된 데이터들을 boardList 안에 담는다
List<Board> boardList = boardRepository.findAllByOrderByCreatedAtDesc();
List<BoardResponseDto> boardResponseDto = new ArrayList<>();
for (Board board : boardList) {
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getComments()) {
commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
}
boardResponseDto.add(new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user))));
}
return boardResponseDto;
}
}
//선택한 게시글 조회
@Transactional(readOnly = true)
public BoardResponseDto getBoard(Long id, User user) {
//boardRepository 와 연결해서, 게시글의 id 를 찾는다
//boardRepository 와 연결? DB 에 접근하여, 이미 저장돼있는 id 가 있는지 확인하기 위함
Board board = boardRepository.findById(id).orElseThrow (
() -> new CustomException(NOT_FOUND_USER)
);
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getComments()) {
commentList.add(new CommentResponseDto(comment,commentLikeRepository.countAllByCommentId(comment.getId())));
}
return new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user)));
}
//선택한 게시글 수정(변경)
@Transactional
public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto, User user) {
Board board;
//user 의 권한이 ADMIN 와 같다면,
if(user.getRole().equals(UserRoleEnum.ADMIN)) {
//boardRepository 와 연결해서, 게시글의 id 를 찾는다
board = boardRepository.findById(id).orElseThrow(
//게시글이 DB 에 없다면
() -> new CustomException(NOT_FOUND_BOARD)
);
//user 의 권한이 ADMIN 이 아니라면,
} else {
//boardRepository 와 연결해서, 게시글의 id 와 유저의 id 를 비교한다.(같은 id 인지 확인) --> 아이디가 같은 유저만 수정 가능하도록
board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
//게시글의 id 와 유저의 id 가 다르다면
() -> new CustomException(AUTHORIZATION)
);
}
board.update(requestDto);
List<CommentResponseDto> commentList = new ArrayList<>();
for (Comment comment : board.getComments()) {
commentList.add(new CommentResponseDto(comment, commentLikeRepository.countAllByCommentId(comment.getId())));
}
if(requestDto.getCategory().equals("TIL") || requestDto.getCategory().equals("Spring") || requestDto.getCategory().equals("Java")){
return new BoardResponseDto(board, commentList, (checkBoardLike(board.getId(), user)));
}else{
throw new CustomException(NOT_EXIST_CATEGORY);
}
}
//선택한 게시글 삭제
@Transactional
public void deleteBoard (Long id, User user) {
Board board;
if(user.getRole().equals(UserRoleEnum.ADMIN)) {
board = boardRepository.findById(id).orElseThrow(
() -> new CustomException(NOT_FOUND_BOARD)
);
} else {
board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
() -> new CustomException(AUTHORIZATION)
);
}
//boardRepository 로 board 정보의 삭제를 위해 delete() 메소드를 실행한다.
boardRepository.delete(board);
}
// 게시글 좋아요 확인
@Transactional(readOnly = true)
public boolean checkBoardLike(Long boardId, User user) {
// DB 에서 해당 유저의 게시글 좋아요 여부 확인
//exists : 데이터가 존재하는지 확인. countBy 보다 성능측면에서 우수. existsBy 의 반환 타입은 boolean. countBy 는 반환 타입이 Long
//이유? 검색 도중 중복되는 게 하나라도 존재하면, 그 즉시 쿼리를 종료하기때문에, 모든 개수를 세는 count 보다 압도적으로 좋다
//쿼리: DB 에게 정보를 요청하는 행위. 보통 DB 에서 특정 주제어, 어귀를 찾기위해 사용됨
return boardLikeRepository.existsByBoardIdAndUserId(boardId, user.getId());
}
// 게시글 좋아요 생성 및 삭제
@Transactional
public MsgResponseDto saveBoardLike(Long boardId, User user) {
// 1. 요청한 글이 DB 에 존재하는지 확인
Board board = boardRepository.findById(boardId).orElseThrow(
() -> new CustomException(NOT_FOUND_BOARD)
);
// 2. 해당 유저의 게시글 좋아요 여부를 확인해서 false 라면
if (!checkBoardLike(boardId, user)) {
// 3. 즉시 해당 유저의 게시글 좋아요를 DB 에 저장
//saveAndFlush(): 즉시 DB 에 변경사항을 적용
//save(): 바로 DB 에 저장되지 않고, 영속성 컨텍스트에 저장되었다가, flush() 또는 commit() 수행 시 DB에 저장
boardLikeRepository.saveAndFlush(new BoardLike(board, user));
return new MsgResponseDto("좋아요 완료", HttpStatus.OK.value());
// 4. 게시글 좋아요 여부가 true 라면, 해당 유저의 게시글 좋아요를 DB 에서 삭제
} else {
boardLikeRepository.deleteByBoardIdAndUserId(boardId, user.getId());
return new MsgResponseDto("좋아요 취소", HttpStatus.OK.value());
}
}
}
package com.sparta.springboards.service;
import com.sparta.springboards.dto.CommentRequestDto;
import com.sparta.springboards.dto.CommentResponseDto;
import com.sparta.springboards.dto.MsgResponseDto;
import com.sparta.springboards.entity.*;
import com.sparta.springboards.entity.Board;
import com.sparta.springboards.entity.Comment;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.repository.BoardRepository;
import com.sparta.springboards.repository.CommentLikeRepository;
import com.sparta.springboards.repository.CommentRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static com.sparta.springboards.exception.ErrorCode.*;
@Service
@RequiredArgsConstructor
public class CommentService {
private final CommentRepository commentRepository;
private final BoardRepository boardRepository;
private final CommentLikeRepository commentLikeRepository;
//댓글 작성
@Transactional
public CommentResponseDto createComment(Long id, CommentRequestDto commentRequestDto, User user) {
Board board = boardRepository.findById(id).orElseThrow(
() -> new CustomException(NOT_FOUND_BOARD)
);
Comment comment = commentRepository.save(new Comment(commentRequestDto, board, user));
//댓글 생성시, 좋아요를 0으로 초기화시켜주기 위함
int cnt = 0;
return new CommentResponseDto(comment,cnt);
}
//댓글 수정
@Transactional
public CommentResponseDto updateComment(Long boardId, Long cmtId, CommentRequestDto commentRequestDto, User user) {
//DB 에 게시글 저장 확인
Board board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
Comment comment;
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
comment = commentRepository.findById(cmtId).orElseThrow(
() -> new CustomException(NOT_FOUND_COMMENT)
);
} else {
comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow(
() -> new CustomException(AUTHORIZATION)
);
}
comment.update(commentRequestDto);
return new CommentResponseDto(comment ,commentLikeRepository.countAllByCommentId(comment.getId()));
}
//댓글 삭제
@Transactional
public CommentResponseDto deleteComment(Long boardId, Long cmtId, User user) {
//DB 에 게시글 저장 확인
Board board = boardRepository.findById(boardId).orElseThrow (
() -> new CustomException(NOT_FOUND_BOARD)
);
Comment comment;
//ADMIN 권한을 가진 user 이라면
if (user.getRole().equals(UserRoleEnum.ADMIN)) {
//DB 에서 댓글을 찾아봄
comment = commentRepository.findById(cmtId).orElseThrow(
() -> new CustomException(NOT_FOUND_COMMENT)
);
//ADMIN 권한을 가진 user 가 아니라면
} else {
//DB 에서 댓글을 찾아봄
comment = commentRepository.findByIdAndUserId(cmtId, user.getId()).orElseThrow(
() -> new CustomException(AUTHORIZATION)
);
}
//해당 댓글 삭제
commentRepository.deleteById(cmtId);
return new CommentResponseDto(comment,commentLikeRepository.countAllByCommentId(comment.getId()));
}
//댓글 좋아요
@Transactional
public MsgResponseDto CommentLike(Long cmtId, User user) {
//DB 에서 댓글을 찾아봄
Comment comment = commentRepository.findById(cmtId).orElseThrow(
() -> new CustomException(NOT_FOUND_COMMENT)
);
//DB 에 해당 usr 가 '댓글 좋아요' 누른 적이 없다면, '댓글 좋아요' 를 추가하기
if (commentLikeRepository.findByCommentIdAndUserId(cmtId, user.getId()).isEmpty()){
CommentLike commentLike = CommentLike.builder()
.comment(comment)
.user(user)
.build();
commentLikeRepository.save(commentLike);
return new MsgResponseDto("좋아요 완료", HttpStatus.OK.value());
//DB 에 해당 usr 가 '댓글 좋아요' 누른 적이 있다면, '댓글 좋아요' 를 제거하기
}else {
commentLikeRepository.deleteByCommentIdAndUserId(comment.getId(), user.getId());
return new MsgResponseDto("좋아요 취소", HttpStatus.OK.value());
}
}
}
package com.sparta.springboards.service;
import com.sparta.springboards.dto.LoginRequestDto;
import com.sparta.springboards.dto.SignupRequestDto;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.exception.CustomException;
import com.sparta.springboards.jwt.JwtUtil;
import com.sparta.springboards.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.servlet.http.HttpServletResponse;
import java.util.Optional;
import static com.sparta.springboards.exception.ErrorCode.*;
@Service
//다른 계층에서 파라미터를 검증하기 위해서
@Validated //추가
//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor
public class UserService {
//회원가입 구현
private final UserRepository userRepository;
private final JwtUtil jwtUtil; //의존성 주입(DI) --> jwtUtil.class 에서 @Component 로 빈을 등록했기때문에 가능
private final PasswordEncoder passwordEncoder;
private static final String ADMIN_TOKEN = "AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC";
@Transactional
//SignupRequestDto 에서 가져온 username, password 를 확인
public void signup(SignupRequestDto signupRequestDto) {
String username = signupRequestDto.getUsername();
String password = passwordEncoder.encode(signupRequestDto.getPassword()); //저장하기 전에 password 를 Encoder 한다
// 회원 중복 확인
//userRepository 에서 username 으로 실제 유저가 있는지 없는지 확인
Optional<User> found = userRepository.findByUsername(username);
//중복된 유저가 존재한다면,
if (found.isPresent()) {
throw new CustomException(DUPLICATED_USERNAME);
}
// 사용자 ROLE(권한) 확인
UserRoleEnum role = UserRoleEnum.USER;
//SignupRequestDto 에서 admin 이 true 라면(admin 으로 로그인을 시도하려고 하는구나),
if (signupRequestDto.isAdmin()) {
//들어온 Token 값과 위의 검증을 위한 Token 값(위쪽에 AAABnvxRVklrnYxKZ0aHgTBcXukeZygoC)이 일치하는지 확인
if (!signupRequestDto.getAdminToken().equals(ADMIN_TOKEN)) {
//일치하지 않으면(매개변수가 의도치 않는 상황 유발시), 해당 메시지를 보낸다
throw new CustomException(INVALID_AUTH_TOKEN);
}
//일치하면, 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 CustomException(NOT_FOUND_USER)
);
//비밀번호 확인
//User 객체에 들어있던 Password 와 가지고 온 Password(String password = loginRequestDto.getPassword()에 있는) 가 일치하는지 확인
if(!passwordEncoder.matches(password, user.getPassword())) {
throw new CustomException(NOT_MATCH_INFORMATION);
}
//response 에 addHeader() 를 사용해서, Header 쪽에 값을 넣는다. (위의 User 객체에서 유저 정보를 가져왔기 떄문에 사용 가능한 것)
//AUTHORIZATION_HEADER: KEY 값
//jwtUtil: 이것을 사용하기 위해, 위에서 의존성 주입을 해줘야 함
//createToken(user.getUsername(), user.getRole()): Username(이름), Role(권한)을 넣어서 토큰을 만든다
response.addHeader(JwtUtil.AUTHORIZATION_HEADER, jwtUtil.createToken(user.getUsername(), user.getRole()));
}
//회원 탈퇴
@Transactional
public void signOut(LoginRequestDto loginRequestDto, User user){
//Username 의 문자열의 길이가 0 이라면,
//isEmpty(): 문자열의 길이가 0인 경우에, true 를 리턴. 비어있지 않으면 false 를 리턴
if (userRepository.findByUsername(loginRequestDto.getUsername()).isEmpty()) {
throw new CustomException(NOT_FOUND_USER);
//Username 의 문자열의 길이가 0 이 아니라면,
} else {
//password 의 일치여부를 비교해서, 일치하지 않다면
if(!passwordEncoder.matches(loginRequestDto.getPassword(), user.getPassword())) {
throw new CustomException(NOT_FOUND_USER);
//password 의 일치여부를 비교해서, 일치하다면
} else {
//loginRequestDto 에서 입력한 Username 을 deleteByUsername() 메소드를 써서, userRepository 로 보낸다.
userRepository.deleteByUsername(loginRequestDto.getUsername());
}
}
}
}
package com.sparta.springboards;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
@EnableJpaAuditing
@SpringBootApplication
public class SpringboardsApplication {
public static void main(String[] args) {
SpringApplication.run(SpringboardsApplication.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 '2.7.6'
id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}
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-security'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.h2database:h2'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-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() //JUnit: 자바 프로그래밍 언어용 단위 테스트 프레임워크 --> JUnit 을 이용한 단위 테스트 가능
}