Skip to content

Commit

Permalink
Merge pull request #28 from OnAndOff-UMC/feat/#24
Browse files Browse the repository at this point in the history
[feat]: 애플 로그인 구현
  • Loading branch information
wu-seong authored Jan 31, 2024
2 parents 46b3418 + 68d2d59 commit b6f4c9c
Show file tree
Hide file tree
Showing 24 changed files with 558 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,8 @@
@AllArgsConstructor
public enum SuccessStatus implements BaseCode {
// 일반적인 응답
_OK(HttpStatus.OK, "COMMON200", "성공입니다."),
_OK(HttpStatus.OK, "COMMON200", "성공입니다.");

// 로그인 응답
NEED_USER_DETAIL(HttpStatus.OK, "LOGIN200", "토큰이 유효하고 유저의 추가정보가 필요합니다.");
private final HttpStatus httpStatus;
private final String code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,19 @@

import com.fasterxml.jackson.core.JsonProcessingException;
import com.onnoff.onnoff.apiPayload.ApiResponse;
import com.onnoff.onnoff.apiPayload.code.status.SuccessStatus;
import com.onnoff.onnoff.auth.UserContext;
import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO;
import com.onnoff.onnoff.auth.dto.LoginRequestDTO;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import com.onnoff.onnoff.auth.jwt.dto.JwtToken;
import com.onnoff.onnoff.auth.jwt.service.JwtUtil;
import com.onnoff.onnoff.auth.service.LoginService;
import com.onnoff.onnoff.auth.service.AppleLoginService;
import com.onnoff.onnoff.auth.service.KakaoLoginService;
import com.onnoff.onnoff.domain.user.User;
import com.onnoff.onnoff.domain.user.converter.UserConverter;
import com.onnoff.onnoff.domain.user.dto.UserResponseDTO;
import com.onnoff.onnoff.domain.user.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -29,11 +28,13 @@
@Controller
@RequiredArgsConstructor
public class LoginController {
private final LoginService loginService;
private final KakaoLoginService kakaoLoginService;
private final AppleLoginService appleLoginService;
private final UserService userService;
private final JwtUtil jwtUtil;



/*
테스트용 API, CORS 때문에 직접 호출하지 않고 redirect
*/
Expand All @@ -51,60 +52,71 @@ public String login(){
*/
@GetMapping("/oauth2/login/kakao")
public ResponseEntity<String> getAccessToken(@RequestParam(name = "code") String code){
String accessToken = loginService.getAccessToken(code);
return ResponseEntity.ok("http://localhost:8080/oauth2/kakao/token/validate?accessToken="+accessToken);
TokenResponse tokenResponse = kakaoLoginService.getAccessTokenByCode(code);
return ResponseEntity.ok("http://localhost:8080/oauth2/kakao/token/validate?accessToken="+ tokenResponse.getAccessToken());
}
/*
1. 토큰 유효성 검증
1. ID 토큰 유효성 검증
2. 사용자 정보 얻어오기
3. DB 조회 및 추가
4. 응답 헤더에 Jwt 토큰 추가
*/

@Operation(summary = "토큰 검증 API",description = "토큰을 검증 하고 이에 대한 결과를 응답합니다. 추가 정보 입력 여부도 같이 응답 합니다.")
@ApiResponses(value = {
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "202", description = "토큰 검증 성공," + " 추가 정보 기입이 필요합니다.",
content = @Content(schema = @Schema(implementation = UserResponseDTO.ApiResponseLoginDTO.class))),
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "200", description = "토큰 검증 성공",
content = @Content(schema = @Schema(implementation = UserResponseDTO.ApiResponseUserDetailDTO.class)))
})
@ResponseBody
@PostMapping("/oauth2/kakao/token/validate")
public ApiResponse<?> validateToken(HttpServletResponse response, @RequestBody String accessToken) {
// 토큰 검증
loginService.validate(accessToken);
public ApiResponse<UserResponseDTO.LoginDTO> validateKakoToken(HttpServletResponse response, @RequestBody LoginRequestDTO.KakaoTokenValidateDTO requestDTO) {
// identity 토큰 검증
kakaoLoginService.validate(requestDTO.getIdentityToken());
// ok -> 유저 정보 가져오기
KakaoOauth2DTO.UserInfoResponseDTO userInfoResponseDTO = null;
KakaoOauth2DTO.UserInfoResponseDTO userInfo;
try {
userInfoResponseDTO = loginService.getUserInfo(accessToken);
userInfo = kakaoLoginService.getUserInfo(requestDTO.getAccessToken());
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException(e);
}
// 유저 정보에 DB 조회하고 정보 있으면 응답만, 없으면 저장까지, 추가정보 입력 여부에 따라서 응답 다르게
Long oauthId = userInfoResponseDTO.getId();
String oauthId = userInfo.getSub();
User user;
if( userService.isExistByOauthId(oauthId)){
User user = userService.getUserByOauthId(oauthId);
// 응답헤더에 토큰 추가
JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId()));
response.addHeader("Access-Token", token.getAccessToken());
response.addHeader("Refresh-Token", token.getRefreshToken());
if(user.isInfoSet()){
return ApiResponse.onSuccess(UserConverter.toUserDetailDTO(user));
}
return ApiResponse.of(SuccessStatus.NEED_USER_DETAIL, UserConverter.toLoginDTO(user));
user = userService.getUserByOauthId(oauthId);
}
else{
User user = UserConverter.toUser(userInfoResponseDTO);
Long id = userService.create(user);
// 응답헤더에 토큰 추가
JwtToken token = jwtUtil.generateToken(String.valueOf(id));
response.addHeader("Access-Token", token.getAccessToken());
response.addHeader("Refresh-Token", token.getRefreshToken());
return ApiResponse.of(SuccessStatus.NEED_USER_DETAIL, UserConverter.toLoginDTO(user));
user = UserConverter.toUser(userInfo);
user = userService.create(user);
}
// 응답헤더에 토큰 추가
JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId()));
response.addHeader("Access-Token", token.getAccessToken());
response.addHeader("Refresh-Token", token.getRefreshToken());
return ApiResponse.onSuccess(UserConverter.toLoginDTO(user));
}

