Skip to content

Commit

Permalink
Merge pull request #6 from kduoh99/Feat/#4
Browse files Browse the repository at this point in the history
Feat: 카카오 소셜 로그인 구현
  • Loading branch information
kduoh99 authored Oct 30, 2024
2 parents 5f019cb + c27d2c2 commit 1fc40b4
Show file tree
Hide file tree
Showing 23 changed files with 602 additions and 68 deletions.
59 changes: 36 additions & 23 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,43 +1,56 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
id 'java'
id 'org.springframework.boot' version '3.3.5'
id 'io.spring.dependency-management' version '1.1.6'
}

group = 'com.hackathon'
version = '0.0.1-SNAPSHOT'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.security:spring-security-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.11.5'

// Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-common:2.1.0'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.1.0'

}

tasks.named('test') {
useJUnitPlatform()
useJUnitPlatform()
}
39 changes: 39 additions & 0 deletions src/main/java/com/hackathon/momento/auth/api/AuthController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package com.hackathon.momento.auth.api;

import com.hackathon.momento.auth.api.dto.AuthResDto;
import com.hackathon.momento.global.oauth.KakaoOauthService;
import com.hackathon.momento.global.template.RspTemplate;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
@Tag(name = "회원가입/로그인", description = "회원가입/로그인을 담당하는 api 그룹")
@RequestMapping("/api/v1/auth")
public class AuthController {

private final KakaoOauthService kakaoOAuthService;

@GetMapping("/callback")
@Operation(
summary = "카카오 회원가입/로그인 콜백",
description = "카카오 로그인 후 리다이렉션된 URI입니다. 인가 코드를 받아서 accessToken을 요청하고, 회원가입 또는 로그인을 처리합니다.",
responses = {
@ApiResponse(responseCode = "200", description = "회원가입/로그인 성공"),
@ApiResponse(responseCode = "400", description = "잘못된 요청"),
@ApiResponse(responseCode = "500", description = "관리자 문의")
}
)
public RspTemplate<AuthResDto> kakaoCallback(@RequestParam(name = "code") String code) {
AuthResDto token = kakaoOAuthService.signUpOrLogin(code);
return new RspTemplate<>(HttpStatus.OK, "토큰 발급", token);
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/hackathon/momento/auth/api/dto/AuthResDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.hackathon.momento.auth.api.dto;

import lombok.Builder;

@Builder
public record AuthResDto(
String accessToken,
String refreshToken
) {
public static AuthResDto of(String accessToken, String refreshToken) {
return AuthResDto.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.hackathon.momento.auth.application;

import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor(access = AccessLevel.PROTECTED)
public class TokenRenewService {

private static final String REFRESH_TOKEN_PREFIX = "refreshToken:";

private final RedisTemplate<String, String> redisTemplate;

@Transactional
public void saveRefreshToken(String refreshToken, Long memberId) {
deleteExistingToken(memberId);

String key = REFRESH_TOKEN_PREFIX + memberId;
redisTemplate.opsForValue().set(key, refreshToken);
}

private void deleteExistingToken(Long memberId) {
String key = REFRESH_TOKEN_PREFIX + memberId;
redisTemplate.delete(key);
}
}
33 changes: 33 additions & 0 deletions src/main/java/com/hackathon/momento/global/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.hackathon.momento.global.config;

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.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String redisHost;

@Value("${spring.data.redis.port}")
private int redisPort;

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

@Bean
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, String> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(new StringRedisSerializer());
return template;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.hackathon.momento.global.config;

import com.hackathon.momento.global.jwt.JwtFilter;
import com.hackathon.momento.global.jwt.TokenProvider;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final TokenProvider tokenprovider;
private final String[] PERMIT_ALL_URLS = {
"swagger-ui/**",
"v3/api-docs/**",
"api/v1/auth/**"
};

@Bean
public SecurityFilterChain filterChain(HttpSecurity httpSecurity) throws Exception {
return httpSecurity
.httpBasic(AbstractHttpConfigurer::disable)
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS))
.formLogin(AbstractHttpConfigurer::disable)
.logout(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorizeRequests -> authorizeRequests
.requestMatchers(PERMIT_ALL_URLS).permitAll()
.requestMatchers("/api/v1/admin/**").hasRole("ADMIN") // 관리자만 해당 URL에 접근 가능
.anyRequest().authenticated()
)
.addFilterBefore(new JwtFilter(tokenprovider), UsernamePasswordAuthenticationFilter.class)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.hackathon.momento.global.config;

import io.swagger.v3.oas.models.Components;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

private Info apiInfo() {
return new Info()
.version("v1.0.0")
.title("Momento API")
.description("Momento API 명세서");
}

@Bean
public OpenAPI openAPI() {
String authHeader = "Authorization";

return new OpenAPI()
.info(apiInfo())
.addSecurityItem(new SecurityRequirement().addList(authHeader))
.components(new Components()
.addSecuritySchemes(authHeader, new SecurityScheme()
.name(authHeader)
.type(SecurityScheme.Type.HTTP)
.scheme("Bearer")
.bearerFormat("JWT")));
}
}
24 changes: 24 additions & 0 deletions src/main/java/com/hackathon/momento/global/entity/BaseEntity.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.hackathon.momento.global.entity;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import java.time.LocalDateTime;
import lombok.Getter;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public class BaseEntity {

@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;

@LastModifiedDate
@Column(nullable = false)
private LocalDateTime updatedAt;
}
32 changes: 32 additions & 0 deletions src/main/java/com/hackathon/momento/global/jwt/JwtFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.hackathon.momento.global.jwt;

import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import java.io.IOException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.GenericFilterBean;

@RequiredArgsConstructor
public class JwtFilter extends GenericFilterBean {

private final TokenProvider tokenProvider;

public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)
throws IOException, ServletException {

String token = tokenProvider.resolveToken((HttpServletRequest) request);

if (StringUtils.hasText(token) && tokenProvider.validateToken(token)) {
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}

filterChain.doFilter(request, response);
}
}
Loading

0 comments on commit 1fc40b4

Please sign in to comment.