Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[4Week_Mission] 한승연 - Jwt-Security 로그인 구현, REST API 개발, SpringDoc #38

Merged
merged 39 commits into from
Nov 18, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
d31c078
#34 - Test: 회원 로그인 POST 요청(JSON 방식) 컨트롤러 테스트 추가
ahah525 Nov 8, 2022
295851c
#33 - Feat: LoginDto 추가, 로그인 POST 요청(JSON) 구현(임시)
ahah525 Nov 8, 2022
60aa1ce
#34 - Test: 올바른 POST /api/v1/member/login 요청의 응답 헤더에 Authentication 값…
ahah525 Nov 8, 2022
96bf825
#33 - Feat: HttpServletResponse 헤더에 임시 Authentication 값을 담아 응답 보내기
ahah525 Nov 8, 2022
058a935
#33 - Feat: ResponseEntity 로 응답 헤더, 바디, 상태코드 설정하여 응답 보내는 것으로 수정
ahah525 Nov 8, 2022
a06a89d
#33 - Feat: 로그인 요청시, 입력 데이터(username, password) 유효성 검증
ahah525 Nov 8, 2022
fc972c3
#34 - Test: 로그인 요청시 username/password 입력데이터 유효성 검증 실패하면 400(BadReques…
ahah525 Nov 8, 2022
330e0f3
#34 - Test: 존재하지 않는 회원(username, password)에 대한 로그인 요청시 400(BadRequest…
ahah525 Nov 8, 2022
53ea4b8
#33 - Feat: 존재하지 않는 회원, 올바르지 않은 비밀번호로 로그인 요청시 400(BadRequest) 응답
ahah525 Nov 8, 2022
7e4ffe9
#2 - Feat: Util.spring.responseEntityOf() 도입
ahah525 Nov 8, 2022
0dd7bab
#2 - Feat: RsData(REST API 용 ResponseEntity Body DTO) 도입
ahah525 Nov 8, 2022
4c258b7
#33 - Feat: 로그인 요청 응답 body에 resultCode, msg, data가 포함되도록 RsData 적용(Re…
ahah525 Nov 8, 2022
886bdf2
#2 - Feat: Ut.mapOf() 도입
ahah525 Nov 8, 2022
8523f92
#35 - Test: Ut.mapOf() 테스트
ahah525 Nov 8, 2022
c26d357
#2 - Feat: 들어온 인자들을 (key, value) 쌍으로 HttpHeaders 에 담아 반환하는 Ut.spring.…
ahah525 Nov 8, 2022
1e1c454
#33 - Feat: 로그인 성공시 응답 헤더, 바디에 모두 accessToken 담아 응답보내기
ahah525 Nov 8, 2022
cef4d55
#36 - Chore: JWT dependency 추가
ahah525 Nov 8, 2022
260b27e
#2 #36 - Feat: Ut.json 도입(Json<->String: 직렬화, 역직렬화 메서드 추가)
ahah525 Nov 8, 2022
0e92a77
#36 - Feat: JwtConfig(JWT SecretKey 관리), JwtProvider(JWT 토큰 생성/검증) 추가
ahah525 Nov 8, 2022
45af459
#33 - Feat: 로그인 성공시 회원 정보를 바탕으로 발급한 accessToken 을 응답 헤더, 바디에 담아 보내기
ahah525 Nov 8, 2022
11034d5
Feat: security 관련 패키지 이동
ahah525 Nov 8, 2022
976e7dc
#36 - Feat: REST API 요청에 대한 ApiSecurityConfig 추가, 인증/인가 수행하는 JwtAutho…
ahah525 Nov 8, 2022
b2e8bfc
#2 - Feat: 성공/실패 디폴트 응답(resultCode, msg 디폴트 값 설정) 메서드 추가
ahah525 Nov 8, 2022
24e0b94
#33 - Feat: GET /api/v1/member/me (로그인한 회원정보 조회) 요청 구현, response Memb…
ahah525 Nov 8, 2022
c23e7a8
#33 - Feat: GET /api/v1/myBooks (내 도서 리스트 조회) 구현, response.MyBookDto,…
ahah525 Nov 8, 2022
033f34e
#33 - Feat: GET /api/v1/myBooks/{myBookId} (내 도서 상세조회 요청) 구현
ahah525 Nov 8, 2022
9c18dbe
#33 - Fix: 리액트 로그인 요청보냈을 때, 응답바디의 accessToken null 문제 해결
ahah525 Nov 8, 2022
50fe75f
#33 - spring doc 의존성 추가, SpringDocConfig 추가, spring doc 관리자 회원만 접근가능하…
ahah525 Nov 8, 2022
cc25bf4
#31 - Docs: 4Week_Record 작성
ahah525 Nov 9, 2022
7b5090d
#31 - Docs: 4Week_Record 작성
ahah525 Nov 9, 2022
9ea8927
#37 - Feat: JWT AccessToken 화이트 리스트 방식 적용(2차 화이트리스트 검증)
ahah525 Nov 9, 2022
30980c6
#31 - Docs: 4Week_Record 작성
ahah525 Nov 9, 2022
67d55ee
#39 - Fix: 상품과 관련된 글을 조회할 때 해당 키워드와 관련된 다른 작가의 글까지 조회되는 문제 해결
ahah525 Nov 10, 2022
ad7198d
#39 - Fix: 내도서만 상세 정보조회 가능하도록 변경
ahah525 Nov 10, 2022
b6b1040
#40 - Refactor: jwt 관련 코드 리팩토링
ahah525 Nov 10, 2022
1ccb91f
#31 - Docs: 4Week_Record 작성
ahah525 Nov 10, 2022
2b18a0a
#31 - Docs: 4Week_Record 작성
ahah525 Nov 10, 2022
588b51b
#41 - Docs: README.md 작성
ahah525 Nov 17, 2022
d6c753e
Feat: 2차 프로젝트 개발을 위한 세팅
ahah525 Nov 17, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions 4Week_Mission/mutbooks/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,14 @@ dependencies {
testImplementation 'org.springframework.batch:spring-batch-test'
// apache httpclient
implementation 'org.apache.httpcomponents:httpclient:4.5'

// jwt
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// spring doc
implementation 'org.springdoc:springdoc-openapi-ui:1.6.11'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public static boolean isTest() {

@Bean
public ObjectMapper objectMapper() {
// LocalDateTime 관련 직렬화/역직렬화 오류시, new JavaTimeModule() 추가
return new ObjectMapper().registerModule(new JavaTimeModule());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package com.example.mutbooks.app.api.member.controller;

import com.example.mutbooks.app.api.member.dto.response.MemberDto;
import com.example.mutbooks.app.base.dto.RsData;
import com.example.mutbooks.app.api.member.dto.request.LoginDto;
import com.example.mutbooks.app.member.entity.Member;
import com.example.mutbooks.app.member.service.MemberService;
import com.example.mutbooks.app.security.dto.MemberContext;
import com.example.mutbooks.util.Ut;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/v1/member")
@RequiredArgsConstructor
@Slf4j
public class MemberApiController {
private final MemberService memberService;
private final PasswordEncoder passwordEncoder;

@PostMapping("/login")
public ResponseEntity<RsData> login(@RequestBody LoginDto loginDto) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

많은 예외 사항에 대해서 고려해서 처리하신 거 잘하신 것 같습니다.!
하지만 만약 다른 컨트롤러에서는 여기보다 더 많은 예외를 처리할 수도 있을 것 같은데, 이럴 경우는 예외를 공통으로 처리하는 RestControllerAdvice를 사용하는 것이 좋아보입니다. RestControllerAdvice는 예외가 발생했을 때 처리하는 것이므로 컨트롤러에서 예외를 검증하지 않고, 서비스단에서 예외가 발생하였을 때 예외를 던지면 RestControllerAdvice가 처리해주기 때문에 컨트롤러 단이 많이 깔끔해질 수 있을 것 같습니다!

log.info("로그인");
// 입력 데이터 유효성 검증
if(loginDto.isNotValid()) {
return Ut.spring.responseEntityOf(RsData.of("F-1", "로그인 정보가 올바르지 않습니다.."));
}

Member member = memberService.findByUsername(loginDto.getUsername());
// 1. 존재하지 않는 회원
if(member == null) {
log.info("존재하지 않는 회원");
return Ut.spring.responseEntityOf(RsData.of("F-2", "일치하는 회원이 존재하지 않습니다."));
}
// 2. 올바르지 않은 비밀번호
// matches(비밀번호 원문, 암호화된 비밀번호)
if(!passwordEncoder.matches(loginDto.getPassword(), member.getPassword())) {
log.info("비밀번호 틀림");
return Ut.spring.responseEntityOf(RsData.of("F-3", "비밀번호가 일치하지 않습니다."));
}

log.debug("Ut.json.toStr(member.getAccessTokenClaims()) : " + Ut.json.toStr(member.getAccessTokenClaims()));
// accessToken 발급
String accessToken = memberService.genAccessToken(member);
// 응답 헤더, 바디에 accessToken 담기
return Ut.spring.responseEntityOf(
RsData.of(
"S-1",
"로그인 성공, Access Token을 발급합니다.",
Ut.mapOf("accessToken", accessToken)
),
Ut.spring.httpHeadersOf("Authentication", accessToken)
);
}

// 회원 정보
@GetMapping("/me")
public ResponseEntity<RsData> test(@AuthenticationPrincipal MemberContext memberContext) {
if(memberContext == null) {
return Ut.spring.responseEntityOf(RsData.failOf(null));
}
MemberDto memberDto = MemberDto.toDto(memberContext.getMember());

return Ut.spring.responseEntityOf(RsData.successOf(Ut.mapOf("member", memberDto)));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.example.mutbooks.app.api.member.dto.request;

import lombok.Data;

import javax.validation.constraints.NotBlank;

@Data
public class LoginDto {
@NotBlank(message = "username 을(를) 입력해주세요.")
private String username;
@NotBlank(message = "password 을(를) 입력해주세요.")
private String password;

public boolean isNotValid() {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

제가 알기론 dto에는 Getter, Setter를 제외하고 다른 로직이 포함되면 안되는 것로 알고 있습니다.
다만, 안되는 것은 아니지만 dto는 Getter, Setter만 가지고 있는 순수 객체로 만드는 것을 권장하는 걸로 알고 있습니다!

return username == null || password == null || username.trim().length() == 0 || password.trim().length() == 0;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.example.mutbooks.app.api.member.dto.response;

import com.example.mutbooks.app.member.entity.Member;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;

@Getter
@Setter
@Builder
@AllArgsConstructor
public class MemberDto {
private Long id;
private LocalDateTime createDate;
private LocalDateTime modifyDate;
private String username;
private String email;
private boolean emailVerified;
private String nickname;

public static MemberDto toDto(Member member) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 static 메서드를 통해서 엔티티와 Dto 매핑을 잘 해주신 것 같습니다!

return MemberDto.builder()
.id(member.getId())
.createDate(member.getCreateDate())
.modifyDate(member.getUpdateDate())
.username(member.getUsername())
.email(member.getEmail())
.nickname(member.getNickname())
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.example.mutbooks.app.api.myBooks;

import com.example.mutbooks.app.base.dto.RsData;
import com.example.mutbooks.app.member.entity.Member;
import com.example.mutbooks.app.mybook.dto.response.MyBookDetailDto;
import com.example.mutbooks.app.mybook.dto.response.MyBookDto;
import com.example.mutbooks.app.mybook.service.MyBookService;
import com.example.mutbooks.app.security.dto.MemberContext;
import com.example.mutbooks.util.Ut;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("/api/v1/myBooks")
@RequiredArgsConstructor
public class MyBooksApiController {
private final MyBookService myBookService;

// 내 도서 리스트
@GetMapping("")
public ResponseEntity<RsData> list(@AuthenticationPrincipal MemberContext memberContext) {
Member member = memberContext.getMember();
List<MyBookDto> myBookDtos = myBookService.findAllByOwner(member);

return Ut.spring.responseEntityOf(
RsData.successOf(Ut.mapOf("myBooks", myBookDtos))
);
}

// 도서 상세 조회
@GetMapping("/{myBookId}")
public ResponseEntity<RsData> detail(@PathVariable long myBookId, @AuthenticationPrincipal MemberContext memberContext) {
MyBookDetailDto myBookDetailDto = myBookService.findByIdForDetail(myBookId, memberContext.getId());

return Ut.spring.responseEntityOf(
RsData.successOf(Ut.mapOf("myBook", myBookDetailDto))
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.mutbooks.app.base;

import io.swagger.v3.oas.models.ExternalDocumentation;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.info.License;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SpringDocConfig {
@Bean
public OpenAPI springShopOpenAPI() {
return new OpenAPI()
.info(new Info().title("SpringShop API")
.description("Spring shop sample application")
.version("v0.0.1")
.license(new License().name("Apache 2.0").url("http://springdoc.org")))
.externalDocs(new ExternalDocumentation()
.description("SpringShop Wiki Documentation")
.url("https://springshop.wiki.github.org/docs"));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.example.mutbooks.app.base.dto;

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

@Getter
@Setter
@AllArgsConstructor
public class RsData<T> {
private String resultCode;
private String msg;
private T data;

public static <T> RsData<T> of(String resultCode, String msg, T data) {
return new RsData<>(resultCode, msg, data);
}

public static <T> RsData<T> of(String resultCode, String msg) {
return of(resultCode, msg, null);
}

// 성공 응답
public static <T> RsData<T> successOf(T data) {
return of("S-1", "성공", data);
}

// 실패 응답
public static <T> RsData<T> failOf(T data) {
return of("F-1", "실패", data);
}

public boolean isSuccess() {
return resultCode.startsWith("S-1");
}

public boolean isFail() {
return isSuccess() == false;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,19 @@
import com.example.mutbooks.app.order.service.OrderService;
import com.example.mutbooks.app.post.service.PostService;
import com.example.mutbooks.app.product.service.ProductService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;

@Configuration
@Profile("test")
@Slf4j
public class TestInitData implements InitDataBefore {
// initData 실행 여부(2번 생성되는 것을 막기 위함)
private boolean initDataDone = false;

@Bean
CommandLineRunner initData(
MemberService memberService,
Expand All @@ -22,6 +27,11 @@ CommandLineRunner initData(
OrderService orderService
) {
return args -> {
if(initDataDone) return;
initDataDone = true;

log.info("TestInitData 실행");

before(memberService, postService, productService, cartService, orderService);
};
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.mutbooks.app.cart.controller;

import com.example.mutbooks.app.base.security.dto.MemberContext;
import com.example.mutbooks.app.security.dto.MemberContext;
import com.example.mutbooks.app.cart.entity.CartItem;
import com.example.mutbooks.app.cart.service.CartService;
import com.example.mutbooks.app.member.entity.Member;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.example.mutbooks.app.member.controller;

import com.example.mutbooks.app.base.security.dto.MemberContext;
import com.example.mutbooks.app.security.dto.MemberContext;
import com.example.mutbooks.app.mail.service.MailService;
import com.example.mutbooks.app.member.entity.Member;
import com.example.mutbooks.app.member.form.JoinForm;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.mutbooks.app.member.entity;

import com.example.mutbooks.app.base.entity.BaseEntity;
import com.example.mutbooks.util.Ut;
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.*;
import lombok.experimental.SuperBuilder;
Expand All @@ -11,6 +12,7 @@
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

@Entity
@Getter
Expand All @@ -33,6 +35,10 @@ public class Member extends BaseEntity {

private int restCash; // 예치금

// accessToken
@Column(columnDefinition = "TEXT")
private String accessToken;

// Member 의 memberExtra 에 값이 저장될 때, MemberExtra 도 같이 저장되도록
@OneToOne(mappedBy = "member", cascade = CascadeType.ALL)
private MemberExtra memberExtra;
Expand Down Expand Up @@ -62,7 +68,7 @@ public boolean hasBankInfo() {
}

// 권한 부여
public List<GrantedAuthority> genAuthorities() {
public List<GrantedAuthority> getAuthorities() {
List<GrantedAuthority> authorities = new ArrayList<>();
// 모든 로그인한 회원에게는 USER 권한 부여
authorities.add(new SimpleGrantedAuthority(AuthLevel.USER.getValue())); // 일반 회원
Expand All @@ -79,4 +85,16 @@ public List<GrantedAuthority> genAuthorities() {

return authorities;
}

// AccessToken 발급을 위해 회원 정보를 바탕으로 claim map 객체 만들어 반환
public Map<String, Object> getAccessTokenClaims() {
return Ut.mapOf(
"id", getId(),
"createDate", getCreateDate(),
"updateDate", getUpdateDate(),
"username", getUsername(),
"email", getEmail(),
"authorities", getAuthorities()
);
}
}
Loading