Skip to content

Commit

Permalink
[JT-30] OAuth2 네이버 로그인 기능 구현
Browse files Browse the repository at this point in the history
  • Loading branch information
ymkim97 authored Sep 11, 2023
2 parents 2623592 + 233e5d9 commit 2946a0d
Show file tree
Hide file tree
Showing 22 changed files with 386 additions and 150 deletions.
3 changes: 3 additions & 0 deletions module-application/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,9 @@
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.security.entity.RefreshToken;
import com.devtoon.jtoon.security.jwt.application.JwtProvider;
import com.devtoon.jtoon.security.repository.RefreshTokenRepository;
import com.devtoon.jtoon.security.request.LogInReq;
import com.devtoon.jtoon.security.response.LoginRes;
import java.util.Optional;
import lombok.RequiredArgsConstructor;

@Service
Expand All @@ -22,7 +20,7 @@ public class AuthService {

private final MemberRepository memberRepository;
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final JwtService jwtService;

private final RefreshTokenRepository refreshTokenRepository;

Expand All @@ -36,29 +34,18 @@ public LoginRes login(LogInReq logInReq) {
}

member.updateLastLogin();
String accessToken = jwtProvider.generateAccessToken(logInReq.email());
String refreshToken = jwtProvider.generateRefreshToken();
Optional<RefreshToken> findToken = refreshTokenRepository.findById(logInReq.email());
RefreshToken token = checkAndGetToken(findToken, refreshToken, logInReq.email());
String accessToken = jwtService.generateAccessToken(logInReq.email());
String refreshToken = jwtService.generateRefreshToken();
RefreshToken token = RefreshToken.builder()
.refreshToken(refreshToken)
.email(logInReq.email())
.build();
refreshTokenRepository.save(token);

return LoginRes.of(accessToken, refreshToken);
return LoginRes.toDto(accessToken, refreshToken);
}

public boolean isPasswordSame(String rawPassword, String memberPassword) {
return passwordEncoder.matches(rawPassword, memberPassword);
}

private RefreshToken checkAndGetToken(Optional<RefreshToken> findToken, String refreshToken, String email) {
if (findToken.isPresent()) {
RefreshToken token = findToken.get();
token.updateToken(refreshToken);
return token;
}

return RefreshToken.builder()
.email(email)
.refreshToken(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package com.devtoon.jtoon.security.application;

import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.devtoon.jtoon.member.entity.LoginType;
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.security.domain.jwt.MemberThreadLocal;
import com.devtoon.jtoon.security.domain.oauth.CustomOAuth2User;
import com.devtoon.jtoon.security.domain.oauth.OAuthAttributes;
import java.util.Collections;
import java.util.Map;
import lombok.RequiredArgsConstructor;

@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final MemberRepository memberRepository;

@Override
@Transactional
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oauth2User = delegate.loadUser(userRequest);
String registrationId = userRequest.getClientRegistration().getRegistrationId();
LoginType loginType = LoginType.from(registrationId);
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails()
.getUserInfoEndpoint()
.getUserNameAttributeName();
Map<String, Object> attributes = oauth2User.getAttributes();
OAuthAttributes extractedAttributes = OAuthAttributes.of(loginType, userNameAttributeName, attributes);
Member member = generateMember(extractedAttributes);
MemberThreadLocal.setMember(member);

return new CustomOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(member.getRole().toString())),
attributes,
userNameAttributeName
);
}

private Member generateMember(OAuthAttributes extractedAttributes) {
Member member = memberRepository.findByEmail(extractedAttributes.email())
.orElse(null);

if (member == null) {
return memberRepository.save(extractedAttributes.toEntity());
}

if (extractedAttributes.loginType() != member.getLoginType()) {
throw new RuntimeException("이미 다른 소셜 로그인으로 등록된 회원입니다");
}

return member;
}
}

Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package com.devtoon.jtoon.security.jwt.application;
package com.devtoon.jtoon.security.application;