@ResponseBody
@PostMapping("/oauth2/apple/token/validate")
public ApiResponse<?> validateAppleToken(HttpServletResponse response, @RequestBody LoginRequestDTO.AppleTokenValidateDTO requestDTO) {
// 검증하기
appleLoginService.validate(requestDTO.getIdentityToken());
// 검증 성공 시 리프레시 토큰 발급받아 저장(기한 무제한, 회원탈퇴 시 필요)
TokenResponse tokenResponse = appleLoginService.getAccessTokenByCode(requestDTO.getAuthorizationCode());
// 유저 정보 조회 및 저장
String oauthId = requestDTO.getOauthId();
User user;
if( userService.isExistByOauthId(oauthId)){
user = userService.getUserByOauthId(oauthId);
}
else{
user = UserConverter.toUser(requestDTO);
user.setAppleRefreshToken(tokenResponse.getRefreshToken());
user = userService.create(user);
}
// 응답헤더에 토큰 추가
JwtToken token = jwtUtil.generateToken(String.valueOf(user.getId()));
response.addHeader("Access-Token", token.getAccessToken());
response.addHeader("Refresh-Token", token.getRefreshToken());
return ApiResponse.onSuccess(UserConverter.toLoginDTO(user));
}
/*
테스트용 API
*/
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/com/onnoff/onnoff/auth/dto/LoginRequestDTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.onnoff.onnoff.auth.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

