Skip to content

Commit

Permalink
Merge pull request #72 from Drink-Easy/dev
Browse files Browse the repository at this point in the history
#70 Merge: 애플 로그인 합병
  • Loading branch information
jeongyeon0208 authored Aug 18, 2024
2 parents 21a5a9a + e7f929c commit 5ac535e
Show file tree
Hide file tree
Showing 18 changed files with 369 additions and 20 deletions.
14 changes: 14 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@ java {
}
}

ext {
springCloudVersion = "2023.0.2"
}

dependencyManagement {
imports {
mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
}
}


configurations {
compileOnly {
extendsFrom annotationProcessor
Expand Down Expand Up @@ -50,6 +61,9 @@ dependencies {
// S3
implementation 'org.springframework.cloud:spring-cloud-starter-aws:2.2.6.RELEASE'

//fegin client
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'


}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,11 +68,16 @@ public enum ErrorStatus implements BaseCode {
// Redis Error
REDIS_NOT_FOUND(HttpStatus.BAD_REQUEST, "REDIS4001", "Redis 설정에 오류가 발생했습니다."),

// appleLogin Error
MATCH_PUBLIC_KEY_NOR_FOUND(HttpStatus.BAD_REQUEST, "APPLE4001", "일치하는 공개키를 찾을 수 없습니다"),
IDENTITY_TOKEN_NOT_FOUND(HttpStatus.BAD_REQUEST, "APPLE4002", "아이덴티티 토큰을 찾을 수 없습니다."),

// WineStore Error
WINE_STORE_NOT_FOUND(HttpStatus.BAD_REQUEST, "WINE_STORE4001", "와인 스토어가 없습니다."),
WINE_STORE_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "WINE_STORE4002", "권한이 없는 스토어입니다.");



private final HttpStatus httpStatus;
private final String code;
private final String message;
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/com/drinkeg/drinkeg/config/FeignConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.drinkeg.drinkeg.config;


import com.drinkeg.drinkeg.DrinkegApplication;
import feign.Logger;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackageClasses = DrinkegApplication.class)
public class FeignConfig {
@Bean
Logger.Level feignLoggerLevel(){
return Logger.Level.FULL;
}
}
3 changes: 2 additions & 1 deletion src/main/java/com/drinkeg/drinkeg/config/RedisConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,5 @@ public RedisTemplate<String, Object> redisTemplate() {

return redisTemplate;
}
}
}

