게시판 Lv2 - 회원가입/로그인, 작성(jwt), 전체/선택 조회, 수정/삭제(jwt)

박영준·2023년 7월 1일
0

Spring

목록 보기
31/58

Spring Lv2 과제

1. 기능 요구 사항

  1. 회원 가입 API

    • username, password를 Client에서 전달받기
    • username은 최소 4자 이상, 10자 이하이며 알파벳 소문자(a~z), 숫자(0~9)로 구성되어야 한다.
    • password는 최소 8자 이상, 15자 이하이며 알파벳 대소문자(a~z, A~Z), 숫자(0~9)로 구성되어야 한다.
    • DB에 중복된 username이 없다면 회원을 저장하고 Client 로 성공했다는 메시지, 상태코드 반환하기
  2. 로그인 API

    • username, password를 Client에서 전달받기
    • DB에서 username을 사용하여 저장된 회원의 유무를 확인하고 있다면 password 비교하기
    • 로그인 성공 시, 로그인에 성공한 유저의 정보와 JWT를 활용하여 토큰을 발급하고,
      발급한 토큰을 Header에 추가하고 성공했다는 메시지, 상태코드 와 함께 Client에 반환하기
  3. 전체 게시글 목록 조회 API

    • 제목, 작성자명(username), 작성 내용, 작성 날짜를 조회하기
    • 작성 날짜 기준 내림차순으로 정렬하기
  4. 게시글 작성 API

    • 토큰을 검사하여, 유효한 토큰일 경우에만 게시글 작성 가능
    • 제목, 작성 내용을 저장하고
    • 저장된 게시글을 Client 로 반환하기(username은 로그인 된 사용자)
  5. 선택한 게시글 조회 API

    • 선택한 게시글의 제목, 작성자명(username), 작성 날짜, 작성 내용을 조회하기
      (검색 기능이 아닙니다. 간단한 게시글 조회만 구현해주세요.)
  6. 선택한 게시글 수정 API

    • 수정을 요청할 때 수정할 데이터와 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후(Lv1 과제)
    • 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 수정 가능
    • 제목, 작성 내용을 수정하고 수정된 게시글을 Client 로 반환하기
  7. 선택한 게시글 삭제 API

    • 삭제를 요청할 때 비밀번호를 같이 보내서 서버에서 비밀번호 일치 여부를 확인 한 후(Lv1 과제)
    • 토큰을 검사한 후, 유효한 토큰이면서 해당 사용자가 작성한 게시글만 삭제 가능
    • 선택한 게시글을 삭제하고 Client 로 성공했다는 메시지, 상태코드 반환하기

2. 유스케이스 다이어그램

요구사항에는 없었지만, 연습해볼 겸 만들어보았다.

<<포함>> 점선 화살표
회원가입, 게시글 작성, 게시글 수정, 게시글 삭제는 로그인이 실행되었다는 전제하에 실행 가능하다.

3. API 명세서

  • 회원가입 및 로그인 후에는 비밀번호 등... 을 입력하지 않고,
    인증된 회원(jwt 토큰을 보유한 회원)에 한해서 작성, 수정, 삭제가 가능하도록 한다.

4. ERD

  • Board 엔티티와 User 엔티티는 각 테이블에 유일한 값인 id 를 가진다.

  • Board : User = N : 1 의 다대일 단방향 관계다.

    • Board 가 연관관계의 주인 -> 따라서, Board 가 FK(외래 키)를 가진다.
      • Board 는 '조회, 저장, 수정, 삭제' 가능
      • User 는 '조회'만 가능

5. 프로젝트 환경 세팅

(1) 프로젝트 생성 및 Dependencies 설정

(2) application.properties

spring.datasource.url=jdbc:mysql://localhost:3306/board
spring.datasource.username=root
spring.datasource.password=비밀번호
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

spring.jpa.hibernate.ddl-auto=update

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

spring.jpa.properties.hibernate.dialect= org.hibernate.dialect.MySQLDialect

jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

  • Lv1 세팅에서 + jwt key 값을 추가했다.

(3) build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.1'
    id 'io.spring.dependency-management' version '1.1.0'
}

