Spring 심화 주차 과제 LV1

박영준·2022년 12월 12일
0

Java

목록 보기
30/111

요구사항

전체적인 구성

config

WebSecurityConfig

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

    @Bean
    //WebSecurityCustomizer 은 SecurityFilterChain 보다 우선적으로 걸리는 설정
    public WebSecurityCustomizer webSecurityCustomizer() {
        //h2-console 사용 및 resources 접근 허용 설정
        //ignoring(): 이러한 경로도 들어온 것들은 인증 처리하는 것을 무시하겠다
        return (web) -> web.ignoring()
                .requestMatchers(PathRequest.toH2Console())
                .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()
//                .antMatchers("/api/search").permitAll()
//                .antMatchers("/api/shop").permitAll()
                //그 이외의 URL 요청들을 전부 다 authentication(인증 처리)하겠다
                .anyRequest().authenticated()
                // JWT 인증/인가를 사용하기 위한 설정
                // Custom Filter 등록하기
                //addFilterBefore: 어떤 Filter 이전에 추가하겠다 --> 우리가 만든 JwtAuthFilterUsernamePasswordAuthenticationFilter 이전에 실행할 수 있도록
                //1. JwtAuthFilter 를 통해 인증 객체를 만들고 --> 2. context 에 추가 --> 3.인증 완료 --> UsernamePasswordAuthenticationFilter 수행 --> 인증됐으므로 다음 Filter 로 이동 --> Controller 까지도 이동
                .and().addFilterBefore(new JwtAuthFilter(jwtUtil), UsernamePasswordAuthenticationFilter.class);

//        // Custom 로그인 페이지 사용
//        // Security 에서 제공하는 default Form Login 을 사용하겠다
//        http.formLogin().loginPage("/api/user/login-page").permitAll();

        //"거부"가 났을 때, 403 Forbidden 페이지(접근 제한 페이지) 이동 설정
        http.exceptionHandling().accessDeniedPage("/api/user/forbidden");

        return http.build();
    }
}

controller

BoardController

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.security.core.annotation.AuthenticationPrincipal;
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;    //boardService 를 가져다 쓰겠다 (의존성 주입)

    //게시글 작성
//    @Secured({"ROLE_ADMIN","ROLE_USER"})
    @PostMapping("/post")
    //BoardResponseDto 반환 타입, createBoard 메소드 명
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
    //@AuthenticationPrincipal: 인증 객체(Authentication)의 Principal 부분의 값을 가져온다
    //UserDetailsImpl userDetails: 인증 객체를 만들 때, Principal 부분에 userDetails 를 넣었기 때문에, userDetails 를 파라미터로 받아올 수 있었음
    public BoardResponseDto createBoard(@RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //데이터를 담아서, boardService 로 응답을 보냄
        return boardService.createBoard(requestDto, userDetails.getUser());
    }

//    @Secured({"ROLE_ADMIN","ROLE_USER"})
    //전체 게시글 목록 조회
    @GetMapping("/posts")
    //BoardResponseDto 를 List 로 반환하는 타입, getListBoards 메소드 명, () 전부 Client 에게로 반환하므로 비워둠
    public List<BoardResponseDto> getListBoards() {
        //() 모든 데이터를 담아서, boardService 로 응답을 보냄
        return boardService.getListBoards();
    }

//    @Secured({"ROLE_ADMIN","ROLE_USER"})
    //선택한 게시글 조회
    @GetMapping("/post/{id}")
    //BoardResponseDto 반환 타입, getBoards 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터 --> 전체 게시글 목록에서 id 값으로 각각의 게시글을 구별
    public BoardResponseDto getBoards(@PathVariable Long id) {
        //id 값을 담아서, boardService 로 응답을 보냄
        return boardService.getBoard(id);
    }

//    @Secured({"ROLE_ADMIN","ROLE_USER"})
    //선택한 게시글 수정(변경)
    @PutMapping("/post/{id}")
    //BoardResponseDto 반환 타입, updateBoard 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터,
    //@RequestBody: HTTP Method 안의 body 값을 Mapping(key:value 로 짝지어줌), BoardRequestDto: 넘어오는 데이터를 받아주는 객체
    //@AuthenticationPrincipal: 인증 객체(Authentication)의 Principal 부분의 값을 가져온다
    //UserDetailsImpl userDetails: 인증 객체를 만들 때, Principal 부분에 userDetails 를 넣었기 때문에, userDetails 를 파라미터로 받아올 수 있었음
    public BoardResponseDto updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        //데이터를 담아서, boardService 로 응답을 보냄
        return boardService.updateBoard(id, requestDto, userDetails.getUser());
    }

