Skip to content

Commit

Permalink
Merge pull request #48 from studio-recoding/feat-oauth-localhost
Browse files Browse the repository at this point in the history
[🚀feat] Spring Security OAuth
  • Loading branch information
JeonHaeseung authored Apr 29, 2024
2 parents ab00797 + 945dce7 commit d574de2
Show file tree
Hide file tree
Showing 15 changed files with 490 additions and 404 deletions.
44 changes: 0 additions & 44 deletions src/main/java/Ness/Backend/domain/auth/AuthController.java

This file was deleted.

81 changes: 0 additions & 81 deletions src/main/java/Ness/Backend/domain/auth/AuthService.java

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;

import java.io.IOException;

Expand Down Expand Up @@ -67,14 +68,6 @@ protected void successfulAuthentication(HttpServletRequest request, HttpServletR
/* 가장 흔한 방식인 Bearer Token을 사용해 응답 */
response.addHeader("Authorization", "Bearer " + jwtToken.getJwtAccessToken());
response.addHeader("Refresh-Token", "Bearer " + jwtToken.getJwtRefreshToken());

//TODO: JwtToken 과 함께 리다이렉트
/*
String targetUrl = UriComponentsBuilder.fromUriString(setRedirectUrl(request.getServerName()))
.queryParam("jwtAccessToken", jwtToken.getJwtAccessToken())
.queryParam("jwtRefreshToken", jwtToken.getJwtRefreshToken())
.build().toUriString();
*/
}

/*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,13 @@
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.util.Collections;

/* 사용자의 권한 부여
* 요청에 포함된 JWT 토큰을 검증하고, 토큰에서 추출한 권한 정보를 기반으로 사용자에 대한 권한을 확인
Expand Down Expand Up @@ -52,7 +56,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
Member tokenMember = jwtTokenProvider.validJwtToken(jwtToken);

if(tokenMember != null){ //토큰이 정상일 경우
AuthDetails authDetails = new AuthDetails(tokenMember);
AuthDetails authDetails = new AuthDetails(tokenMember, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")));

/* JWT 토큰 서명이 정상이면 Authentication 객체 생성 */
Authentication authentication = new UsernamePasswordAuthenticationToken(authDetails, null, authDetails.getAuthorities());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.web.client.HttpServerErrorException;

