Skip to content

Commit

Permalink
Merge pull request #75 from kakao-tech-campus-2nd-step3/Master
Browse files Browse the repository at this point in the history
9조 BE 코드리뷰 4회차
  • Loading branch information
leaf-upper authored Nov 3, 2024
2 parents 5f72a89 + 0005206 commit 63506b1
Show file tree
Hide file tree
Showing 60 changed files with 1,248 additions and 234 deletions.
10 changes: 3 additions & 7 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,10 +66,6 @@ jobs:
key: ${{ secrets.AWS_EC2_PRIVATE_KEY }} # EC2 인스턴스 pem key
port: ${{ secrets.REMOTE_SSH_PORT }} # 접속 포트(생략 시 22번 기본 사용)
script: |
echo '${{ secrets.APPLICATION_YAML }}' > test.yaml
cat test.yaml
docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
docker pull ${{ secrets.DOCKER_USERNAME }}/katecam-backend:latest
docker stop katecam-backend
docker rm $(docker ps --filter 'status=exited' -a -q)
docker run -d --name katecam-backend --network katecam-backend --log-driver=syslog -p 8080:8080 ${{ secrets.DOCKER_USERNAME }}/katecam-backend:latest
cd /home/ubuntu # EC2 인스턴스의 배포 스크립트 파일 경로로 이동
chmod +x deploy.sh # 배포 스크립트 실행 권한 부여
./deploy.sh # 배포 스크립트 실행
8 changes: 8 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.security:spring-security-oauth2-client'

// Lombok
compileOnly 'org.projectlombok:lombok'
Expand Down Expand Up @@ -70,7 +71,14 @@ dependencies {

//S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

//Test
testImplementation 'org.testcontainers:testcontainers:1.20.2'
testImplementation 'org.testcontainers:junit-jupiter:1.20.2'
testImplementation 'org.testcontainers:mysql:1.20.2'

}