//    @Secured({"ROLE_ADMIN","ROLE_USER"})
    //선택한 게시글 삭제
    @DeleteMapping("/post/{id}")
    //MsgResponseDto 반환 타입, deleteBoard 메소드 명
    //@PathVariable: URL 경로에 변수를 넣기, Long id: 담을 데이터
    //@AuthenticationPrincipal: 인증 객체(Authentication)의 Principal 부분의 값을 가져온다
    //UserDetailsImpl userDetails: 인증 객체를 만들 때, Principal 부분에 userDetails 를 넣었기 때문에, userDetails 를 파라미터로 받아올 수 있었음
    public MsgResponseDto deleteBoard(@PathVariable Long id, @AuthenticationPrincipal UserDetailsImpl userDetails) {
        boardService.deleteBoard(id,userDetails.getUser());
        return new MsgResponseDto("삭제 성공", HttpStatus.OK.value());
    }
}

UserController

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.service.UserService;
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;

import javax.servlet.http.HttpServletResponse;
import javax.validation.Valid;

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

dto

BoardRequestDto

package com.sparta.springboards.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Setter

//파라미터가 없는 기본생성자를 생성
@NoArgsConstructor
@AllArgsConstructor

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

BoardResponseDto

package com.sparta.springboards.dto;

import com.sparta.springboards.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();

    }
}

LoginRequestDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.Setter;

@Setter
@Getter

public class LoginRequestDto {
    //필드
    private String username;
    private String password;
}

MsgResponseDto

package com.sparta.springboards.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;
//    }
}

SecurityExceptionDto

package com.sparta.springboards.dto;

import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class SecurityExceptionDto {

    private int statusCode;
    private String msg;

    public SecurityExceptionDto(int statusCode, String msg) {
        this.statusCode = statusCode;
        this.msg = msg;
    }
}

SignupRequestDto

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 = "아이디 형식이 일치하지 않습니다.")
    private String username;

    @Size(min = 8,max = 15,message ="비밀번호는 8에서 15자 사이 입니다.")
    @Pattern(regexp = "[a-zA-Z0-9`~!@#$%^&*()_=+|{};:,.<>/?]*$",message = "비밀번호 형식이 일치하지 않습니다.")
    private String password;

    //private String email;
    private boolean admin = false;  //admin 인지 아닌지 확인
    private String adminToken = "";
}

entity

Board

package com.sparta.springboards.entity;

import com.sparta.springboards.dto.BoardRequestDto;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

import javax.persistence.*;

//@Getter, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter
@Setter
@Entity
@Table(name = "Board")

//파라미터가 없는 기본생성자를 생성
@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;

    @ManyToOne
    @JoinColumn(name = "user_id")
    //private Long userId;
    private User user;      //User 빨간줄> -->  @ManyToOne 로 해결


    //생성자
    //게시글 작성
    public Board(BoardRequestDto requestDto, User user) {   //userId -> user
        this.title = requestDto.getTitle();             //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
        this.username = user.getUsername();
        this.contents = requestDto.getContents();
        this.user = user;          //userId -> user          //userId: 다른 값과 일치하는지를 비교해서 본인 인증을 위해 (연관관계를 짓기 위함)?
    }

    //선택한 게시글 수정(변경)
    public void update(BoardRequestDto boardrequestDto) {       //boardrequestDto? requestDto?
        this.title = boardrequestDto.getTitle();             //this.title: (위에서 선언된) 필드, BoardRequestDto 객체의 requestDto 매개변수로 들어온 데이터를 getTitle() 에 담는다(DB 로 보내기 위해)
        this.contents = boardrequestDto.getContents();

    }
}

Timestamped

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, @Setter: 필드에 선언시 자동으로 get, set 메소드 생성. 클래스에서 선언시 모든 필드에 접근자와 설정자가 자동으로 생성
@Getter

//객체의 입장에서 공통 매핑 정보가 필요할 때 사용
//공통 매핑 정보가 필요할 때, 부모 클래스에 선언하고, 속성만 상속 받아서 사용하고 싶을 때 --> Board 가 Timestamped 를 상속받고있다!
@MappedSuperclass

