Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feat] 카카오 로그인 초기 세팅 #6

Merged
merged 13 commits into from
Jan 19, 2024
Merged
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-webflux'


implementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-ui', version: '2.3.0'
testImplementation group: 'org.springdoc', name: 'springdoc-openapi-starter-webmvc-api', version: '2.3.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import com.ttubeog.global.DefaultAssert;
import com.ttubeog.global.config.security.auth.OAuth2UserInfo;
import com.ttubeog.global.config.security.auth.OAuth2UserInfoFactory;
import com.ttubeog.global.config.security.token.UserPrincipal;
import com.ttubeog.global.config.security.token.MemberPrincipal;
import lombok.RequiredArgsConstructor;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
Expand Down Expand Up @@ -37,21 +37,21 @@ public OAuth2User loadUser(OAuth2UserRequest oAuth2UserRequest) throws OAuth2Aut
private OAuth2User processOAuth2User(OAuth2UserRequest oAuth2UserRequest, OAuth2User oAuth2User) {
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfoFactory.getOAuth2UserInfo(oAuth2UserRequest.getClientRegistration().getRegistrationId(), oAuth2User.getAttributes());
DefaultAssert.isAuthentication(!oAuth2UserInfo.getEmail().isEmpty());

Optional<Member> userOptional = memberRepository.findByEmail(oAuth2UserInfo.getEmail());
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 부분만 memberOptional이 아니고 userOptional인 건 이유가 있나요?!
궁금해서 질문 남깁니다 ,,!

Member member;
if(userOptional.isPresent()) {
member = userOptional.get();
DefaultAssert.isAuthentication(member.getProvider().equals(Provider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId())));
member = updateExistingUser(member, oAuth2UserInfo);
member = updateExistingMember(member, oAuth2UserInfo);
} else {
member = registerNewUser(oAuth2UserRequest, oAuth2UserInfo);
member = registerNewMember(oAuth2UserRequest, oAuth2UserInfo);
}

return UserPrincipal.create(member, oAuth2User.getAttributes());
return MemberPrincipal.create(member, oAuth2User.getAttributes());
}