group = 'com.SpringProject'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}

repositories {
    mavenCentral()
}

dependencies {
    // JPA 설정
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'

    // MySQL
    implementation 'mysql:mysql-connector-java:8.0.33'

    // JWT
    compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
    runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'

    // validation
    implementation 'org.springframework.boot:spring-boot-starter-validation'

    // 추가
    implementation group: 'org.javassist', name: 'javassist', version: '3.15.0-GA'

    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
    useJUnitPlatform()
}
  • Lv1 세팅에서 + JWT, validation 을 추가했다.

6. 구현

1) Controller

ResponseEntity 를 이용하여, 메시지와 에러코드를 함께 반환하도록 했다.

BoardController

(1)

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class BoardController {
    private final BoardService boardService;
    
    ...

}    

@RestController

@RequiredArgsConstructor

  • 해당 어노테이션으로 의존성을 주입함으로써, BoardService 를 매개변수로 받는 생성자를 추가하지 않게 하여 코드를 깔끔하게 만들었다.
  • 다른 클래스에서도 의존성 주입이 필요할 경우, 해당 어노테이션을 사용했다.(이하 생략)

(2)

// 게시글 작성
@PostMapping("/board")
public ResponseEntity<BoardResponseDto> createBoard(@RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
    return ResponseEntity.ok(boardService.createBoard(requestDto, request));
}

@RequestBody

  • JSON 형태의 데이터를 담는다

HttpServletRequest

  • 헤더(Header)에 담긴 jwt 토큰을 담고있는 객체
    • 즉, 로그인한 사용자의 정보다.

ResponseEntity.ok(body)

  • body 까지 보낼 것이기 때문

(3)

// 게시글 전체 조회
@GetMapping("/board")
public ResponseEntity<List<BoardResponseDto>> getBoardList() {
    return ResponseEntity.ok(boardService.getBoardList());
}

// 게시글 선택 조회
@GetMapping("/board/{id}")
public ResponseEntity<BoardResponseDto> getBoard(@PathVariable Long id) {
    return ResponseEntity.ok(boardService.getBoard(id));
}
  • 전체/선택 조회의 경우 jwt 토큰이 필요하지도 않기 때문에 Lv1 과 다른 점은 없다.

(4)

// 게시글 수정
@PutMapping("/board/{id}")
public ResponseEntity<BoardResponseDto> updateBoard(@PathVariable Long id, @RequestBody BoardRequestDto requestDto, HttpServletRequest request) {
    return ResponseEntity.ok(boardService.updateBoard(id, requestDto, request));
}

// 게시글 삭제
@DeleteMapping("/board/{id}")
public ResponseEntity<MsgResponseDto> deleteBoard(@PathVariable Long id, HttpServletRequest request) {
    return ResponseEntity.ok(boardService.deleteBoard(id, request));
}
  • 게시글 수정과 삭제의 경우, 게시글 작성과 같은 이유로 jwt 토큰을 담았다.

  • 게시글 수정에서는 "게시글을 삭제했습니다"라는 메시지만 반환하면 되므로, MsgResponseDto 클래스를 따로 만들어서 반환하도록 했다.

UserController

(1)

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class UserController {
	...
    
}    

@RestController

(2)

// 회원 가입
@PostMapping("/signup")
public ResponseEntity<MsgResponseDto> signup(@RequestBody @Valid SignupRequestDto signupRequestDto) {
    userService.signup(signupRequestDto);
    return ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()));
}

ResponseEntity<MsgResponseDto>

  • 회원가입과 로그인의 요구사항에 '회원가입 및 로그인 성공 시 성공했다는 메시지, 상태코드를 함께 반환' 하라는 조건이 있었다.

    • ResponseEntity 는 복수의 데이터를 반환하는데 적합했다.
  • ? 와일드 카드 ResponseEntity<?> 를 사용해도 동일한 결과를 반환한다.

    • 단, 데이터 타입을 명시하는 편이 가독성/에러/유지보수 측면에서 더 우수하다
  • 참고: ResponseEntity

