Skip to content

Commit f777be0

Browse files
chw0912rjswjddn
andauthored
release: 개발 완료 코드 release 브랜치 병합 (#87)
* feat: workflow에 EC2 자동 배포 추가 (#78) * feat: workflow에 EC2 자동 배포 추가 * chore: appleboy/ssh-action 최신버전으로 변경 * feat: 발표방 캐싱 및 인덱싱 적용 feat 새로운 기능 추가 (#80) * docs: 발표방 생성 요청 dto 스웨거 수정 * feat: Room secretCode Unique 설정 * feat: Room 캐시 적용 * test: Room 캐시 테스트 * test: 비밀코드 중복 문제 해결 * fix: 로그인 500에러 수정 * fix: 로그인 500에러 수정 * feat: 카카오 로그인 API (#86) * fix: cors disable 추가 * feat: 카카오 로그인 API * feat: 시큐리티에 카카오 로그인 url 추가 * docs: swagger 수정 * test: test yml 수정 * refactor: Presigned URL 업로드용, 조회용 로직 분리 (#84) * refactor: 기존 presigned-url 발급 로직을 업로드용, 조회용으로 분리 * docs: 발표자료 Swagger tag 추가 --------- Co-authored-by: 김건우 <[email protected]> Co-authored-by: 김건우 <[email protected]>
1 parent b0469da commit f777be0

File tree

13 files changed

+215
-55
lines changed

13 files changed

+215
-55
lines changed

.github/workflows/release-workflow.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ on:
55
branches:
66
- release
77
workflow_dispatch:
8-
8+
99
env:
1010
REGISTRY: ghcr.io
1111
IMAGE_NAME: ${{ github.repository }}

src/main/java/com/oronaminc/join/document/api/DocumentController.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
import com.oronaminc.join.member.security.MemberDetails;
88
import io.swagger.v3.oas.annotations.Operation;
99
import io.swagger.v3.oas.annotations.responses.ApiResponse;
10+
import io.swagger.v3.oas.annotations.tags.Tag;
1011
import jakarta.validation.Valid;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.http.HttpStatus;
1314
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1415
import org.springframework.web.bind.annotation.*;
1516

17+
@Tag(name = "발표자료")
1618
@RestController
1719
@RequiredArgsConstructor
1820
@RequestMapping("/api/documents")
@@ -37,6 +39,6 @@ public DocumentResponse generatePresignedUrl(
3739
@AuthenticationPrincipal MemberDetails memberDetails
3840
) {
3941
String memberRole = memberDetails.getRole();
40-
return documentService.generatePresignedUrl(documentRequest, memberRole);
42+
return documentService.generateUploadPresignedUrl(documentRequest, memberRole);
4143
}
4244
}

src/main/java/com/oronaminc/join/document/service/DocumentService.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ public void deleteByRoomId(Long roomId) {
3737
documentRepository.deleteByRoomId(roomId);
3838
}
3939

40-
public DocumentResponse generatePresignedUrl(DocumentRequest request, String memberRole) {
40+
public DocumentResponse generateUploadPresignedUrl(DocumentRequest request, String memberRole) {
4141
if (!memberRole.equals(MemberType.MEMBER.name())) {
4242
throw new ErrorException(ErrorCode.UNAUTHORIZED_MEMBER);
4343
}
@@ -52,7 +52,7 @@ public DocumentResponse generatePresignedUrl(DocumentRequest request, String mem
5252

5353
String uuid = UUID.randomUUID().toString();
5454
String objectKey = "temp/" + uuid + extension;
55-
String presignedUrl = s3Service.generatePresignedUrl(objectKey);
55+
String presignedUrl = s3Service.generateUploadPresignedUrl(objectKey);
5656

5757
return new DocumentResponse(presignedUrl, objectKey);
5858
}

src/main/java/com/oronaminc/join/infra/service/S3Service.java

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
1212
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
1313
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
14+
import software.amazon.awssdk.services.s3.presigner.model.PresignedPutObjectRequest;
15+
import software.amazon.awssdk.services.s3.presigner.model.PutObjectPresignRequest;
1416

1517
import java.time.Duration;
1618

@@ -25,7 +27,8 @@ public class S3Service {
2527
@Value("${cloud.aws.s3.bucket}")
2628
private String bucket;
2729

28-
public String generatePresignedUrl(String key) {
30+
// 조회용
31+
public String generateGetPresignedUrl(String key) {
2932
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
3033
.bucket(bucket)
3134
.key(key)
@@ -41,6 +44,24 @@ public String generatePresignedUrl(String key) {
4144
return presignedRequest.url().toString();
4245
}
4346

47+
// 업로드용
48+
public String generateUploadPresignedUrl(String key) {
49+
PutObjectRequest putObjectRequest = PutObjectRequest.builder()
50+
.bucket(bucket)
51+
.contentType("application/pdf")
52+
.key(key)
53+
.build();
54+
55+
PutObjectPresignRequest presignRequest = PutObjectPresignRequest.builder()
56+
.signatureDuration(Duration.ofMinutes(3))
57+
.putObjectRequest(putObjectRequest)
58+
.build();
59+
60+
PresignedPutObjectRequest presignedRequest = s3Presigner.presignPutObject(presignRequest);
61+
62+
return presignedRequest.url().toString();
63+
}
64+
4465
public void deleteFile(String key) {
4566
try {
4667
if (isFileExist(key)) {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
public record KakaoLoginRequest(
4+
String code,
5+
String state
6+
) {
7+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
5+
public record KakaoLoginResponse(
6+
@Schema(description = "회원 id", example = "1001")
7+
Long id
8+
) {
9+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package com.oronaminc.join.member.dto;
2+
3+
import lombok.Builder;
4+
5+
@Builder
6+
public record KakaoUserResponse(
7+
String email,
8+
String nickname,
9+
String profileImageUrl
10+
) {
11+
}

src/main/java/com/oronaminc/join/member/security/AuthController.java

Lines changed: 32 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,5 @@
11
package com.oronaminc.join.member.security;
22

3-
import io.swagger.v3.oas.annotations.Operation;
4-
import io.swagger.v3.oas.annotations.responses.ApiResponse;
5-
import io.swagger.v3.oas.annotations.responses.ApiResponses;
6-
import io.swagger.v3.oas.annotations.tags.Tag;
7-
import io.swagger.v3.oas.annotations.tags.Tags;
83
import java.util.List;
94

105
import org.springframework.http.HttpStatus;
@@ -24,8 +19,13 @@
2419

2520
import com.oronaminc.join.member.dto.GuestLoginRequest;
2621
import com.oronaminc.join.member.dto.GuestLoginResponse;
22+
import com.oronaminc.join.member.dto.KakaoLoginRequest;
23+
import com.oronaminc.join.member.dto.KakaoLoginResponse;
2724
import com.oronaminc.join.member.dto.SessionInfoResponse;
2825

26+
import io.swagger.v3.oas.annotations.Operation;
27+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
28+
import io.swagger.v3.oas.annotations.tags.Tag;
2929
import jakarta.servlet.http.Cookie;
3030
import jakarta.servlet.http.HttpServletRequest;
3131
import jakarta.servlet.http.HttpServletResponse;
@@ -40,6 +40,33 @@
4040
public class AuthController {
4141
private final AuthService authService;
4242

43+
@Operation(
44+
summary = "카카오 로그인",
45+
description = "redirect url 에 포함된 파라미터의 code와 state를 입력해주세요. 이후 모든 요청에 세션 인증이 적용됩니다."
46+
)
47+
@PostMapping("/kakao")
48+
@ResponseStatus(HttpStatus.OK)
49+
public KakaoLoginResponse kakaoLogin(
50+
@RequestBody KakaoLoginRequest kakaoLoginRequest,
51+
HttpServletRequest request
52+
) {
53+
MemberDetails memberDetails = authService.kakaoLogin(kakaoLoginRequest.code());
54+
55+
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
56+
memberDetails, null, List.of(new SimpleGrantedAuthority(memberDetails.getRole()))
57+
);
58+
59+
SecurityContext context = SecurityContextHolder.createEmptyContext();
60+
context.setAuthentication(authentication);
61+
SecurityContextHolder.setContext(context);
62+
63+
SecurityContextHolder.getContext().setAuthentication(authentication);
64+
65+
request.getSession(true).setAttribute(HttpSessionSecurityContextRepository.SPRING_SECURITY_CONTEXT_KEY, context);
66+
67+
return new KakaoLoginResponse(memberDetails.getId());
68+
}
69+
4370
@Operation(
4471
summary = "비회원 로그인",
4572
description = "닉네임을 입력하면 비회원 세션이 생성되고 인증이 설정됩니다. 이후 모든 요청에 세션 인증이 적용됩니다.",

src/main/java/com/oronaminc/join/member/security/AuthService.java

Lines changed: 85 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,19 +3,26 @@
33
import static com.oronaminc.join.member.util.MemberMapper.*;
44

55
import java.util.Map;
6-
import java.util.Optional;
76

7+
import org.springframework.beans.factory.annotation.Value;
8+
import org.springframework.http.HttpEntity;
9+
import org.springframework.http.HttpHeaders;
10+
import org.springframework.http.HttpMethod;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.http.ResponseEntity;
813
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
9-
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
10-
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
11-
import org.springframework.security.oauth2.core.user.OAuth2User;
1214
import org.springframework.stereotype.Service;
1315
import org.springframework.transaction.annotation.Transactional;
16+
import org.springframework.util.LinkedMultiValueMap;
17+
import org.springframework.util.MultiValueMap;
18+
import org.springframework.web.client.RestTemplate;
1419

1520
import com.oronaminc.join.member.dao.MemberRepository;
1621
import com.oronaminc.join.member.domain.Member;
1722
import com.oronaminc.join.member.dto.GuestLoginRequest;
23+
import com.oronaminc.join.member.dto.KakaoUserResponse;
1824
import com.oronaminc.join.member.service.MemberReader;
25+
import com.oronaminc.join.member.util.MemberMapper;
1926

2027
import lombok.RequiredArgsConstructor;
2128
import lombok.extern.slf4j.Slf4j;
@@ -27,32 +34,91 @@ public class AuthService extends DefaultOAuth2UserService {
2734
private final MemberRepository memberRepository;
2835
private final MemberReader memberReader;
2936

30-
@Override
31-
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
32-
OAuth2User oAuth2User = super.loadUser(userRequest);
33-
Map<String, Object> attributes = oAuth2User.getAttributes();
37+
private final RestTemplate restTemplate = new RestTemplate();
38+
39+
private static final String TOKEN_URI = "https://kauth.kakao.com/oauth/token";
40+
private static final String USER_INFO_URI = "https://kapi.kakao.com/v2/user/me";
41+
42+
@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
43+
private String clientId;
44+
45+
@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
46+
private String redirectUri;
47+
48+
@Value("${spring.security.oauth2.client.registration.kakao.client-secret}")
49+
private String clientSecret;
50+
51+
// @Override
52+
// public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
53+
// OAuth2User oAuth2User = super.loadUser(userRequest);
54+
// Map<String, Object> attributes = oAuth2User.getAttributes();
55+
//
56+
// log.info("attributes :: " + attributes);
57+
//
58+
// Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
59+
// Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
60+
//
61+
//
62+
// Optional<Member> optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString());
63+
//
64+
// Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile)));
65+
//
66+
// return toOAuth2MemberDetails(member);
67+
// }
3468

35-
log.info("attributes :: " + attributes);
69+
@Transactional
70+
public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) {
71+
Member guest = toGuestMember(guestLoginRequest);
3672

37-
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
38-
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
73+
memberRepository.save(guest);
74+
guest.registerGuest();
3975

76+
return toGuestMemberDetails(guest);
77+
}
4078

41-
Optional<Member> optionalMember = memberReader.findByEmail(kakaoAccount.get("email").toString());
79+
@Transactional
80+
public MemberDetails kakaoLogin(String code) {
81+
String accessToken = getAccessToken(code);
82+
KakaoUserResponse kakaoUser = getUserInfo(accessToken);
4283

43-
Member member = optionalMember.orElseGet(() -> memberRepository.save(toKakaoMember(kakaoAccount, profile)));
84+
Member member = memberRepository.findByEmail(kakaoUser.email())
85+
.orElseGet(() -> memberRepository.save(MemberMapper.toNewKakaoMember(kakaoUser)));
4486

4587
return toOAuth2MemberDetails(member);
4688
}
4789

48-
@Transactional
49-
public MemberDetails loadGuest(GuestLoginRequest guestLoginRequest) {
50-
Member guest = toGuestMember(guestLoginRequest);
90+
private String getAccessToken(String code) {
91+
HttpHeaders headers = new HttpHeaders();
92+
headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
5193

52-
memberRepository.save(guest);
53-
guest.registerGuest();
94+
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
95+
params.add("grant_type", "authorization_code");
96+
params.add("client_id", clientId);
97+
params.add("redirect_uri", redirectUri);
98+
params.add("code", code);
99+
params.add("client_secret", clientSecret);
54100

55-
return toGuestMemberDetails(guest);
101+
HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(params, headers);
102+
103+
ResponseEntity<Map> response = restTemplate.postForEntity(TOKEN_URI, request, Map.class);
104+
105+
return (String) response.getBody().get("access_token");
56106
}
57107

108+
private KakaoUserResponse getUserInfo(String accessToken) {
109+
HttpHeaders headers = new HttpHeaders();
110+
headers.setBearerAuth(accessToken);
111+
headers.setContentType(MediaType.APPLICATION_JSON);
112+
113+
HttpEntity<Void> entity = new HttpEntity<>(headers);
114+
115+
ResponseEntity<Map> response = restTemplate.exchange(USER_INFO_URI, HttpMethod.GET, entity, Map.class);
116+
117+
Map<String, Object> attributes = response.getBody();
118+
119+
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
120+
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
121+
122+
return MemberMapper.toKakaoUserResponse(kakaoAccount, profile);
123+
}
58124
}

src/main/java/com/oronaminc/join/member/security/SecurityConfig.java

Lines changed: 2 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,13 @@
22

33
import static org.springframework.security.config.Customizer.*;
44

5-
import java.util.Arrays;
6-
import java.util.List;
7-
85
import org.springframework.context.annotation.Bean;
96
import org.springframework.context.annotation.Configuration;
107
import org.springframework.context.annotation.Profile;
118
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
129
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1310
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
1411
import org.springframework.security.web.SecurityFilterChain;
15-
import org.springframework.web.cors.CorsConfiguration;
16-
import org.springframework.web.cors.CorsConfigurationSource;
17-
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
1812

1913
import lombok.RequiredArgsConstructor;
2014

@@ -29,10 +23,11 @@ public class SecurityConfig {
2923
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
3024
return http
3125
.csrf(csrf -> csrf.disable())
32-
.cors(withDefaults())
26+
.cors(cors -> cors.disable())
3327
.authorizeHttpRequests(auth -> auth
3428
.requestMatchers(
3529
"/api/auth/guest",
30+
"/api/auth/kakao",
3631
"/login"
3732
)
3833
.anonymous()
@@ -56,22 +51,4 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
5651
.logout(withDefaults())
5752
.build();
5853
}
59-
60-
@Bean
61-
public CorsConfigurationSource corsConfigurationSource() {
62-
CorsConfiguration configuration = new CorsConfiguration();
63-
configuration.setAllowedOrigins(
64-
Arrays.asList(
65-
"http://localhost:8080",
66-
"http://localhost:3000"
67-
)
68-
);
69-
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
70-
configuration.setAllowedHeaders(List.of("*"));
71-
configuration.setAllowCredentials(true);
72-
73-
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
74-
source.registerCorsConfiguration("/**", configuration);
75-
return source;
76-
}
7754
}

0 commit comments

Comments
 (0)