private Member registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
private Member registerNewMember(OAuth2UserRequest oAuth2UserRequest, OAuth2UserInfo oAuth2UserInfo) {
Member member = Member.builder()
.provider(Provider.valueOf(oAuth2UserRequest.getClientRegistration().getRegistrationId()))
.providerId(oAuth2UserInfo.getId())
Expand All @@ -64,7 +64,7 @@ private Member registerNewUser(OAuth2UserRequest oAuth2UserRequest, OAuth2UserIn
return memberRepository.save(member);
}

private Member updateExistingUser(Member member, OAuth2UserInfo oAuth2UserInfo) {
private Member updateExistingMember(Member member, OAuth2UserInfo oAuth2UserInfo) {

member.updateName(oAuth2UserInfo.getName());
member.updateImageUrl(oAuth2UserInfo.getImageUrl());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import com.ttubeog.domain.member.domain.Member;
import com.ttubeog.domain.member.domain.repository.MemberRepository;
import com.ttubeog.global.DefaultAssert;
import com.ttubeog.global.config.security.token.UserPrincipal;
import com.ttubeog.global.config.security.token.MemberPrincipal;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
Expand All @@ -15,7 +15,7 @@

@RequiredArgsConstructor
@Service
public class CustomUserDetailsService implements UserDetailsService{
public class CustomMemberDetailsService implements UserDetailsService{

private final MemberRepository memberRepository;

Expand All @@ -24,18 +24,18 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep

Member member = memberRepository.findByEmail(email)
.orElseThrow(() ->
new UsernameNotFoundException("유저 정보를 찾을 수 없습니다.")
new UsernameNotFoundException("멤버 정보를 찾을 수 없습니다.")
);

return UserPrincipal.create(member);
return MemberPrincipal.create(member);
}

@Transactional
public UserDetails loadUserById(Long id) {
Optional<Member> user = memberRepository.findById(id);
DefaultAssert.isOptionalPresent(user);

return UserPrincipal.create(user.get());
return MemberPrincipal.create(user.get());
}

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

import com.ttubeog.domain.auth.dto.TokenMapping;
import com.ttubeog.global.config.security.OAuth2Config;
import com.ttubeog.global.config.security.token.UserPrincipal;
import com.ttubeog.global.config.security.token.MemberPrincipal;
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
Expand All @@ -24,10 +24,10 @@ public class CustomTokenProviderService {
private OAuth2Config oAuth2Config;

@Autowired
private CustomUserDetailsService customUserDetailsService;
private CustomMemberDetailsService customMemberDetailsService;

public TokenMapping refreshToken(Authentication authentication, String refreshToken) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
MemberPrincipal memberPrincipal = (MemberPrincipal) authentication.getPrincipal();
Date now = new Date();

Date accessTokenExpiresIn = new Date(now.getTime() + oAuth2Config.getAuth().getAccessTokenExpirationMsec());
Expand All @@ -37,21 +37,21 @@ public TokenMapping refreshToken(Authentication authentication, String refreshTo
Key key = Keys.hmacShaKeyFor(keyBytes);

String accessToken = Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setSubject(Long.toString(memberPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
.compact();

return TokenMapping.builder()
.userEmail(userPrincipal.getEmail())
.userEmail(memberPrincipal.getEmail())
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}

public TokenMapping createToken(Authentication authentication) {
UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();
MemberPrincipal memberPrincipal = (MemberPrincipal) authentication.getPrincipal();

Date now = new Date();

Expand All @@ -64,7 +64,7 @@ public TokenMapping createToken(Authentication authentication) {
Key key = Keys.hmacShaKeyFor(keyBytes);

String accessToken = Jwts.builder()
.setSubject(Long.toString(userPrincipal.getId()))
.setSubject(Long.toString(memberPrincipal.getId()))
.setIssuedAt(new Date())
.setExpiration(accessTokenExpiresIn)
.signWith(key, SignatureAlgorithm.HS512)
Expand All @@ -76,7 +76,7 @@ public TokenMapping createToken(Authentication authentication) {
.compact();

return TokenMapping.builder()
.userEmail(userPrincipal.getEmail())
.userEmail(memberPrincipal.getEmail())
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
Expand All @@ -94,13 +94,13 @@ public Long getUserIdFromToken(String token) {

public UsernamePasswordAuthenticationToken getAuthenticationById(String token){
Long userId = getUserIdFromToken(token);
UserDetails userDetails = customUserDetailsService.loadUserById(userId);
UserDetails userDetails = customMemberDetailsService.loadUserById(userId);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
return authentication;
}

public UsernamePasswordAuthenticationToken getAuthenticationByEmail(String email){
UserDetails userDetails = customUserDetailsService.loadUserByUsername(email);
UserDetails userDetails = customMemberDetailsService.loadUserByUsername(email);
UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
return authentication;
}
Expand Down
120 changes: 120 additions & 0 deletions src/main/java/com/ttubeog/domain/auth/application/JwtTokenService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package com.ttubeog.domain.auth.application;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Date;
import java.util.Random;

@Service
public class JwtTokenService implements InitializingBean {

private final String secretKey;
private final long accessTokenExpirationInSeconds;
private final long refreshTokenExpirationInSeconds;
private static Key key;


public JwtTokenService(
@Value("${jwt.secret-key}") String secretKey,
@Value("${jwt.access-expired-time}") long accessTokenExpirationInSeconds,
@Value("${jwt.refresh-expired-time}") long refreshTokenExpirationInSeconds
) {
this.secretKey = secretKey;
this.accessTokenExpirationInSeconds = accessTokenExpirationInSeconds;
this.refreshTokenExpirationInSeconds = refreshTokenExpirationInSeconds;
}

@Override
public void afterPropertiesSet() throws Exception {
key = getKeyFromBase64EncodedKey(encodeBase64SecretKey(secretKey));
}

// JWT 토큰 생성
public String createToken(String payload, long expireLength) {
// 토큰에 포함될 정보
Claims claims = Jwts.claims().setSubject(payload);
// 현재 날짜와 시간
Date now = new Date();
// 유효기간
Date validity = new Date(now.getTime() + expireLength);

return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(validity)
.signWith(key, SignatureAlgorithm.HS256) // 토큰 서명
.compact();
}

// 액세스 토큰 생성
public String createAccessToken(String payload) {
return createToken(payload, accessTokenExpirationInSeconds);
}

// 리프레시 토큰 생성
public String createRefreshToken() {
byte[] array = new byte[7];
new Random().nextBytes(array);
String newPayload = new String(array, StandardCharsets.UTF_8);

return createToken(newPayload, refreshTokenExpirationInSeconds);
}

public String getPayload(String token) {
try {
return Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token)
.getBody()
.getSubject();
} catch (ExpiredJwtException e) {
return e.getClaims().getSubject();
}
}

public boolean validateToken(String token) {
try {
Jws<Claims> claimsJws = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(token);
return !claimsJws.getBody().getExpiration().before(new Date());
} catch (JwtException | IllegalArgumentException exception) {
return false;
}
}


private String encodeBase64SecretKey(String secretKey) {
return Encoders.BASE64.encode(secretKey.getBytes(StandardCharsets.UTF_8));
}

private Key getKeyFromBase64EncodedKey(String encodedSecretKey) {
byte[] keyBytes = Decoders.BASE64.decode(encodedSecretKey);

Key key = Keys.hmacShaKeyFor(keyBytes);

return key;
}

// 클라이언트 쿠키에 리프레시 토큰을 저장
public void addRefreshTokenToCookie(String refreshToken, HttpServletResponse response) {
Long age = refreshTokenExpirationInSeconds;
Cookie cookie = new Cookie("refresh_token", refreshToken);
cookie.setPath("/");
cookie.setMaxAge(age.intValue());
cookie.setHttpOnly(true);
response.addCookie(cookie);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com.ttubeog.domain.auth.application;


import com.ttubeog.domain.auth.dto.KakaoInfoDto;
import com.ttubeog.domain.member.application.MemberService;
import com.ttubeog.domain.member.dto.MemberDto;
import lombok.RequiredArgsConstructor;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.stereotype.Service;
import org.springframework.web.reactive.function.client.WebClient;

import java.util.Map;

@RequiredArgsConstructor
@Service
public class KakaoOauthService {
private final MemberService memberService;

// 카카오 API를 호출해서 AccessToken으로 멤버 정보를 가져오는 로직
public Map<String, Object> getMemberInfoByToken(String accessToken) {
return WebClient.create()
.get()
.uri("https://kapi.kakao.com/v2/user/me")
.headers(httpHeaders -> httpHeaders.setBasicAuth(accessToken))
.retrieve()
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {
})
.block();
}


// 카카오 API에서 가져온 멤버 정보를 DB 저장, 업데이트
public MemberDto getMemberProfileByToken(String accessToken) {
Map<String, Object> memberInfoByToken = getMemberInfoByToken(accessToken);
KakaoInfoDto kakaoInfoDto = new KakaoInfoDto(memberInfoByToken);
MemberDto memberDto = MemberDto.builder()
.id(kakaoInfoDto.getId())
.email(kakaoInfoDto.getEmail())
.platform("kakao")
.build();

if(memberService.findById(memberDto.getId()) != null) {
memberService.update(memberDto);
} else {
memberService.save(memberDto);
}
return memberDto;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.ttubeog.domain.auth.application;

import com.ttubeog.domain.member.application.MemberService;
import com.ttubeog.domain.member.dto.MemberDto;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

@RequiredArgsConstructor
@Service
public class OauthService {
private final MemberService memberService;
private final JwtTokenService jwtTokenService;
private final KakaoOauthService kakaoOauthService;

// 카카오 로그인
public String loginWithKakao(String accessToken, HttpServletResponse response) {
MemberDto memberDto = kakaoOauthService.getMemberProfileByToken(accessToken);
return getTokens(memberDto.getId(), response);
}

// 액세스, 리프레시 토큰 생성
public String getTokens(Long id, HttpServletResponse response) {
final String accessToken = jwtTokenService.createAccessToken(id.toString());
final String refreshToken = jwtTokenService.createRefreshToken();

MemberDto memberDto = memberService.findById(id);
memberDto.setRefreshToken(refreshToken);
memberService.updateRefreshToken(memberDto);

return accessToken;
}

// 리프레시 토큰을 액세스 토큰으로 갱신
public String refreshToAccessToken(String refreshToken) {
MemberDto memberDto = memberService.findByRefreshToken(refreshToken);

if(memberDto == null) {

}

return jwtTokenService.createAccessToken(memberDto.getId().toString());
}
}
Loading
Loading