tasks.named('test') {
useJUnitPlatform()
}
2 changes: 2 additions & 0 deletions src/main/java/com/helpmeCookies/Step3Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.ConfigurationPropertiesScan;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@SpringBootApplication
public class Step3Application {
Expand Down
21 changes: 21 additions & 0 deletions src/main/java/com/helpmeCookies/global/entity/BaseTimeEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.helpmeCookies.global.entity;

import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
@Getter
public abstract class BaseTimeEntity {

@CreatedDate
private LocalDateTime createdDate;

@LastModifiedDate
private LocalDateTime modifiedDate;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.helpmeCookies.global.infra;

import com.helpmeCookies.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.stereotype.Component;

@Component
@RequiredArgsConstructor
public class CustomDatabaseHealthIndicator implements HealthIndicator {

private final UserRepository userRepository;
@Override
public Health health() {
try {
// 테이블이 존재하는지 확인하는 쿼리 (Users 테이블 사용)
long count = userRepository.count();
// 테이블 존재 시 0 이상을 반환
return Health.up().withDetail("Users table exists, count: ", count).build();
} catch (Exception e) {
// 테이블이 없거나 데이터베이스 연결에 문제가 있는 경우
return Health.down(e).withDetail("Users table", "Table missing or database issue")
.build();
}
}
}
29 changes: 12 additions & 17 deletions src/main/java/com/helpmeCookies/global/jwt/JwtProvider.java
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ public JwtToken createToken(JwtUser jwtUser) {
.build();
}

// 유요한 토큰인지 확인
/*
토큰 검증시 rawToken을 Claims로 변환하고, 해당 토큰이 accessToken이면서 만료되어있지 않다면 True를 반환한다.
*/

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;
Expand All @@ -62,22 +63,24 @@ public boolean validateToken(String rawToken, boolean isAccessToken) {
/**
* refreshToken을 통해, accessToken을 재발급하는 메서드.
* refreshToken의 유효성을 검사하고, isAccessToken이 true일때만 accessToken을 재발급한다.
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요
* TODO: refreshToken을 저장하고, 저장된 refreshToken과 비교하는 로직 필요 redis 추가 후 구현
*/
public String reissueAccessToken(String refreshToken) {
Claims claims = extractClaims(refreshToken);
if (claims.get(IS_ACCESS_TOKEN, Boolean.class)) {
throw new IllegalArgumentException("리프레시 토큰이 아닙니다.");
}

Date expiration = claims.getExpiration();
if (expiration.before(new Date())) {
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);
Expand All @@ -88,10 +91,6 @@ private JwtUser claimsToJwtUser(Claims claims) {
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);
Expand All @@ -103,7 +102,6 @@ private String generateToken(JwtUser jwtUser, boolean isAccessToken) {
.compact();
}


private Claims extractClaims(String rawToken) {
return Jwts.parserBuilder()
.setSigningKey(secretKey)
Expand All @@ -112,9 +110,6 @@ private Claims extractClaims(String rawToken) {
.getBody();
}

/**
* HS256방식의 키를 생성한다.
*/
@Override
public void afterPropertiesSet() {
secretKey = new SecretKeySpec(secret.getBytes(), SignatureAlgorithm.HS256.getJcaName());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,5 @@ public class JwtAccessDeniedHandler implements AccessDeniedHandler {
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
Expand Up @@ -24,6 +24,5 @@ public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
log.debug("Token : {}", request.getHeader("Authorization"));
response.setStatus(401);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
return;
}

// TODO: UserDetailsService를 통해 사용자 정보를 가져와 인증을 진행한다.
if (jwtProvider.validateToken(rawToken, true)) {
JwtUser jwtUser = jwtProvider.getJwtUser(rawToken);
Authentication authentication = new UsernamePasswordAuthenticationToken(jwtUser, null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.helpmeCookies.global.security;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;

import com.helpmeCookies.user.service.UserService;

import lombok.RequiredArgsConstructor;

@RequiredArgsConstructor
public class Oauth2CustomUserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
final DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest);
return oAuth2User;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package com.helpmeCookies.global.security;

import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.helpmeCookies.global.jwt.JwtUser;
import com.helpmeCookies.user.entity.User;
import com.helpmeCookies.user.entity.UserInfo;
import com.helpmeCookies.user.repository.UserRepository;

import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
public class UserDetailService {
private final UserRepository userRepository;


public JwtUser loadUserByEmail(String email,String nickname) throws UsernameNotFoundException {
// 만약 유저가 존재하지 않는다면 저장
User user = userRepository.findByUserInfoEmail(email)
.orElseGet(() -> {
User newUser = User.builder()
.userInfo(UserInfo.builder()
.email(email)
.build())
.nickname(nickname)
.build();
return userRepository.save(newUser);
});
return JwtUser.of(user.getId());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,6 @@
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor


@Controller
public class WebSecurityConfig {
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
Expand All @@ -30,8 +28,33 @@ public class WebSecurityConfig {
@Bean
public WebSecurityCustomizer configure() {
return (web) -> web.ignoring()
.requestMatchers("/static/**")
.requestMatchers("/test/**");
.requestMatchers("/swagger-ui")
.requestMatchers("/static/**");
}

@Bean
public SecurityFilterChain oauth2SecurityFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/oauth2/**")
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests((authorize) -> authorize
.requestMatchers("/oauth2/authorization/**",
"/oauth2/code/kakao/**"
).permitAll()
.anyRequest().authenticated()
)
.oauth2Login((oauth2) -> oauth2
.redirectionEndpoint(redirection -> redirection
.baseUri("/oauth2/code/*"))
.userInfoEndpoint((userInfo) -> userInfo
.userService(new Oauth2CustomUserService())
)
// 추후 로그인 방식이 다양해지면, Handler의 세부 내용을 변경.
.successHandler((request, response, authentication) -> {
response.sendRedirect("/oauth2/login/kakao");
})
);
return http.build();
}

@Bean
Expand All @@ -43,19 +66,18 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests((authorize) ->
authorize
.requestMatchers(
"/login", "/signup", "/", "/user",
"/login", "/signup", "/ttt/*", "/user",
"/api/auth/**",
"/swagger-ui/**",
"/swagger-resources",
"/v3/api-docs/**",
"/actuator/**",
"/v1/**",
"swagger-ui/**"
"swagger-ui/**",
"/test/signup"
).permitAll()
.anyRequest().authenticated()
);

http.exceptionHandling((exception) -> exception
).exceptionHandling((exception) -> exception
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
);
Expand Down
32 changes: 13 additions & 19 deletions src/main/java/com/helpmeCookies/global/utils/AwsS3FileUtils.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import com.amazonaws.services.s3.model.CannedAccessControlList;
import com.amazonaws.services.s3.model.ObjectMetadata;
import com.amazonaws.services.s3.model.PutObjectRequest;
import com.helpmeCookies.product.dto.FileUploadResponse;
import com.helpmeCookies.product.dto.ImageUpload;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
Expand All @@ -14,7 +14,6 @@

import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.UUID;
Expand All @@ -28,26 +27,21 @@ public class AwsS3FileUtils {
private String bucket;

//다중파일 업로드후 url 반환
public List<FileUploadResponse> uploadMultiImages(List<MultipartFile> multipartFiles) {
List<FileUploadResponse> fileList = new ArrayList<>();
public ImageUpload uploadMultiImages(MultipartFile multipartFile) {

multipartFiles.forEach(file -> {
String fileName = createFileName(file.getOriginalFilename()); //파일 이름 난수화
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(file.getSize());
objectMetadata.setContentType(file.getContentType());
String fileName = createFileName(multipartFile.getOriginalFilename()); //파일 이름 난수화
ObjectMetadata objectMetadata = new ObjectMetadata();
objectMetadata.setContentLength(multipartFile.getSize());
objectMetadata.setContentType(multipartFile.getContentType());

try (InputStream inputStream = file.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드 실패" + fileName);
}

fileList.add(new FileUploadResponse(amazonS3.getUrl(bucket,fileName).toString(),fileName));
});
try (InputStream inputStream = multipartFile.getInputStream()) {
amazonS3.putObject(new PutObjectRequest(bucket, fileName, inputStream, objectMetadata)
.withCannedAcl(CannedAccessControlList.PublicRead));
} catch (IOException e) {
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "파일 업로드 실패" + fileName);
}

return fileList;
return new ImageUpload(amazonS3.getUrl(bucket,fileName).toString());
}

public String createFileName(String fileName) {
Expand Down
Loading

0 comments on commit 63506b1

Please sign in to comment.