@Valid

  • 객체의 제약 조건을 검증하도록 지시하는 어노테이션

    • 회원가입 시, 사용자의 username 과 password 에 제약조건을 걸었다.
    • 해당 어노테이션이 붙지 않으면, 기능이 정상적으로 작동하지 않는다.
  • 참고: @Valid(+ 정규 표현식), @Validated

ResponseEntity.ok(new MsgResponseDto("회원가입 완료", HttpStatus.OK.value()))

  • body 와 상태코드를 함께 반환하도록 하기 위해 ResponseEntity.ok 를 사용
    • MsgResponseDto 클래스를 생성하여, 해당 클래스가 body와 상태코드를 반환하도록 했다

(3)

// 로그인
@PostMapping("/login")
public ResponseEntity<MsgResponseDto> login(@RequestBody LoginRequestDto loginRequestDto, HttpServletResponse response) {
    userService.login(loginRequestDto, response);
    return ResponseEntity.ok(new MsgResponseDto("로그인 완료",HttpStatus.OK.value()));
}

HttpServletResponse response

  • userService 의 login 메서드로 jwt 토큰 을 받아와서, 사용자에게 반환한다

2) Dto

@Setter 를 모두 삭제한 이유에 대해서는 아래에 정리해두었다.
참고: Dto 에서의 어노테이션 언제 사용할까

requestDto

(1) BoardRequestDto

@Getter
@NoArgsConstructor
public class BoardRequestDto {
    private String title;
    private String contents;
}
  • 게시글 작성에는 제목, 작성내용만 입력한다
    • 로그인한 사용자만 작성가능하므로, 사용자에 대한 정보는 필요없다

(2) SignupRequestDto

@Getter
@NoArgsConstructor
public class SignupRequestDto {
    @NotBlank
    private String username;
    @NotBlank
    private String password;
}
  • 회원가입에는 username, password 이 필요하다

  • @NotBlank : username, password 입력시 공백을 허용하지 않도록 했다.

(3) LoginRequestDto

@Getter
@NoArgsConstructor
public class LoginRequestDto {
    private String username;
    private String password;
}
  • 로그인에는 username, password 이 필요하다

responseDto

(1) BoardResponseDto

@Getter
@JsonInclude(JsonInclude.Include.NON_NULL)
public class BoardResponseDto {
    private Long id;
    private String title;
    private String username;
    private String contents;
    private LocalDateTime createdAt;
    private LocalDateTime modifiedAt;

    // 생성자
    public BoardResponseDto(Board board) {
        this.id = board.getId();
        this.title = board.getTitle();
        this.username = board.getUsername();
        this.contents = board.getContents();
        this.createdAt = board.getCreatedAt();
        this.modifiedAt = board.getModifiedAt();
    }
}
  • request 와 달리, 게시글을 작성 후 조회할 때는 id, 제목, 작성자명, 작성내용, 생성/수정시간을 표시하도록 했다.

(2) MsgResponseDto

방법 1 : @AllArgsConstructor 사용

@Getter
@AllArgsConstructor
public class MsgResponseDto {
    private String msg;
    private int statusCode;
}

@AllArgsConstructor

  • 모든 필드값을 파라미터(인수)로 받는 생성자를 만들어준다.
  • 이 어노테이션을 사용하기보다는 build 패턴 or 정적 팩토리 메소드를 사용하는 것이 코드 가동성에 더 좋다.
  • 참고: 생성자

방법 2 : 생성자를 직접 작성

@Getter
public class MsgResponseDto {
    private String msg;
    private int statusCode;

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

3) Service

  • 게시글 전체/선택 조회는 Lv1 과 동일하다

BoardService

(1)

ublic class BoardService {
    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final JwtUtil jwtUtil;
    
    ...

}    
  • BoardRepository, UserRepository, JwtUtil 를 주입 받았다.

(2)

	// 게시글 작성
    public BoardResponseDto createBoard(BoardRequestDto requestDto, HttpServletRequest request) {
        User user = getUserFromToken(request);			// getUserFromToken 메서드를 호출

        // 여기 3줄은 입문 에서의 '게시글 작성'과 동일
        Board board = new Board(requestDto, user);
        Board saveBoard = boardRepository.save(board);

        return new BoardResponseDto(saveBoard);
    }

