Skip to content
Merged
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
8 changes: 7 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.mysql:mysql-connector-j:8.2.0'
implementation 'org.hibernate:hibernate-core:6.3.0.CR1'
implementation 'org.springframework.data:spring-data-redis:3.1.2'
implementation 'io.jsonwebtoken:jjwt:0.9.1'
implementation 'org.springframework.security:spring-security-core:6.1.2'
implementation 'org.springframework.security:spring-security-config:6.1.2'
implementation 'org.springframework.security:spring-security-web:6.1.2'
implementation 'io.lettuce:lettuce-core:6.2.5.RELEASE'
implementation 'javax.xml.bind:jaxb-api:2.4.0-b180830.0359'
compileOnly 'org.projectlombok:lombok:1.18.26'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.example.solidconnection.auth.controller;

import com.example.solidconnection.auth.dto.KakaoCodeDto;
import com.example.solidconnection.auth.dto.KakaoOauthResponseDto;
import com.example.solidconnection.auth.service.KakaoOAuthService;
import com.example.solidconnection.custom.response.CustomResponse;
import com.example.solidconnection.custom.response.DataResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("auth")
@RequiredArgsConstructor
public class AuthController {
private final KakaoOAuthService kakaoOAuthService;

@PostMapping("/kakao")
public CustomResponse signUp(@RequestBody KakaoCodeDto kakaoCodeDto) {
KakaoOauthResponseDto kakaoOauthResponseDto = kakaoOAuthService.processOauth(kakaoCodeDto.getCode());
return new DataResponse<>(kakaoOauthResponseDto);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.solidconnection.auth.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FirstAccessResponseDto extends KakaoOauthResponseDto {
private boolean registered;
private String nickname;
private String email;
private String profileImageUrl;
private String kakaoOauthToken;

public static FirstAccessResponseDto fromKakaoUserInfo(KakaoUserInfoDto kakaoUserInfoDto, String kakaoOauthToken){
return FirstAccessResponseDto.builder()
.registered(false)
.email(kakaoUserInfoDto.getKakaoAccount().getEmail())
.profileImageUrl(kakaoUserInfoDto.getKakaoAccount().getProfile().getProfileImageUrl())
.nickname(kakaoUserInfoDto.getKakaoAccount().getProfile().getNickname())
.kakaoOauthToken(kakaoOauthToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.solidconnection.auth.dto;

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

@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public class KakaoAccount {
@JsonProperty("profile")
private KakaoProfile profile;
private String email;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.example.solidconnection.auth.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@AllArgsConstructor
@NoArgsConstructor
public class KakaoCodeDto {
private String code;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package com.example.solidconnection.auth.dto;

public class KakaoOauthResponseDto {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.example.solidconnection.auth.dto;

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

@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public class KakaoProfile {
private String nickname;
@JsonProperty("profile_image_url")
private String profileImageUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.solidconnection.auth.dto;

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

@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public class KakaoTokenDto {
@JsonProperty("access_token")
private String accessToken;
@JsonProperty("refresh_token")
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.example.solidconnection.auth.dto;

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

@Getter
@Setter
@JsonIgnoreProperties(ignoreUnknown = true)
@AllArgsConstructor
@NoArgsConstructor
public class KakaoUserInfoDto {
@JsonProperty("kakao_account")
private KakaoAccount kakaoAccount;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.solidconnection.auth.dto;

import lombok.*;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class SignInResponseDto extends KakaoOauthResponseDto {
private boolean registered;
private String accessToken;
private String refreshToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.example.solidconnection.auth.service;

import com.example.solidconnection.auth.dto.*;
import com.example.solidconnection.config.security.TokenProvider;
import com.example.solidconnection.config.security.TokenType;
import com.example.solidconnection.custom.exception.CustomException;
import com.example.solidconnection.siteuser.repository.SiteUserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.*;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;

import static com.example.solidconnection.custom.exception.ErrorCode.*;

@Service
@RequiredArgsConstructor
public class KakaoOAuthService {

private final RestTemplate restTemplate;
private final TokenProvider tokenProvider;
private final SiteUserRepository siteUserRepository;

@Value("${kakao.client_id}")
private String clientId;
@Value("${kakao.redirect_uri}")
private String redirectUri;
@Value("${kakao.token_url}")
private String tokenUrl;
@Value("${kakao.user_info_url}")
private String userInfoUrl;

public KakaoOauthResponseDto processOauth(String code) {
String kakaoAccessToken = getKakaoAccessToken(code);
KakaoUserInfoDto kakaoUserInfoDto = getKakaoUserInfo(kakaoAccessToken);
String email = kakaoUserInfoDto.getKakaoAccount().getEmail();
boolean isAlreadyRegistered = siteUserRepository.existsByEmail(email);
if (isAlreadyRegistered) {
return kakaoSignIn(email);
}
String kakaoOauthToken = tokenProvider.generateToken(email, TokenType.KAKAO_OAUTH);
return FirstAccessResponseDto.fromKakaoUserInfo(kakaoUserInfoDto, kakaoOauthToken);
}

private String getKakaoAccessToken(String code) {
// 카카오 엑세스 토큰 요청
ResponseEntity<KakaoTokenDto> response = restTemplate.exchange(
buildTokenUri(code),
HttpMethod.POST,
null,
KakaoTokenDto.class
);

// 응답 예외처리
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody().getAccessToken();
} else {
throw new CustomException(KAKAO_ACCESS_TOKEN_FAIL);
}
}

// 카카오에게 엑세스 토큰 발급 요청하는 URI 생성
private String buildTokenUri(String code) {
return UriComponentsBuilder.fromHttpUrl(tokenUrl)
.queryParam("grant_type", "authorization_code")
.queryParam("client_id", clientId)
.queryParam("redirect_uri", redirectUri)
.queryParam("code", code)
.toUriString();
}

private KakaoUserInfoDto getKakaoUserInfo(String accessToken) {
// 카카오 엑세스 토큰을 헤더에 담은 HttpEntity
HttpHeaders headers = new HttpHeaders();
headers.setBearerAuth(accessToken);
HttpEntity<?> entity = new HttpEntity<>(headers);

// 사용자의 정보 요청
ResponseEntity<KakaoUserInfoDto> response = restTemplate.exchange(
userInfoUrl,
HttpMethod.GET,
entity,
KakaoUserInfoDto.class
);

// 응답 예외처리
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
return response.getBody();
} else {
throw new CustomException(KAKAO_USER_INFO_FAIL);
}
}

private SignInResponseDto kakaoSignIn(String email) {
siteUserRepository.findByEmail(email)
.orElseThrow(() -> new CustomException(EMAIL_NOT_FOUND));

var accessToken = tokenProvider.generateToken(email, TokenType.ACCESS);
var refreshToken = tokenProvider.saveToken(email, TokenType.REFRESH);
return SignInResponseDto.builder()
.registered(true)
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.example.solidconnection.config.redis;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
public class RedisConfig {

private final String redisHost;

private final int redisPort;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
return new LettuceConnectionFactory(redisHost, redisPort);
}

public RedisConfig(@Value("${spring.data.redis.host}") final String redisHost,
@Value("${spring.data.redis.port}") final int redisPort) {
this.redisHost = redisHost;
this.redisPort = redisPort;
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setConnectionFactory(redisConnectionFactory());
return redisTemplate;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.example.solidconnection.config.rest;

import org.springframework.boot.web.client.RestTemplateBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

import java.time.Duration;

@Configuration
public class RestTemplateConfig {

@Bean
public RestTemplate restTemplate(RestTemplateBuilder restTemplateBuilder) {
return restTemplateBuilder
.setConnectTimeout(Duration.ofSeconds(5))
.setReadTimeout(Duration.ofSeconds(5))
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.example.solidconnection.config.security;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Authentication Failed: " + authException.getMessage());
}

public void customCommence(HttpServletResponse response) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write("Authentication Failed: You are logged out." );
}
}
Loading