//Entity 가 삽입, 삭제, 수정, 조회 등...의 작업을 하기 전/후에 어떤 작업을 하기 위해 이벤트 처리를 위해서
//예시: 데이터 삽입 전 또는 수정 전에 createdAt 과 modifiedAt 의 시간을 조작
//@EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능(시간에 대해서 자동으로 값을 넣어주는 기능)을 포함
@EntityListeners(AuditingEntityListener.class)

public class Timestamped {
    //createdAt, modifiedAt 컬럼 2개를 가진다

    //생성일자
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;

    //마지막 수정일자
    @LastModifiedDate
    @Column
    private LocalDateTime modifiedAt;
}

User

package com.sparta.springboards.entity;

import lombok.Getter;
import lombok.NoArgsConstructor;

import javax.persistence.*;

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

    //생성자
    //필드명 앞에 this. 를 붙임으로써
    //1. 객체 자기 자신을 참조한다고 밝히고,
    //2. 매개변수이름과 똑같았던 필드명을 구분지어줘서 필드에 접근 가능하도록 만듦(원래는 매개변수의 우선순위가 더 높았기때문에)
    public User(String username, String password, UserRoleEnum role) {
        this.username = username;       //this.username: (위에서 선언된) 필드, username: 매개변수
        this.password = password;
        this.role = role;
    }
}

UserRoleEnum

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

jwt

JwtAuthFilter

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 {

        String token = jwtUtil.resolveToken(request);

        //로그인, 회원가입은 인증이 필요 없음 --> 그래서 Token 이 Header 에 들어가 있지 않음 --> 이런 처리(if) 해주지 않으면, 토큰을 검증하는 부분에서 exception 발생해버림
        //인증이 필요없는 URL 는 if 문이 수행되지 않고, 다음 Filter 로 이동
        if(token != null) {
            //validateToken() 메소드를 실행해서 false 가 된다면,
            if(!jwtUtil.validateToken(token)){
                //jwtExceptionHandler 를 실행
                jwtExceptionHandler(response, "Token Error", HttpStatus.UNAUTHORIZED.value());
                return;
            }
            //Token 에 문제가 없다면, 여기를 수행
            //getUserInfoFromToken: Token 에서 UserInfo 유저의 정보를 가져온다
            Claims info = jwtUtil.getUserInfoFromToken(token);
            setAuthentication(info.getSubject());
        }
        filterChain.doFilter(request,response);
    }

    // 인증 객체 생성 및 등록
    public void setAuthentication(String username) {
        SecurityContext context = SecurityContextHolder.createEmptyContext();   //SecurityContext context 안에 인증한 객체가 들어가게 됨
        Authentication authentication = jwtUtil.createAuthentication(username);
        context.setAuthentication(authentication);

        SecurityContextHolder.setContext(context);
    }

    //위의 if(token != null) 여기에서 Token 에 대한 오류가 발생했을 때, Exception 한 결과값을 Client 에게 넘긴다
    //validateToken() 메소드를 실행해서 false 가 됐다면, jwtExceptionHandler() 메소드 실행 --> Client 로 반환
    public void jwtExceptionHandler(HttpServletResponse response, String msg, int statusCode) {
        response.setStatus(statusCode);
        response.setContentType("application/json");
        try {
            //ObjectMapper 를 통해서 변환한다
            String json = new ObjectMapper().writeValueAsString(new SecurityExceptionDto(statusCode, msg));
            response.getWriter().write(json);
        } catch (Exception e) {
            log.error(e.getMessage());
        }
    }

}

JwtUtil

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: 로깅 추상화 라이브러리. 변수명이 log 로 고정된다
//log 가 왜 필요한가?  로그를 작성해두면, 어떤 동작 중인지, 어느 부분에 에러가 났는지 파악 가능
@Slf4j

//Bean Configuration 파일에 Bean 을 따로 등록하지 않아도 사용 가능해짐
//빈 등록을 위해
@Component

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@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;


    //메소드
    //종속성 주입이 완료된 후 실행되어야 하는 메서드에 사용
    //처음에 객체가 생성될 때, 초기화하는 함수
    //사용 이유?
    //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();
    }

    // 인증 객체 생성
    public Authentication createAuthentication(String username) {
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);  //userDetailsService 에서 User 가 있는지 없는지 확인
        return new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
    }
}

repository

BoardRepository

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

    //게시글 수정, 삭제
    //Optional: null 을 반환하면 오류가 발생할 가능성이 매우 높은 경우에 '결과 없음'을 명확하게 드러내기 위해
    //--> 수정, 삭제에서 id 와 userid 를 비교해서 일치하지 않으면, 예외 처리를 하기때문에 여기선 적절한 사용법으로 보임
    //findByIdAndUserId(Long id, Long userId): Board 의 id 와 userId 가 일치하는 Board 를 가져온다
    //--> 그래야, 로그인한 해당 사용자가 작성한 게시글만 수정, 삭제되기 때문(본인 인증)
    Optional<Board> findByIdAndUserId(Long id, Long userId);
}

