Skip to content

Commit

Permalink
Merge pull request #38 from likelion-backendschool/develop
Browse files Browse the repository at this point in the history
[4Week_Mission] 한승연 - Jwt-Security 로그인 구현, REST API 개발, SpringDoc
  • Loading branch information
ahah525 authored Nov 18, 2022
2 parents 1a2cf31 + d6c753e commit 49df4c5
Show file tree
Hide file tree
Showing 205 changed files with 11,276 additions and 23 deletions.
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) {
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() {
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) {
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

0 comments on commit 49df4c5

Please sign in to comment.