HttpServletRequest

  • 헤더(Header)에 담긴 jwt 토큰을 담고있는 객체
    • 즉, 로그인한 사용자의 정보다.

(3)

	// 게시글 수정
    @Transactional
    public BoardResponseDto updateBoard(Long id, BoardRequestDto requestDto, HttpServletRequest request) {
        User user = getUserFromToken(request);			// getUserFromToken 메서드를 호출

        Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                () -> new IllegalArgumentException("해당 사용자의 게시글을 찾을 수 없습니다.")
        );

        board.update(requestDto);

        return new BoardResponseDto(board);
    }

(4)

	// 게시글 삭제
    public MsgResponseDto deleteBoard(Long id, HttpServletRequest request) {
        User user = getUserFromToken(request);			// getUserFromToken 메서드를 호출

        Board board = boardRepository.findByIdAndUserId(id, user.getId()).orElseThrow(
                () -> new IllegalArgumentException("해당 사용자의 게시글을 찾을 수 없습니다.")
        );

        boardRepository.delete(board);

        return new MsgResponseDto("게시글을 삭제했습니다.", HttpStatus.OK.value());
    }

(5) 작성, 수정, 삭제에 공통 적용되는 메서드

private User getUserFromToken(HttpServletRequest request) {
        String token = jwtUtil.getJwtFromHeader(request);       // request 에서 Token 가져오기
        Claims claims;          // JWT 안에 있는 정보를 담는 Claims 객체

        if (StringUtils.hasText(token)) {        // JWT 토큰 있는지 확인
            if (jwtUtil.validateToken(token)) {     // JWT 토큰 검증
                claims = jwtUtil.getUserInfoFromToken(token);       // ture 일 경우, JWT 토큰에서 사용자 정보 가져오기
            } else {
                throw new IllegalArgumentException("올바른 token 이 아닙니다.");
            }

            // 검증된 JWT 토큰에서 사용자 정보 조회 및 가져오기
            return userRepository.findByUsername(claims.getSubject()).orElseThrow(
                    () -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다.")
            );
        }

        throw new IllegalArgumentException("token 이 없습니다.");
    }
  • jwt 토큰을 가져와 검증하고, 사용자 정보를 조회하는 부분이 공통된다.
    • 해당 부분을 메서드로 묶어 코드 중복을 줄였다.

StringUtils.hasText(token)

  • StringUtils.hasText(값)

    • 값이 있을 경우에는 true를 반환, 공백이나 NULL 이 들어올 경우에는 false를 반환
  • token != null 으로 token 에 담긴 값의 NULL 유무를 확인해도 무방하다

UserService

(1)

@Service
@Validated
@RequiredArgsConstructor
public class UserService {
    private final UserRepository userRepository;
    
    ...

}

@Validated

  • 클래스(UserService)에 @Validated 를 붙이고,
    유효성을 검증할 메소드의 파라미터(UserController 에서 signup 메서드 안의 signupRequestDto)에 @Valid 를 붙인다.
  • 참고: @Valid(+ 정규 표현식), @Validated

(2)

	// 회원 가입
    public void signup(SignupRequestDto requestDto) {
        String username = requestDto.getUsername();		// requestDto 에서 사용자 이름을 조회
        String password = requestDto.getPassword();		// requestDto 에서 비밀번호를 조회

        // 회원 중복 확인
        Optional<User> checkUsername = userRepository.findByUsername(username);		// 조회한 이름이 DB 에 있는지 확인
        if (checkUsername.isPresent()) {
            throw new IllegalArgumentException("중복된 사용자가 존재합니다.");
        }

        // 조회한 이름이 DB 에 없을 경우, 사용자 등록 (admin = false 일 경우 아래 코드 수행)
        User user = new User(username, password);
        userRepository.save(user);      // DB 에 저장
    }