5 changes: 3 additions & 2 deletions src/main/java/com/drinkeg/drinkeg/config/SecurityConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,15 @@ public class SecurityConfig {
private final RedisClient redisClient;
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;


// AuthenticationManager가 인자로 받을 AuthenticationConfiguraion 객체 생성자 주입
private final AuthenticationConfiguration authenticationConfiguration;

@Bean
public WebSecurityCustomizer webSecurityCustomizer() {
return web -> {
web.ignoring()
.requestMatchers("/join",
.requestMatchers("/join","/login/apple/**",
"/api-docs/**", "/swagger-ui/**", "/swagger-ui.html/**", "/v3/api-docs/**", "/swagger-ui/index.html#/**");// 필터를 타면 안되는 경로
};
}
Expand Down Expand Up @@ -129,7 +130,7 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
.authorizeHttpRequests((authorize) -> authorize
//.requestMatchers("/my").authenticated()
.requestMatchers("/api-docs/**", "/swagger-ui/**", "/swagger-ui.html/**", "/v3/api-docs/**", "/swagger-ui/index.html#/**").permitAll()
.requestMatchers("/", "/join", "/login", "/reissue").permitAll()
.requestMatchers("/", "/join", "/login", "/reissue","/login/apple").permitAll()

// home 인가
.requestMatchers(HttpMethod.GET,"/home").hasRole("USER")
Expand Down
25 changes: 25 additions & 0 deletions src/main/java/com/drinkeg/drinkeg/controller/MemberController.java
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
package com.drinkeg.drinkeg.controller;

import com.drinkeg.drinkeg.apipayLoad.ApiResponse;
import com.drinkeg.drinkeg.apipayLoad.code.status.ErrorStatus;
import com.drinkeg.drinkeg.apipayLoad.handler.TempHandler;
import com.drinkeg.drinkeg.domain.Member;
import com.drinkeg.drinkeg.dto.AppleLoginDTO.AppleLoginRequestDTO;
import com.drinkeg.drinkeg.dto.loginDTO.jwtDTO.JoinDTO;
import com.drinkeg.drinkeg.dto.loginDTO.oauth2DTO.LoginResponse;
import com.drinkeg.drinkeg.jwt.JWTUtil;
import com.drinkeg.drinkeg.service.loginService.AppleLoginService;
import com.drinkeg.drinkeg.dto.loginDTO.commonDTO.MemberRequestDTO;
import com.drinkeg.drinkeg.dto.loginDTO.commonDTO.MemberResponseDTO;
import com.drinkeg.drinkeg.dto.loginDTO.commonDTO.PrincipalDetail;
Expand All @@ -10,6 +17,7 @@
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
Expand All @@ -22,6 +30,8 @@ public class MemberController {

private final JoinService joinService;
private final TokenService tokenService;
private final AppleLoginService appleLoginService;
private final JWTUtil jwtUtil;

@PostMapping("/join")
public ApiResponse<?> joinProcess(@RequestBody JoinDTO joinDTO) {
Expand All @@ -41,4 +51,19 @@ public ApiResponse<?> reissue(HttpServletRequest request, HttpServletResponse re
public ApiResponse<MemberResponseDTO> addMemberDetail(@RequestBody MemberRequestDTO memberRequestDTO, @AuthenticationPrincipal PrincipalDetail principalDetail) {
return ApiResponse.onSuccess(joinService.addMemberDetail(memberRequestDTO, principalDetail.getUsername()));
}

@PostMapping("/login/apple")
public ApiResponse<LoginResponse> appleLogin(@RequestBody AppleLoginRequestDTO appleLoginRequestDTO,HttpServletResponse response) throws Exception{

System.out.println("=========start apple login controller============");

if(appleLoginRequestDTO.getIdentityToken() == null){
throw new TempHandler(ErrorStatus.IDENTITY_TOKEN_NOT_FOUND);
}

return ApiResponse.onSuccess(appleLoginService.appleLogin(appleLoginRequestDTO, response));
}



}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.drinkeg.drinkeg.dto.AppleLoginDTO;


import jakarta.validation.constraints.NotEmpty;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class AppleLoginRequestDTO {

@NotEmpty
private String identityToken; // 프론트한테 유저 정보가 들어있는 identityToken
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.drinkeg.drinkeg.dto.AppleLoginDTO;


import com.drinkeg.drinkeg.apipayLoad.code.status.ErrorStatus;
import com.drinkeg.drinkeg.apipayLoad.handler.TempHandler;
import lombok.Getter;

import java.util.List;

// 애플 서버에서 주는 공개키 (public key) DTO
@Getter
public class ApplePublicKeyResponseDTO {

private List<ApplePublicKey> keys;

@Getter
public static class ApplePublicKey {
private String kty;
private String kid;
private String use;
private String alg;
private String n;
private String e;
}

// 여러개 받은 공개 키 중에 Identity Token과 kid와 alg가 일치하는 토큰 찾아주는 메서드
public ApplePublicKey getMatchedKeyBy(String kid, String alg) {
return keys.stream()
.filter(key -> key.getKid().equals(kid) && key.getAlg().equals(alg))
.findAny()
.orElseThrow(() -> new TempHandler(ErrorStatus.MATCH_PUBLIC_KEY_NOR_FOUND));
}


}
16 changes: 16 additions & 0 deletions src/main/java/com/drinkeg/drinkeg/fegin/AppleAuthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.drinkeg.drinkeg.fegin;

import com.drinkeg.drinkeg.dto.AppleLoginDTO.ApplePublicKeyResponseDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.context.annotation.Bean;
import org.springframework.web.bind.annotation.GetMapping;


// 공개키를 요청함
// url은 https://appleid.apple.com/auth/keys

@FeignClient(name= "appleAuthClient", url = "https://appleid.apple.com/auth/keys")
public interface AppleAuthClient {
@GetMapping
ApplePublicKeyResponseDTO getAppleAuthPublicKey();
}
4 changes: 2 additions & 2 deletions src/main/java/com/drinkeg/drinkeg/jwt/CustomLogoutFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,11 @@ private void doFilter(HttpServletRequest request, HttpServletResponse response,

String username = jwtUtil.getUsername(refresh);

//DB에 저장되어 있는지 확인
// DB에 저장되어 있는지 확인
String redisRefresh = redisClient.getValue(username);
if (StringUtils.isEmpty(redisRefresh) || !refresh.equals(redisRefresh)) {

//response body
// response body
throw new GeneralException(ErrorStatus.INVALID_REFRESH_TOKEN);
}

Expand Down
1 change: 1 addition & 0 deletions src/main/java/com/drinkeg/drinkeg/jwt/JWTFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
.build();

System.out.println(userDTO);

//UserDetails에 회원 정보 객체 담기
PrincipalDetail principalDetail = new PrincipalDetail(userDTO);

Expand Down
2 changes: 0 additions & 2 deletions src/main/java/com/drinkeg/drinkeg/jwt/LoginFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR

System.out.println("---------------LoginFilter------------------");

System.out.println("accessToken === " + accessToken);
System.out.println("refreshToken == " + refreshToken);

// 토큰을 쿠키에 저장하여 응답 (access 의 경우 추후 프론트와 협의하여 헤더에 넣어서 반환할 예정)
response.addCookie(tokenService.createCookie("accessToken", accessToken));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,6 @@ public class CustomSuccessHandler extends SimpleUrlAuthenticationSuccessHandler
private final TokenService tokenService;
private final RedisClient redisClient;

// CustomSuccessHandler(JWTUtil jwtUtil) {

//this.jwtUtil = jwtUtil;
//}

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/com/drinkeg/drinkeg/redis/RedisClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,5 @@ public boolean checkExistsValue(String key) {
throw new GeneralException(ErrorStatus.REDIS_NOT_FOUND);
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.drinkeg.drinkeg.service.loginService;

import com.drinkeg.drinkeg.domain.Member;
import com.drinkeg.drinkeg.dto.AppleLoginDTO.AppleLoginRequestDTO;
import com.drinkeg.drinkeg.dto.loginDTO.oauth2DTO.LoginResponse;
import com.drinkeg.drinkeg.fegin.AppleAuthClient;
import com.drinkeg.drinkeg.jwt.JWTUtil;
import com.drinkeg.drinkeg.redis.RedisClient;
import com.drinkeg.drinkeg.repository.MemberRepository;
import com.drinkeg.drinkeg.utils.ApplePublicKeyGenerator;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletResponse;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.security.oauth2.core.ClaimAccessor;
import org.springframework.stereotype.Service;

import javax.naming.AuthenticationException;
import javax.print.DocFlavor;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Map;
import java.util.Optional;

@Service
@RequiredArgsConstructor
public class AppleLoginService {

private final TokenService tokenService;
private final AppleAuthClient appleAuthClient;
private final ApplePublicKeyGenerator applePublicKeyGenerator;
private final MemberRepository memberRepository;
private final RedisClient redisClient;
private final JWTUtil jwtUtil;

public LoginResponse appleLogin(AppleLoginRequestDTO appleLoginRequestDTO, HttpServletResponse response)throws AuthenticationException, NoSuchAlgorithmException, InvalidKeySpecException,
JsonProcessingException {

System.out.println("--------------apple Login Start---------------");

String identityToken = appleLoginRequestDTO.getIdentityToken();

// identity Token에서 헤더 추출
final Map<String, String> appleTokenHeader = tokenService.parseHeaders(appleLoginRequestDTO.getIdentityToken());

// 애플 서버에서 publicKey 받아온 후에 identity 토큰의 헤더와 일치하는 publicKey 만들기
PublicKey publicKey = applePublicKeyGenerator.generatePublicKey(appleTokenHeader,
appleAuthClient.getAppleAuthPublicKey());

// identity Token에서 claims 추출
Claims claims = tokenService.getTokenClaims(identityToken, publicKey);

// 회원 가입 된 사용자인지 확인하기
String username = "apple "+ claims.getSubject();
Optional<Member> existData = memberRepository.findByUsername(username);


if (existData.isEmpty()){

Member member = Member.builder()
.username(username)
.email(claims.get("email", String.class))
.role("ROLE_USER")
.isFirst(true)
.build();
memberRepository.save(member);

System.out.println("첫 로그인임");

jwtProvider(member, response);

return LoginResponse.builder()
.username(username)
.role(member.getRole())
.isFirst(member.getIsFirst())
.build();

}
else{

Member member = existData.get();
member.updateEmail(claims.get("email", String.class));

System.out.println("첫 로그인아님");
memberRepository.save(member);

jwtProvider(member, response);

return LoginResponse.builder()
.username(username)
.role(member.getRole())
.isFirst(member.getIsFirst())
.build();
}
}

public void jwtProvider(Member member, HttpServletResponse response) {

String accessToken = jwtUtil.createJwt("access",member.getUsername(), member.getRole(), 60000000000L); // 임의로 10000배로 해놓았음. 나중에 수정 필요.
String refreshToken = jwtUtil.createJwt("refresh",member.getUsername(), member.getRole(),864000000L);

// 토큰을 쿠키에 저장하여 응답
response.addCookie(tokenService.createCookie("accessToken", accessToken));
response.addCookie(tokenService.createCookie("refreshToken", refreshToken));
response.setStatus(HttpStatus.OK.value());

// redis에 refresh 토큰 저장
redisClient.setValue(member.getUsername(), refreshToken, 864000000L);
}

}
Loading

0 comments on commit 5ac535e

Please sign in to comment.