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

9조 BE 코드리뷰 2회차 #36

Merged
merged 54 commits into from
Oct 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
35bcf42
style:[#22]-format
yooonwodyd Sep 23, 2024
233084a
style:[#22]-Add
yooonwodyd Sep 23, 2024
adcd15e
style:[#22]-Add
yooonwodyd Sep 23, 2024
60d69c0
feat:[#22]-Add JWT Token
yooonwodyd Sep 23, 2024
75bbfea
feat:[#22]-Add JwtProvider
yooonwodyd Sep 23, 2024
817e4ef
feat:[#22]-Add Error Handler
yooonwodyd Sep 23, 2024
203326b
feat:[#22]-Add JwtFilter
yooonwodyd Sep 23, 2024
d12f481
fix:[#22]- ChangeTableName
yooonwodyd Sep 23, 2024
69122c9
feat:[#22]- add UserRepository
yooonwodyd Sep 23, 2024
4ec6419
feat:[#22]- add WebSecurityConfig
yooonwodyd Sep 23, 2024
7b2c575
feat:[#22]- add LoginController
yooonwodyd Sep 23, 2024
96fbb00
establish:[#22]- change gitignore
yooonwodyd Sep 23, 2024
5cc28b6
establish:[#22]- change gitignore
yooonwodyd Sep 23, 2024
699df05
feat: [#26] 해시태그, 카테고리 ENUM 항목 추가
jupyter471 Sep 24, 2024
aa2dd0b
feat: [#25] String -> ENUM 변환코드
jupyter471 Sep 25, 2024
4db141d
feat: [#25] product 등록기능
jupyter471 Sep 25, 2024
e598d57
Merge pull request #23 from yooonwodyd/weekly
yooonwodyd Sep 25, 2024
b9fa473
feat: [#25] product 개별 상세 조회 기능
jupyter471 Sep 25, 2024
3359119
test:[#22]- Add data.sql
yooonwodyd Sep 27, 2024
6f7a3ab
feat:[#24]- Add BusinessArtist
yooonwodyd Sep 27, 2024
c1c0608
feat:[#24]- Add LocalDateTime
yooonwodyd Sep 27, 2024
d374059
feat:[#24]- Add FetchType
yooonwodyd Sep 27, 2024
3b4ada5
feat:[#24]- Add repository
yooonwodyd Sep 27, 2024
3b8dcac
feat: [#25] product 상세정보 수정
jupyter471 Sep 29, 2024
b1f875d
feat: [#25] product 삭제
jupyter471 Sep 29, 2024
a1b891b
feat: [kakao-tech-campus-2nd-step3#31] Grafana, Prometheus 의존성 추가
donghyuun Sep 30, 2024
cc9709d
feat: [kakao-tech-campus-2nd-step3#31] 모니터링 엔드포인트 시큐리티 적용 해제
donghyuun Sep 30, 2024
2cd742a
Merge pull request #29 from jupyter471/jupyter471
yooonwodyd Sep 30, 2024
974c807
Merge branch 'kakao-tech-campus-2nd-step3:weekly' into weekly
yooonwodyd Sep 30, 2024
efdac3a
Merge pull request #32 from donghyuun/weekly
yooonwodyd Oct 4, 2024
f5b0587
Merge branch 'kakao-tech-campus-2nd-step3:weekly' into weekly
yooonwodyd Oct 4, 2024
a479f51
feat:[#24]- Add findById
yooonwodyd Oct 4, 2024
c4129bd
feat:[#24]- Add EntityListeners
yooonwodyd Oct 4, 2024
77c60cd
refactor:[#24]- refact data.sql
yooonwodyd Oct 4, 2024
c49e395
feat:[#24]- add CascadeType.ALL
yooonwodyd Oct 4, 2024
bf79893
refactor:[#24]- refact testUser
yooonwodyd Oct 4, 2024
5595140
feat:[#22]- add authorities
yooonwodyd Oct 4, 2024
5f0c1d1
feat:[#22]- add ArtistInfo
yooonwodyd Oct 4, 2024
9c785a8
feat:[#24]- add userDto
yooonwodyd Oct 4, 2024
c5054d8
chore:[#24]- add security test
yooonwodyd Oct 4, 2024
3357746
feat:[#24]- add data.sql
yooonwodyd Oct 4, 2024
e3d7758
feat:[#24]- add test yaml
yooonwodyd Oct 4, 2024
2183a14
feat:[#24]- add jwtTestUtils
yooonwodyd Oct 4, 2024
b1f5377
feat:[#24]- add UserController
yooonwodyd Oct 4, 2024
da981c8
feat:[#24]- add ArtistService
yooonwodyd Oct 4, 2024
c402de3
feat:[#24]- add StudentArtistRepository
yooonwodyd Oct 4, 2024
856f034
feat:[#24]- add ArtistController
yooonwodyd Oct 4, 2024
a2725c7
feat:[#24]- add UserService
yooonwodyd Oct 4, 2024
e4ee47b
feat:[#24]- add LoginE2ETest
yooonwodyd Oct 4, 2024
21cf8a0
feat:[#24]- add ArtistE2Etest
yooonwodyd Oct 4, 2024
0df7e23
Merge pull request #33 from yooonwodyd/weekly
yooonwodyd Oct 4, 2024
f788326
Merge pull request #34 from kakao-tech-campus-2nd-step3/weekly
yooonwodyd Oct 4, 2024
a6c27e1
Merge pull request #35 from kakao-tech-campus-2nd-step3/develop
yooonwodyd Oct 4, 2024
0eace57
Update README.md
yooonwodyd Oct 4, 2024
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
HELP.md
.gradle

build/
!gradle/wrapper/gradle-wrapper.jar
!**/src/main/**/build/
!**/src/main/resource/application.yaml
!**/src/test/**/build/

### STS ###
Expand Down
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,15 @@

## 리뷰사항
연휴 이후 다음주 부터 본격적인 개발에 들어가려 합니다. 열심히하겠습니다! 감사합니다.



# 5주차 github 코드리뷰 질문
(윤재용)
몇몇 컨트롤러에 대한 E2E 테스트를 작성하였습니다.
처음에는 @WithMockUser 를 사용해서 테스트를 진행하려고 했는데, Header를 검증하다보니 불가능했습니다.
저희의 요구사항이 특정 url이 아니라면 헤더에 토큰이 필요하다보니 사용이 불가능하였기에 JwtTestUtils 클래스를 통해 테스트 유저를 사용하였습니다.

이런 전체 테스트를 처음 구현하다 보니
현재 작성한 테스트가 E2E 테스트라고 불려도 될 지 잘 모르겠습니다..!
추가로 테스트코드의 개선점이 있을지 궁금합니다.
37 changes: 32 additions & 5 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
buildscript {
ext {
spring_boot_version = '3.3.3'
spring_dependency_management = '1.1.6'
}

repositories {
mavenCentral()
}
}

plugins {
id 'java'
id 'org.springframework.boot' version '3.3.3'
id 'io.spring.dependency-management' version '1.1.6'
id 'org.springframework.boot' version "${spring_boot_version}"
id 'io.spring.dependency-management' version "${spring_dependency_management}"
}

group = 'com.helpmeCookies'
Expand All @@ -24,17 +35,33 @@ repositories {
}

dependencies {

// Spring
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-security'

// Lombok
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'

// DB
runtimeOnly 'com.mysql:mysql-connector-j'
runtimeOnly 'com.h2database:h2'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// Spring docs
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0'

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

// Monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
implementation 'io.micrometer:micrometer-registry-prometheus'
}

tasks.named('test') {
Expand Down
9 changes: 9 additions & 0 deletions src/main/java/com/helpmeCookies/global/config/JpaConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.helpmeCookies.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
125 changes: 125 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package com.helpmeCookies.global.jwt;

import java.security.Key;
import java.util.Date;

import javax.crypto.spec.SecretKeySpec;

import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

@Component
public class JwtProvider implements InitializingBean {
@Value("${jwt.secret}")
private String secret;

Choose a reason for hiding this comment

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

요렇게 Value로 바인딩 받는것들은 프로퍼티로 묶어서 바인딩 받으면 조금더 코드가 깔끔해질것 같아요

@Value("${jwt.access-token-expire-time}")
private long accessTokenExpireTime;
@Value("${jwt.refresh-token-expire-time}")
private long refreshTokenExpireTime;
private Key secretKey;
private static final String ROLE = "role";
private static final String IS_ACCESS_TOKEN = "isAccessToken";
private static final String HEADER_PREFIX = "Bearer ";

public String parseHeader(String header) {

Choose a reason for hiding this comment

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

parseHeader는 JwtProvider와는 다른 ex HtthHeaderUtils 별도 클래스를 두고 해당 클래스에서 코드를 작성해도 좋을것 같아요.

if (header == null || header.isEmpty()) {
throw new IllegalArgumentException("Authorization 헤더가 없습니다.");
} else if (!header.startsWith(HEADER_PREFIX)) {
throw new IllegalArgumentException("Authorization 올바르지 않습니다.");
} else if (header.split(" ").length != 2) {
throw new IllegalArgumentException("Authorization 올바르지 않습니다.");
}

return header.split(" ")[1];
}

public JwtToken createToken(JwtUser jwtUser) {
String accessToken = generateToken(jwtUser, true);
String refreshToken = generateToken(jwtUser, false);
return JwtToken.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

// 유요한 토큰인지 확인
public boolean validateToken(String rawToken, boolean isAccessToken) {
try {
// 엑세스 토큰인지 확인
Claims claims = extractClaims(rawToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class) != isAccessToken) {
return false;
}
// 만료시간 확인
return !claims.getExpiration().before(new Date());
} catch (Exception e) {
return false;
}
}

/**
* refreshToken을 통해, accessToken을 재발급하는 메서드.
* refreshToken의 유효성을 검사하고, isAccessToken이 true일때만 accessToken을 재발급한다.
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요
*/
public String reissueAccessToken(String refreshToken) {
Claims claims = extractClaims(refreshToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class)) {
throw new IllegalArgumentException("리프레시 토큰이 아닙니다.");
}
JwtUser jwtUser = claimsToJwtUser(claims);
return generateToken(jwtUser, true);
}

/**
* [validateToken] 이후 호출하는 메서드.
* rawToken을 통해 JwtUser를 추출한다.
* [jwtUser]는 userId와 role을 가지고 있다. 즉 JWT에 저장된 정보를 추출한다.
*/
public JwtUser getJwtUser(String rawToken) {
Claims claims = extractClaims(rawToken);
return claimsToJwtUser(claims);
}

private JwtUser claimsToJwtUser(Claims claims) {
String userId = claims.getSubject();
return JwtUser.of(Long.parseLong(userId));
}

/**
* Jwt 토큰생성
* accessToken과 refreshToken의 다른점은 만료시간과, isAccessToken이다.
*/
private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
long expireTime = isAccessToken ? accessTokenExpireTime : refreshTokenExpireTime;
Date expireDate = new Date(System.currentTimeMillis() + expireTime);
return Jwts.builder()
.signWith(secretKey)
.claim(IS_ACCESS_TOKEN, isAccessToken)
.setSubject(jwtUser.getId().toString())
.setExpiration(expireDate)
.compact();
}


private Claims extractClaims(String rawToken) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
.build()
.parseClaimsJws(rawToken)
.getBody();
}

/**
* HS256방식의 키를 생성한다.
*/
@Override
public void afterPropertiesSet() {
secretKey = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
}
}
11 changes: 11 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtToken.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.helpmeCookies.global.jwt;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class JwtToken {
private String accessToken;

Choose a reason for hiding this comment

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

final 키워드를 써도 괜찮지 않을까요?

private String refreshToken;
}
62 changes: 62 additions & 0 deletions src/main/java/com/helpmeCookies/global/jwt/JwtUser.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package com.helpmeCookies.global.jwt;

import java.util.Collection;
import java.util.List;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import lombok.Builder;
import lombok.Getter;

@Builder
@Getter
public class JwtUser implements UserDetails {
private Long id;

Choose a reason for hiding this comment

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

해당 클래스의 필드들도 final 키워드를 붙일수 있을까요?

private String username;
private Collection<? extends GrantedAuthority> authorities;

public static JwtUser of(Long id) {
return JwtUser.builder()
.id(id)
.build();
}

@Override
// 임시 기본 권한을 USER로 설정
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_" + "USER"));
}

@Override
public String getPassword() {
return null;
}

@Override
public String getUsername() {
return this.username;
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.helpmeCookies.global.security;

import java.io.IOException;
import java.io.PrintWriter;

import org.springframework.http.MediaType;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;

@Override
public void handle(HttpServletRequest request, HttpServletResponse response,
AccessDeniedException accessDeniedException) {
log.error("Token : {}", request.getHeader("Authorization"));
// TODO: 에러코드 추가
response.setStatus(403);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com.helpmeCookies.global.security;

import java.io.IOException;

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import com.fasterxml.jackson.databind.ObjectMapper;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@RequiredArgsConstructor
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.debug("Token : {}", request.getHeader("Authorization"));
response.setStatus(401);
}
}
Loading
Loading