(3)

	// 로그인
    public void login(LoginRequestDto loginRequestDto, HttpServletResponse response) {
        String username = loginRequestDto.getUsername();
        String password = loginRequestDto.getPassword();

        User user = userRepository.findByUsername(username).orElseThrow(
                () -> new IllegalArgumentException("해당 사용자를 찾을 수 없습니다.")
        );

        // 비밀번호 일치 여부 확인
        if (!user.getPassword().equals(password)) {
            throw new IllegalArgumentException("비밀번호가 일치하지 않습니다.");
        }

        response.addHeader(JwtUtil.AUTHORIZATION_HEADER, JwtUtil.createToken(user.getUsername()));
    }

response.addHeader(JwtUtil.AUTHORIZATION_HEADER, JwtUtil.createToken(user.getUsername()))

  • Header key 값 과 사용자 이름이 들어가서 만들어진 Token 을 Header 에 담는다.
    • JwtUtil.AUTHORIZATION_HEADER : Header key 값

4) Repository

BoardRepository

public interface BoardRepository extends JpaRepository<Board, Long> {
    List<Board> findAllByOrderByCreatedAtDesc();
    Optional<Board> findByIdAndUserId(Long id, Long userId);
}

findAllByOrderByCreatedAtDesc

  • 게시글 전체 조회 시, 작성 기준으로 내림차순 정렬

findByIdAndUserId

  • 게시글 수정 및 삭제 시, 게시글의 id와 사용자의 id 일치 여부를 비교하기 위함

UserRepository

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
}
  • 회원가입과 로그인에서 회원 중복 확인, JWT 토큰에 담긴 사용자 정보를 조회할 경우에 사용

5) Entity

@Getter, @NoArgsConstructor 를 사용한 이유에 대해서는 아래에 정리해두었다.
참고: Dto 에서의 어노테이션 언제 사용할까

Board

(1)

@Entity
@Getter
@Table(name = "board")
@NoArgsConstructor
public class Board extends Timestamped {
    ...
}
  • @Getter, @NoArgsConstructor 를 사용했다. (User 엔티티에서도 동일하다. 이하 생략)

(2)

public class Board extends Timestamped {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "title", nullable = false)
    private String title;

    @Column(name = "username", nullable = false)
    private String username;

    @Column(name = "contents", nullable = false, length = 500)
    private String contents;

    // board : user = N : 1 다대일 단방향 연관관계
    @ManyToOne(fetch = FetchType.LAZY)      // 
    @JoinColumn(name = "user_id", nullable = false)
    private User user;
    
    ...

}

@ManyToOne(fetch = FetchType.LAZY)

(3)

	...

    // 게시글 작성
    public Board(BoardRequestDto requestDto, User user) {
        this.title = requestDto.getTitle();
        this.username = user.getUsername();
        this.contents = requestDto.getContents();
        this.user = user;
    }

    // 게시글 수정
    public void update(BoardRequestDto requestDto) {
        this.title = requestDto.getTitle();
        this.contents = requestDto.getContents();           
    }
}

this.username = user.getUsername()

  • this.user = user 에서 사용자의 정보를 가져왔으나,
    게시글 작성 후, 반환되는 정보에 작성자명을 포함시켰어야 했다.
    • 해당 필드에 작성자명을 넣어야만 게시글 조회 시에 작성자명을 확인 할 수 있다.

this.user = user

  • 게시글 작성에서 : 사용자가 게시글을 작성하기 위해서는 해당 유저의 정보가 필요하다
  • 게시글 수정에서 : 수정에서는 불필요하다 -> 서비스단에서 DB 에 저장된 유저 정보를 조회한 후, 수정이 이뤄지기 때문

User

(1)

public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true)        // unique = true : username 중복된 값 받지 않기 위해
    @Size(min = 4,max = 10, message ="작성자명은 4자 이상 10자 이하만 가능합니다.")
    @Pattern(regexp = "^[a-z0-9]*$", message = "작성자명은 알파벳 소문자, 숫자만 사용 가능합니다.")      // 정규식
    private String username;

    @Column(nullable = false)
    @Size(min = 8,max = 15, message ="비밀번호는 8자 이상 15자 이하만 가능합니다.")
    @Pattern(regexp = "^[a-zA-Z_0-9]*$", message = "비밀번호는 알파벳 대소문자, 숫자만 사용 가능합니다.")
    private String password;
    
    ...

}
  • username 과 password 에 제약조건을 걸어,
    회원가입 시 입력가능한 문자에 제한을 뒀다.

