Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'

// OAuth2 login
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.umc8th.entity.Member;
import com.example.umc8th.global.apiPayload.CustomResponse;
import com.example.umc8th.service.command.MemberCommandService;
import com.example.umc8th.service.oauth.OAuth2Service;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

Expand All @@ -14,6 +15,7 @@
public class MemberController {

private final MemberCommandService memberCommandService;
private final OAuth2Service oAuth2Service;

@PostMapping("/sign-up")
public CustomResponse<MemberResponseDTO.SignUpResponseDTO> signUp(@RequestBody MemberRequestDTO.SignUpRequestDTO dto) {
Expand All @@ -26,4 +28,9 @@ public CustomResponse<MemberResponseDTO.LoginResponseDTO> login(@RequestBody Mem
MemberResponseDTO.LoginResponseDTO member = memberCommandService.login(dto);
return CustomResponse.ok(member);
}

@GetMapping("/oauth2/callback/kakao")
public CustomResponse<MemberResponseDTO.MemberTokenDTO> loginWithKakao(@RequestParam("code") String code) {
return CustomResponse.ok(oAuth2Service.login(code));
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/example/umc8th/dto/MemberResponseDTO.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,19 @@ public static LoginResponseDTO from(Member member) {
}

}

@Getter
@Builder
public static class MemberTokenDTO{
private Long id;
private String accessToken;
private String refreshToken;
public static MemberTokenDTO from(Member member) {
return MemberTokenDTO.builder()
.id(member.getId())
.accessToken(member.getAccessToken())
.refreshToken(member.getRefreshToken())
.build();
}
}
}
54 changes: 54 additions & 0 deletions src/main/java/com/example/umc8th/dto/OAuth2DTO.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package com.example.umc8th.dto;

import lombok.Getter;

public class OAuth2DTO {

@Getter
public static class OAuth2TokenDTO {
String token_type;
String access_token;
String refresh_token;
Long expires_in;
Long refresh_token_expires_in;
String scope;
}

@Getter
public static class KakaoProfile {
private Long id;
private String connected_at;
private Properties properties;
private KakaoAccount kakao_account;

@Getter
public class Properties {
private String nickname;
private String profile_image;
private String thumbnail_image;
}

@Getter
public class KakaoAccount {
private String email;
private Boolean is_email_verified;
private Boolean email_needs_agreement;
private Boolean has_email;
private Boolean profile_nickname_needs_agreement;
private Boolean profile_image_needs_agreement;
private Boolean email_needs_argument;
private Boolean is_email_valid;
private Profile profile;

@Getter
public class Profile {
private String nickname;
private String thumbnail_image_url;
private String profile_image_url;
private Boolean is_default_nickname;
private Boolean is_default_image;
}
}
}
}

6 changes: 6 additions & 0 deletions src/main/java/com/example/umc8th/entity/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ public class Member {
@Column(name = "refresh_token", length = 1000)
private String refreshToken;

@Column(name="email", unique = true, nullable = true)
private String email;

@Column(name="role", unique = true, nullable = true)
private String role;

public void updateTokens(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
public enum MemberErrorCode implements BaseErrorCode {

NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "멀버λ₯Ό μ°Ύμ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."),
BAD_CREDENTIAL(HttpStatus.BAD_REQUEST, "MEMBER401", "μœ νš¨ν•˜μ§€ μ•ŠλŠ” ν† ν°μž…λ‹ˆλ‹€.");
BAD_CREDENTIAL(HttpStatus.BAD_REQUEST, "MEMBER401", "μœ νš¨ν•˜μ§€ μ•ŠλŠ” ν† ν°μž…λ‹ˆλ‹€."),

OAUTH_TOKEN_FAIL(HttpStatus.MULTI_STATUS, "MEMBER2400", "OAuth 토큰을 λ³€κ²½ν•˜μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€."),
OAUTH_USER_INFO_FAIL(HttpStatus.MULTI_STATUS, "MEMBER2500", "μ‚¬μš©μž 정보λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.");

private final HttpStatus httpStatus;
private final String code;
Expand Down
14 changes: 7 additions & 7 deletions src/main/java/com/example/umc8th/global/config/JwtFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,18 +33,18 @@ protected void doFilterInternal(HttpServletRequest request,
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
String token = authorizationHeader.substring(7);

if (!jwtUtil.isValid(token)) {
throw new JwtException("Invalid JWT token");
}
// if (!jwtUtil.isValid(token)) {
// throw new JwtException("Invalid JWT token");
// }

String username = jwtUtil.getUsername(token);
if (username == null) {
throw new JwtException("Username is null in token");
}
// if (username == null) {
// throw new JwtException("Username is null in token");
// }

UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);

UsernamePasswordAuthenticationToken authentication =
Authentication authentication =
new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());

SecurityContextHolder.getContext().setAuthentication(authentication);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
Expand Down Expand Up @@ -32,6 +33,7 @@ public class SecurityConfig {
private String[] allowUrl = {
"/auth/sign-up",
"/auth/login",
"/auth/oauth2/**",
"/swagger-ui/**",
"/swagger-resources/**",
"/v3/api-docs/**",
Expand All @@ -55,6 +57,10 @@ SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.authenticationEntryPoint(customEntryPoint)
.accessDeniedHandler(customAccessDeniedHandler)
)

.formLogin(AbstractHttpConfigurer::disable)
.oauth2Login(Customizer.withDefaults())

.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class)
/* // formLogin μ„€μ •
.formLogin(formLogin -> formLogin
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@
public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByUsername(String username);
Optional<Member> findByEmail(String email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import com.example.umc8th.entity.Member;
import com.example.umc8th.dto.MemberResponseDTO;

import java.util.List;

public interface TokenCommandService {
MemberResponseDTO.LoginResponseDTO createLoginToken(Member member);
List<String> createTokens(Member member);
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

@Service
@RequiredArgsConstructor
public class TokenCommandServiceImpl implements TokenCommandService {
Expand All @@ -31,4 +34,11 @@ public MemberResponseDTO.LoginResponseDTO createLoginToken(Member member) {
.refreshToken(refreshToken)
.build();
}

public List<String> createTokens(Member member){
List<String> result = new ArrayList<>();
result.add(jwtUtil.createAccessToken(member));
result.add(jwtUtil.createRefreshToken(member));
return result;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.example.umc8th.service.oauth;

import com.example.umc8th.dto.MemberResponseDTO;

public interface OAuth2Service {
MemberResponseDTO.MemberTokenDTO login(String code);
}
157 changes: 157 additions & 0 deletions src/main/java/com/example/umc8th/service/oauth/OAuth2ServiceImpl.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
package com.example.umc8th.service.oauth;

import com.example.umc8th.dto.MemberResponseDTO;
import com.example.umc8th.dto.OAuth2DTO;
import com.example.umc8th.entity.Member;
import com.example.umc8th.exception.MemberErrorCode;
import com.example.umc8th.exception.MemberException;
import com.example.umc8th.global.config.JwtUtil;
import com.example.umc8th.repository.MemberRepository;
import com.example.umc8th.service.command.TokenCommandService;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;

import java.util.List;
import java.util.Optional;
import java.util.UUID;

// OAuth2Service Impl
@Service
@RequiredArgsConstructor
@Slf4j
public class OAuth2ServiceImpl implements OAuth2Service{

@Value("${spring.security.oauth2.client.provider.kakao.token-uri}")
private String tokenURI; // Resource Server에 토큰 μš”μ²­μ‹œ μ‚¬μš©ν•  URI

@Value("${spring.security.oauth2.client.provider.kakao.user-info-uri}")
private String userInfoURI; // μ‚¬μš©μž 정보 κ°€μ Έμ˜¬ λ•Œ μ‚¬μš©ν•  URI

@Value("${spring.security.oauth2.client.registration.kakao.client-id}")
private String clientId; // API KEY

@Value("${spring.security.oauth2.client.registration.kakao.redirect-uri}")
private String redirectURI; // μ„€μ •ν•œ Redirect uri

private final MemberRepository memberRepository;
private final TokenCommandService tokenCommandService;
private final ObjectMapper objectMapper = new ObjectMapper();

@Override
public MemberResponseDTO.MemberTokenDTO login(String code) {

OAuth2DTO.OAuth2TokenDTO tokenDTO = getKakaoAccessToken(code);

OAuth2DTO.KakaoProfile profile = getKakaoProfile(tokenDTO.getAccess_token());

Member member = processMemberRegistration(profile);

return generateTokenResponse(member);
}

private OAuth2DTO.OAuth2TokenDTO getKakaoAccessToken(String code) {
HttpHeaders headers = new HttpHeaders();
headers.add("Content-Type", "application/x-www-form-urlencoded");

MultiValueMap<String, String> body = new LinkedMultiValueMap<>();
body.add("grant_type", "authorization_code");
body.add("client_id", clientId);
body.add("redirect_uri", redirectURI);
body.add("code", code);

ResponseEntity<String> response = new RestTemplate().exchange(
tokenURI,
HttpMethod.POST,
new HttpEntity<>(body, headers),
String.class
);

return parseResponse(response, OAuth2DTO.OAuth2TokenDTO.class, MemberErrorCode.OAUTH_TOKEN_FAIL);
}

private OAuth2DTO.KakaoProfile getKakaoProfile(String accessToken) {
RestTemplate restTemplate = new RestTemplate();
HttpHeaders headers = new HttpHeaders();
headers.add("Authorization", "Bearer " + accessToken);
headers.add("Content-Type", "application/x-www-form-urlencoded;charset=utf-8");

ResponseEntity<String> response = restTemplate.exchange(
userInfoURI,
HttpMethod.GET,
new HttpEntity<>(headers),
String.class
);

return parseResponse(response, OAuth2DTO.KakaoProfile.class, MemberErrorCode.OAUTH_USER_INFO_FAIL);
}

private <T> T parseResponse(ResponseEntity<String> response, Class<T> valueType, MemberErrorCode errorCode) {
try {
return objectMapper.readValue(response.getBody(), valueType);
} catch (Exception e) {
throw new MemberException(errorCode);
}
}

private Member processMemberRegistration(OAuth2DTO.KakaoProfile profile) {
String email = extractEmail(profile);

return memberRepository.findByEmail(email)
.orElseGet(() -> registerNewMember(email));
}

private String extractEmail(OAuth2DTO.KakaoProfile profile) {
return Optional.ofNullable(profile.getKakao_account())
.map(OAuth2DTO.KakaoProfile.KakaoAccount::getEmail) // email κ·ΈλŒ€λ‘œ μ‚¬μš©
.orElseThrow(() -> new MemberException(MemberErrorCode.OAUTH_TOKEN_FAIL));
}

private Member registerNewMember(String email) {
Member tempMember = Member.builder()
.email(email)
.password(generateTemporaryPassword())
.username(extractUsernameFromEmail(email))
.role("ROLE_USER")
.build();

List<String> tokens = tokenCommandService.createTokens(tempMember);
Member member = Member.builder()
.email(tempMember.getEmail())
.password(tempMember.getPassword())
.username(tempMember.getUsername())
.role(tempMember.getRole())
.accessToken(tokens.get(0))
.refreshToken(tokens.get(1))
.build();

return memberRepository.save(member);
}

private String generateTemporaryPassword() {
return UUID.randomUUID().toString().replace("-", "").substring(0, 15);
}

private String extractUsernameFromEmail(String email) {
return email.split("@")[0];
}

private MemberResponseDTO.MemberTokenDTO generateTokenResponse(Member member) {
List<String> tokens = tokenCommandService.createTokens(member);

return MemberResponseDTO.MemberTokenDTO.builder()
.accessToken(member.getAccessToken())
.refreshToken(member.getRefreshToken())
.id(member.getId())
.build();
}
}