import java.time.LocalDateTime;
Expand Down Expand Up @@ -69,7 +70,7 @@ public String generateAccessToken(String authKey, Date now){
.withHeader(headerMap)
.withIssuer("re:coding")
.withIssuedAt(now)
.withSubject(authKey) //토큰의 사용자를 식별하는 고유 주제
.withSubject(authKey) //토큰의 사용자를 식별하는 고유 주제(이메일)
.withExpiresAt(accessTokenExpireDate) //토큰의 만료 시간
.withClaim(AUTHORITIES_KEY, authKey) //토큰에 포함되는 정보인 Claim 설정
.sign(this.getSign());
Expand Down
61 changes: 38 additions & 23 deletions src/main/java/Ness/Backend/domain/auth/oAuth/OAuth2Controller.java
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package Ness.Backend.domain.auth.oAuth;

import Ness.Backend.domain.auth.jwt.entity.JwtToken;
import Ness.Backend.domain.member.entity.Member;
import Ness.Backend.global.auth.AuthUser;
import Ness.Backend.global.response.ApiResponse;
import io.swagger.v3.oas.annotations.Operation;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.HttpStatusCode;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
Expand All @@ -13,28 +16,40 @@
public class OAuth2Controller {
private final OAuth2Service oAuth2Service;

@PostMapping("/login/oauth/{registration}")
@Operation(summary = "OAuth 로그인 요청", description = "구글 계정으로 로그인하는 API 입니다.")
public ResponseEntity<?> socialLogin(@RequestParam String code, @PathVariable String registration) {
return new ResponseEntity<>(oAuth2Service.socialLogin(code, registration), HttpStatusCode.valueOf(200));
}

@PostMapping("/logout/oauth/{registration}")
@Operation(summary = "OAuth 로그아웃 요청", description = "구글 계정 로그아웃 요청 API 입니다.")
public void logout(@AuthUser Member member) {
oAuth2Service.logout(member);
}

@DeleteMapping("/withdrawal/oauth/{registration}")
@Operation(summary = "OAuth 회원탈퇴 요청", description = "구글 계정 회원탈퇴 요청 API 입니다.")
public void withdrawal(@AuthUser Member member) {
oAuth2Service.withdrawal(member);
}

@PostMapping("/reIssuance")
@Operation(summary = "OAuth JWT access 토큰 재발급 요청", description = "JWT access 토큰 재발급 요청 API 입니다.")
public void reIssuance(@AuthUser Member member) {
//추후 구현 예정
//return oAuth2Service.reIssuance(member);
@GetMapping("/oauth/google/success")
@Operation(summary = "개발용 회원가입으로, 클라이언트는 모르는 테스트 용입니다.")
public ApiResponse<JwtToken> devSocialLogin(@RequestParam(value = "jwtAccessToken") String jwtAccessToken, @RequestParam(value = "jwtRefreshToken") String jwtRefreshToken) {
JwtToken jwtToken = JwtToken.builder()
.jwtAccessToken(jwtAccessToken)
.jwtRefreshToken(jwtRefreshToken)
.build();
return ApiResponse.getResponse(HttpStatus.OK.value(), "테스트용 ", jwtToken);
}
//
// @PostMapping("/login/oauth/{registration}")
// @Operation(summary = "OAuth 로그인 요청", description = "구글 계정으로 로그인하는 API 입니다.")
// public ResponseEntity<?> socialLogin(@RequestParam String code, @PathVariable String registration) {
// return new ResponseEntity<>(oAuth2Service.socialLogin(code, registration), HttpStatusCode.valueOf(200));
// }
//
//
//
// @PostMapping("/logout/oauth/{registration}")
// @Operation(summary = "OAuth 로그아웃 요청", description = "구글 계정 로그아웃 요청 API 입니다.")
// public void logout(@AuthUser Member member) {
// oAuth2Service.logout(member);
// }
//
// @DeleteMapping("/withdrawal/oauth/{registration}")
// @Operation(summary = "OAuth 회원탈퇴 요청", description = "구글 계정 회원탈퇴 요청 API 입니다.")
// public void withdrawal(@AuthUser Member member) {
// oAuth2Service.withdrawal(member);
// }
//
// @PostMapping("/reIssuance")
// @Operation(summary = "OAuth JWT access 토큰 재발급 요청", description = "JWT access 토큰 재발급 요청 API 입니다.")
// public void reIssuance(@AuthUser Member member) {
// //추후 구현 예정
// //return oAuth2Service.reIssuance(member);
// }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
package Ness.Backend.domain.auth.oAuth;


import Ness.Backend.domain.auth.security.AuthDetails;
import Ness.Backend.domain.member.MemberRepository;
import Ness.Backend.domain.member.MemberService;
import Ness.Backend.domain.member.entity.Member;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.registration.ClientRegistration;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Collections;
import java.util.Map;
import java.util.Objects;

@Slf4j
@RequiredArgsConstructor
@Service
public class OAuth2CustomUserService extends DefaultOAuth2UserService {
private static final String DEFAULT_STRING = "default";

private final MemberRepository memberRepository;
private final MemberService memberService;

/* OAuth2 로그인 요청에 필요한 정보(클라이언트 등록 정보, 사용자의 권한 부여 코드, 액세스 토큰)를 가지고 있는 객체 OAuth2UserRequest*/
@Override
public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2AuthenticationException {
/*리소스 서버에 사용자 정보 요청 후 리소스 객체(OAuth2User) 받아오기*/
OAuth2User oAuth2User = super.loadUser(oAuth2UserRequest);

Map<String, Object> attributes = oAuth2User.getAttributes();

/*공급자 정보인 registrationId(구글, 카카오, 네이버)*/
String registrationId = oAuth2UserRequest.getClientRegistration().getRegistrationId();

/*사용자 정보 엔드포인트인 userInfoEndpoint*/
ClientRegistration.ProviderDetails.UserInfoEndpoint userInfoEndpoint = oAuth2UserRequest.getClientRegistration().getProviderDetails().getUserInfoEndpoint();

String password = DEFAULT_STRING;
String email = DEFAULT_STRING;
String picture = DEFAULT_STRING;
String nickname = DEFAULT_STRING;
String name = DEFAULT_STRING;

if (Objects.equals(registrationId, "google")){
password = (String) attributes.get("id");
email = (String) attributes.get("email");
picture = (String) attributes.get("picture");
nickname = (String) attributes.get("name");
name = (String) attributes.get("name");
}

if (Objects.equals(registrationId, "kakao")){
Map<String, Object> properties = (Map<String, Object>) attributes.get("properties");
Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");

password = String.valueOf(attributes.get("id"));
email = (String) kakaoAccount.get("email");
picture = (String) properties.get("profile_image");
nickname = (String) properties.get("nickname");
name = (String) properties.get("nickname");
}

if (Objects.equals(registrationId, "naver")){
Map<String, Object> response = (Map<String, Object>) attributes.get("response");

password = String.valueOf(response.get("id"));
email = (String) response.get("email");
picture = (String) response.get("profile_image");
nickname = (String) response.get("nickname");
name = (String) response.get("name");
}

Member member;
/*이메일로 회원 가입 여부 확인*/
if (!memberRepository.existsByEmail(email) && !Objects.equals(password, DEFAULT_STRING)) {
memberService.createMember(email, password, picture, nickname, name);
}
member = memberRepository.findMemberByEmail(email);

return new AuthDetails(member, attributes, Collections.singleton(new SimpleGrantedAuthority(member.getMemberRole().getRole())));
}

// -------------google response------------
// {
// "id" : "00000000000000000000",
// "email" : "[email protected]",
// "verified_email" : true,
// "name" : "홍길동",
// "given_name" : "길동",
// "family_name" : "홍",
// "picture" : "https://url 경로",
// "locale" : "ko"
// }

// -------------kakao response------------
// {
// "id":00000000000000000000,
// "properties":{
// "nickname":"홍길동",
// "profile_image":"https://url 경로"
// },
// "kakao_account":{
// "email":"[email protected]",
// }
// }

// -------------naver response------------
// {
// "response": {
// "email": "[email protected]",
// "nickname": "홍길동",
// "profile_image": "https://url 경로"
// "id": "00000000000000000000",
// "name": "홍길동",
// }
}
Loading

0 comments on commit d574de2

Please sign in to comment.