public class LoginRequestDTO {
@Getter
public static class AppleTokenValidateDTO{
@JsonProperty("user")
private String oauthId;
@JsonProperty("full_name")
private String fullName;
private String email;
@JsonProperty("identity_token")
private String identityToken;
@JsonProperty("authorization_code")
private String authorizationCode;
}
@Getter
public static class KakaoTokenValidateDTO{
@JsonProperty("identity_token")
private String identityToken;
@JsonProperty("access_token")
private String accessToken;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.fasterxml.jackson.databind.ObjectMapper;
import com.onnoff.onnoff.apiPayload.code.status.ErrorStatus;
import com.onnoff.onnoff.apiPayload.exception.GeneralException;
import com.onnoff.onnoff.auth.feignClient.dto.ErrorResponseDTO;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.ErrorResponseDTO;
import feign.Response;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.onnoff.onnoff.auth.feignClient.client;


import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.util.MultiValueMap;
import org.springframework.web.bind.annotation.GetMapping;

@FeignClient(name = "apple-auth-client",url = "https://appleid.apple.com/auth")
public interface AppleAuthClient{
@GetMapping("/keys")
JwkResponse.JwkSet getKeys();

@GetMapping(value = "/token", consumes = "application/x-www-form-urlencoded")
TokenResponse getToken(MultiValueMap requestBody);

//회원 탈퇴 메서드
// @GetMapping("/revoke")
// KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken);
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package com.onnoff.onnoff.auth.feignClient.client;

import com.onnoff.onnoff.auth.feignClient.config.FeignConfig;
import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO;
import feign.Headers;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;

/*
토큰 유효성 검증하고 사용자 정보 가져오는 client
토큰 유효성 검증 하고 사용자 정보 가져오는 client
*/

@FeignClient(name = "kakao-api-client", url = "https://kapi.kakao.com", configuration = FeignConfig.class)
public interface KakaoApiClient {
@GetMapping("v1/user/access_token_info")
KakaoOauth2DTO.TokenValidateResponseDTO getTokenValidate(@RequestHeader("Authorization") String accessToken);
@GetMapping(value = "/v2/user/me")
KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(@RequestHeader("Authorization") String accessToken, @RequestParam(name = "property_keys") String propertyKeys);
@GetMapping(value = "/v1/oidc/userinfo")
KakaoOauth2DTO.UserInfoResponseDTO getUserInfo(@RequestHeader("Authorization") String accessToken);

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@


import com.onnoff.onnoff.auth.feignClient.config.FeignConfig;
import com.onnoff.onnoff.auth.feignClient.dto.KakaoOauth2DTO;
import com.onnoff.onnoff.auth.feignClient.dto.JwkResponse;
import com.onnoff.onnoff.auth.feignClient.dto.TokenResponse;
import com.onnoff.onnoff.auth.feignClient.dto.kakao.KakaoOauth2DTO;
import feign.Headers;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.*;
Expand All @@ -11,10 +13,11 @@
public interface KakaoOauth2Client {
@Headers("Content-Type: application/x-www-form-urlencoded")
@PostMapping("/oauth/token")
KakaoOauth2DTO.TokenResponseDTO getAccessToken(@RequestParam(name = "grant_type") String grantType,
@RequestParam(name = "client_id") String clientId,
@RequestParam(name = "redirect_uri") String redirectUri,
@RequestParam(name = "code") String code
TokenResponse getAccessToken(@RequestParam(name = "grant_type") String grantType,
@RequestParam(name = "client_id") String clientId,
@RequestParam(name = "redirect_uri") String redirectUri,
@RequestParam(name = "code") String code
);

@GetMapping("/.well-known/jwks.json")
JwkResponse.JwkSet getKeys();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.onnoff.onnoff.auth.feignClient.dto;


import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.List;

public class JwkResponse {
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Jwk {
private String alg;
private String e;
private String kid;
private String kty;
private String n;
private String use;
}
@Getter
public static class JwkSet{
@JsonProperty("keys")
private List<Jwk> jwkList;
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.onnoff.onnoff.auth.feignClient.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Getter;

@Getter
public class TokenResponse {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("expires_in")
private Integer expiresIn;
@JsonProperty("id_token")
private String idToken;
@JsonProperty("refresh_token")
private String refreshToken;
@JsonProperty("token_type")
private String tokenType;
}
Loading

0 comments on commit b6f4c9c

Please sign in to comment.