UserRepository

package com.sparta.springboards.repository;

import com.sparta.springboards.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<com.sparta.springboards.entity.User> findByUsername(String username);
    Optional<User> findByUsername(String username);
}

security

UserDetailsImpl

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

UserDetailsServiceImpl

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
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
                .orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));

        return new UserDetailsImpl(user, user.getUsername());
    }

}

service

BoardService

package com.sparta.springboards.service;

import com.sparta.springboards.dto.BoardRequestDto;
import com.sparta.springboards.dto.BoardResponseDto;
import com.sparta.springboards.entity.Board;
import com.sparta.springboards.entity.User;
import com.sparta.springboards.entity.UserRoleEnum;
import com.sparta.springboards.jwt.JwtUtil;
import com.sparta.springboards.repository.BoardRepository;
import com.sparta.springboards.repository.UserRepository;
import lombok.RequiredArgsConstructor;
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, User user) {  //HttpServletRequest request?

        Board board = boardRepository.save(new Board(requestDto, user));
        return new BoardResponseDto(board);
    }


    //전체 게시글 목록 조회
    //(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, User user) {    //HttpServletRequest request?

        Board board;    //board 를 사용하기위해서는 이런 변수 선언 필요함

        //user 의 권한이 ADMIN 와 같다면,
        if(user.getRole().equals(UserRoleEnum.ADMIN)) {
            board = boardRepository.findById(id).orElseThrow(
                    () -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
            );

        } else {
            //user 의 권한이 ADMIN 이 아니라면, 아이디가 같은 유저만 수정 가능
            board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
            );
        }

        board.update(requestDto);

        return new BoardResponseDto(board);

    }

    //선택한 게시글 삭제
    @Transactional
    //MsgResponseDto 반환 타입, deleteBoard 메소드 명
    //Long id: 담을 데이터, HttpServletRequest request 객체: 누가 로그인 했는지 알기위한 토큰을 담고 있음
    public void deleteBoard (Long id, User user) {    //HttpServletRequest request?

        Board board;    //board 를 사용하기위해서는 이런 변수 선언 필요함

        //user 의 권한이 ADMIN 와 같다면,
        if(user.getRole().equals(UserRoleEnum.ADMIN)) {
            board = boardRepository.findById(id).orElseThrow(
                    () -> new IllegalArgumentException("게시글이 존재하지 않습니다.")
                    //() -> new RequestException(ErrorCode.게시글이_존재하지_않습니다_400)
            );

        } else {
            //user 의 권한이 ADMIN 이 아니라면, 아이디가 같은 유저만 수정 가능
            board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                    () -> new IllegalArgumentException("아이디가 존재하지 않습니다.")
            );
        }

        boardRepository.delete(board);
    }
}

UserService

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

@Service

//다른 계층에서 파라미터를 검증하기 위해서
@Validated          //추가

//final 또는 @NotNull 이 붙은 필드의 생성자를 자동 생성. 주로 의존성 주입(Dependency Injection) 편의성을 위해서 사용
@RequiredArgsConstructor

public class UserService {

//회원가입 구현

    //의존성 주입(DI)
    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) {      //@Valid 추가 주의!
        String username = signupRequestDto.getUsername();
        String password = passwordEncoder.encode(signupRequestDto.getPassword());   //저장하기 전에 password 를 Encoder 한다

        // 회원 중복 확인
        //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(!passwordEncoder.matches(password, user.getPassword())) {
            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()));
    }
}

application.properties

spring.h2.console.enabled=true
spring.datasource.url=jdbc:h2:mem:db;MODE=MYSQL;
spring.datasource.username=sa
spring.datasource.password=

spring.thymeleaf.cache=false

spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true

jwt.secret.key=7ZWt7ZW0OTntmZTsnbTtjIXtlZzqta3snYTrhIjrqLjshLjqs4TroZzrgpjslYTqsIDsnpDtm4zrpa3tlZzqsJzrsJzsnpDrpbzrp4zrk6TslrTqsIDsnpA=

logging.level.org.springframework=debug
logging.level.org.springframework.web=debug

build.gradle

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()
}
profile
개발자로 거듭나기!

0개의 댓글