(2)

	...

    public User(String username, String password) {
        this.username = username;
        this.password = password;
    }
}
  • 사용자를 통해 필드값에 데이터를 넣어준다

6) Timestamped

(1) Timestamped

Lv1 과 동일하다

(2) SpringProjectApplication

Lv1 과 동일하다

7) JwtUtil

JwtUtil 클래스를 통해 쿠키를 직접 만들 수 있다.

(1)

@Slf4j(topic = "JwtUtil")
@Component
public class JwtUtil {
	...

}

@Slf4j(topic = "JwtUtil")

  • @Slf4j

    • 다양한 로깅 프레임 워크에 대한 추상화를 수행
    • 최종 사용하는 SLF4J를 통해 로그를 남기면 되고, 이 경우 원하는 로깅 프레임워크에 연결하여 로그 기능을 수행
    • 추후 로깅 라이브러리를 교체하더라도, 설정 파일 외의 코드를 건드리지 않아도 된다.
    • Lombok 에서 SLF4J 어노테이션을 제공해준다
  • topic

    • 로그 생성 시 부여되는 카테고리를 나타낸다.
    • 로그 분류 등을 위해 사용

(2) JWT 데이터 준비하기

    public static final String AUTHORIZATION_HEADER = "Authorization";      // Header KEY 값
    public static final String AUTHORIZATION_KEY = "auth";      // 사용자 권한 값의 KEY
    public static final String BEARER_PREFIX = "Bearer ";       // Token 식별자
    private static final long TOKEN_TIME = 60 * 60 * 1000L;        // 토큰 만료시간 : 60분     // static 을 추가함

    @Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey (application.properties 에 추가해둔 값)
    private String secretKey;       // 그 값을 가져와서 secretKey 변수에 넣는다
    private static Key key;        // Secret key 를 담을 변수        // static 을 추가함
    private static final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;     // 사용할 알고리즘 선택        // static 을 추가함

    @PostConstruct      // 한 번만 받으면 값을 사용할 때마다, 매번 요청을 새로 호출하는 것을 방지
    public void init() {
        byte[] bytes = Base64.getDecoder().decode(secretKey);       // Base64 를 디코딩
        key = Keys.hmacShaKeyFor(bytes);
    }

staic final

(3) JWT 토큰 생성

    public static String createToken(String username) {
        Date date = new Date();

        // 암호화
        return BEARER_PREFIX +
                Jwts.builder()
                        .setSubject(username)               // 사용자 식별자값(ID). 여기에선 username 을 넣음
                        .claim(AUTHORIZATION_KEY, username)     // 사용자 권한 (key, value)
                        .setExpiration(new Date(date.getTime() + TOKEN_TIME))   // 만료 시간 : 현재시간 date.getTime() + 위에서 지정한 토큰 만료시간(60분)
                        .setIssuedAt(date)                  // 발급일
                        .signWith(key, signatureAlgorithm)  // 암호화 알고리즘 (Secret key, 사용할 알고리즘 종류)
                        .compact();
    }

(4) header 에서 JWT 가져오기

    public String getJwtFromHeader(HttpServletRequest request) {
        String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
            return bearerToken.substring(7);
        }
        
        return null;
    }

(5) JWT 토큰 검증

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
            return true;
        } catch (SecurityException | MalformedJwtException e) {
            log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
        } catch (ExpiredJwtException e) {
            log.error("Expired JWT token, 만료된 JWT token 입니다.");
        } catch (UnsupportedJwtException e) {
            log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
        } catch (IllegalArgumentException e) {
            log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
        }
        return false;
    }
  • 토큰의 만료, 위/변조 를 검증

(6) JWT 토큰에서 사용자 정보 가져오기

    public Claims getUserInfoFromToken(String token) {
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();     // body 에 있는 claims 를 가져온다
    }
profile
개발자로 거듭나기!

0개의 댓글