import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.repository.MemberRepository;
import com.devtoon.jtoon.security.jwt.domain.CustomUserDetails;
import com.devtoon.jtoon.security.domain.jwt.CustomUserDetails;
import lombok.RequiredArgsConstructor;

@Service
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package com.devtoon.jtoon.security.jwt.application;
package com.devtoon.jtoon.security.application;

import static com.devtoon.jtoon.security.util.SecurityConstant.*;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.devtoon.jtoon.security.domain.jwt.CustomUserDetails;
import com.devtoon.jtoon.security.entity.RefreshToken;
import com.devtoon.jtoon.security.jwt.domain.CustomUserDetails;
import com.devtoon.jtoon.security.repository.RefreshTokenRepository;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
Expand All @@ -28,10 +28,10 @@
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Component
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class JwtProvider {
public class JwtService {

@Value("${jwt.iss}")
private String ISS;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import com.devtoon.jtoon.security.application.CustomOAuth2UserService;
import com.devtoon.jtoon.security.application.JwtService;
import com.devtoon.jtoon.security.filter.JwtAuthenticationFilter;
import com.devtoon.jtoon.security.jwt.application.JwtProvider;
import com.devtoon.jtoon.security.handler.OAuth2SuccessHandler;
import lombok.RequiredArgsConstructor;

@Configuration
Expand All @@ -22,7 +24,9 @@
public class WebSecurityConfiguration {

private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtProvider jwtProvider;
private final JwtService jwtService;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;

@Bean
public PasswordEncoder encoder() {
Expand All @@ -39,9 +43,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.anyRequest().permitAll())
.csrf(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthenticationFilter(handlerExceptionResolver, jwtProvider),
.addFilterBefore(new JwtAuthenticationFilter(handlerExceptionResolver, jwtService),
UsernamePasswordAuthenticationFilter.class)
.oauth2Login(login -> login
.userInfoEndpoint(endpoint -> endpoint.userService(customOAuth2UserService))
.successHandler(oAuth2SuccessHandler))
;
return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.devtoon.jtoon.security.jwt.domain;
package com.devtoon.jtoon.security.domain.jwt;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.devtoon.jtoon.security.jwt.domain;
package com.devtoon.jtoon.security.domain.jwt;

import com.devtoon.jtoon.member.entity.Member;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.devtoon.jtoon.security.domain.oauth;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

import java.util.Collection;
import java.util.Map;

public class CustomOAuth2User extends DefaultOAuth2User {

public CustomOAuth2User(
Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes,
String nameAttributeKey
) {
super(authorities, attributes, nameAttributeKey);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package com.devtoon.jtoon.security.domain.oauth;

import com.devtoon.jtoon.member.entity.Gender;
import com.devtoon.jtoon.member.entity.LoginType;
import com.devtoon.jtoon.member.entity.Member;
import com.devtoon.jtoon.member.entity.Role;
import java.util.Map;
import java.util.UUID;
import lombok.AccessLevel;
import lombok.Builder;

@Builder(access = AccessLevel.PRIVATE)
public record OAuthAttributes(
String nameAttributeKey,
String email,
String name,
String nickname,
String mobile,
String gender,
String password,
LoginType loginType
) {

public static OAuthAttributes of(LoginType loginType, String nameAttributeKey, Map<String, Object> attributes) {
return switch (loginType) {
case NAVER -> ofNaver(loginType, nameAttributeKey, attributes);
case KAKAO -> ofKakao(loginType, nameAttributeKey, attributes);
case LOCAL -> throw new IllegalArgumentException("안돼");
};
}

private static OAuthAttributes ofNaver(
LoginType loginType,
String nameAttributeKey,
Map<String, Object> attributes
) {
Map<String, Object> response = (Map<String, Object>)attributes.get("response");
String naverEmail = (String)response.get("email");
String naverNickname = response.get("nickname") == null ? naverEmail : (String)response.get("nickname");
String naverPhone = ((String)response.get("mobile")).replace("-", "");

return OAuthAttributes.builder()
.nameAttributeKey(nameAttributeKey)
.email(naverEmail)
.nickname(naverNickname)
.mobile(naverPhone)
.name((String)response.get("name"))
.gender((String)response.get("gender"))
.loginType(loginType)
.build();
}

private static OAuthAttributes ofKakao(LoginType loginType, String nameAttributeKey,
Map<String, Object> attributes) {
// TODO : KAKAO 추입 도입 예정
return null;
}

public Member toEntity() {
return Member.builder()
.email(this.email)
.password(UUID.randomUUID().toString())
.name(this.name)
.nickname(this.nickname)
.gender(Gender.from(this.gender))
.phone(this.mobile)
.role(Role.USER)
.loginType(this.loginType)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;

import com.devtoon.jtoon.security.jwt.application.JwtProvider;
import com.devtoon.jtoon.security.jwt.domain.CustomUserDetails;
import com.devtoon.jtoon.security.jwt.domain.MemberThreadLocal;
import com.devtoon.jtoon.security.application.JwtService;
import com.devtoon.jtoon.security.domain.jwt.CustomUserDetails;
import com.devtoon.jtoon.security.domain.jwt.MemberThreadLocal;
import com.devtoon.jtoon.security.util.TokenCookie;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
Expand All @@ -24,7 +26,7 @@
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private final HandlerExceptionResolver handlerExceptionResolver;
private final JwtProvider jwtProvider;
private final JwtService jwtService;

@Override
protected void doFilterInternal(
Expand All @@ -36,8 +38,8 @@ protected void doFilterInternal(

if (accessToken != null && accessToken.startsWith(BEARER_VALUE)) {
try {
accessToken = accessToken.split(SPACE)[1];
if (!jwtProvider.isTokenValid(accessToken)) {
accessToken = accessToken.split(SPLIT_DATA)[1];
if (!jwtService.isTokenValid(accessToken)) {
String refreshToken = validateAndGetRefreshToken(request);
accessToken = regenerateTokens(refreshToken, response);
}
Expand All @@ -53,25 +55,27 @@ protected void doFilterInternal(

private String validateAndGetRefreshToken(HttpServletRequest request) {
String refreshToken = request.getHeader(REFRESH_TOKEN_HEADER);
refreshToken = refreshToken.split(SPACE)[1];
jwtProvider.isTokenValid(refreshToken);
jwtProvider.verifyRefreshTokenDb(refreshToken);
refreshToken = refreshToken.split(SPLIT_DATA)[1];
jwtService.isTokenValid(refreshToken);
jwtService.verifyRefreshTokenDb(refreshToken);

return refreshToken;
}

private String regenerateTokens(String refreshToken, HttpServletResponse response) {
String newAccessToken = jwtProvider.reGenerateAccessToken(refreshToken);
String newRefreshToken = jwtProvider.generateRefreshToken();
jwtProvider.updateRefreshTokenDb(newAccessToken, newRefreshToken);
response.setHeader(ACCESS_TOKEN_HEADER, BEARER_VALUE + newAccessToken);
response.setHeader(REFRESH_TOKEN_HEADER, BEARER_VALUE + newRefreshToken);
String newAccessToken = jwtService.reGenerateAccessToken(refreshToken);
String newRefreshToken = jwtService.generateRefreshToken();
jwtService.updateRefreshTokenDb(newAccessToken, newRefreshToken);
Cookie accessCookie = TokenCookie.of(ACCESS_TOKEN_HEADER, newAccessToken);
Cookie refreshCookie = TokenCookie.of(REFRESH_TOKEN_HEADER, newRefreshToken);
response.addCookie(accessCookie);
response.addCookie(refreshCookie);

return newAccessToken;
}

private void authenticate(String accessToken) {
Authentication auth = jwtProvider.getAuthentication(accessToken);
Authentication auth = jwtService.getAuthentication(accessToken);
CustomUserDetails customUserDetails = (CustomUserDetails)auth.getPrincipal();
SecurityContextHolder.getContext().setAuthentication(auth);
MemberThreadLocal.setMember(customUserDetails.getMember());
Expand Down
Loading

0 comments on commit 2946a0d

Please sign in to comment.