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: 소셜 로그인 구현 #15

Merged
merged 37 commits into from
Jan 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
dc3c7ba
chore: rest docs 의존성 제거
uwoobeat Jan 27, 2024
13377ab
refactor: 패키지 구조 변경
uwoobeat Jan 27, 2024
acf0b35
docs: .env 제외
uwoobeat Jan 27, 2024
670fb76
feat: 커스텀 유저 서비스 구현
uwoobeat Jan 27, 2024
b38a6b0
feat: 프로퍼티 세팅 및 JWT 설정 추가
uwoobeat Jan 27, 2024
c996126
feat: 쿠키 유틸리티 구현
uwoobeat Jan 27, 2024
f5c1b65
feat: 소셜 로그인 성공 시 토큰 발급해주는 핸들러 구현
uwoobeat Jan 27, 2024
3c7cd1d
feat: 시큐리티 관련 상수 클래스 추가
uwoobeat Jan 27, 2024
ced5c80
refactor: 토큰 프로퍼티 객체 구조 개선
uwoobeat Jan 27, 2024
f8f8141
feat: 토큰 생성 유틸리티 구현
uwoobeat Jan 27, 2024
e5a73b3
feat: 소셜 로그인 성공 핸들러에 토큰 발급 로직 추가
uwoobeat Jan 27, 2024
b93b5e5
docs: JWT 유틸리티에 주석 추가
uwoobeat Jan 27, 2024
96432d3
chore: 레디스 관련 세팅
uwoobeat Jan 27, 2024
95ac6fa
feat: 레디스 리프레시 토큰 구현
uwoobeat Jan 27, 2024
bad3fb5
refactor: 토큰 전송 시 DTO 사용하도록 리팩토링
uwoobeat Jan 27, 2024
adae5bd
style: spotless 적용
uwoobeat Jan 27, 2024
3081063
feat: JwtService 구현 및 적용
uwoobeat Jan 27, 2024
14df664
refactor: TokenType을 JwtConstant로 리팩토링
uwoobeat Jan 27, 2024
d97409a
feat: JwtService 토큰 파싱 및 조회 로직 구현
uwoobeat Jan 28, 2024
c89c0a7
refactor: 쿠키 maxAge 제거
uwoobeat Jan 28, 2024
e2bff29
feat: PrincipalDetails 구현
uwoobeat Jan 28, 2024
0fc4fd1
feat: JWT 파싱 로직 추가
uwoobeat Jan 28, 2024
3cfd751
feat: 엑세스 토큰 만료 체크 로직 추가
uwoobeat Jan 28, 2024
2010305
feat: 로그 제거 및 파싱 로직 수정
uwoobeat Jan 28, 2024
6058f0b
feat: 엑세스 토큰 재발급 로직 추가
uwoobeat Jan 28, 2024
248c7ec
feat: JWT 필터 구현
uwoobeat Jan 28, 2024
ea34e5e
chore: 시큐리티 설정 추가
uwoobeat Jan 28, 2024
3bdbe55
chore: 레디스 컨테이너 설정
uwoobeat Jan 28, 2024
7a0dfbf
test: 레디스 테스트 설정 추가
uwoobeat Jan 28, 2024
476dc47
chore: 테스트용 임시 값 추가
uwoobeat Jan 28, 2024
fe528e2
refactor: setter로 변경
uwoobeat Jan 28, 2024
8279899
fix: 생성자 사용하도록 수정
uwoobeat Jan 28, 2024
2f311d2
chore: redis 컨테이너 설정 추가
uwoobeat Jan 28, 2024
c40ce52
chore: 레디스 만료 이벤트 수신하지 않도록 변경
uwoobeat Jan 28, 2024
ae2d5fa
style: spotless 적용
uwoobeat Jan 28, 2024
adce498
chore: setup-java 버전 변경
uwoobeat Jan 28, 2024
e30fba9
refactor: 사용하지 않는 메서드 및 의존성 제거
uwoobeat Jan 28, 2024
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
4 changes: 4 additions & 0 deletions .github/workflows/develop_build_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ jobs:
- name: Run chmod to make gradlew executable
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

# Gradle 빌드
- name: Build with Gradle
id: gradle
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/pull_request_gradle_build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,18 @@ jobs:
uses: actions/[email protected]

- name: JDK 설치
uses: actions/setup-java@v3
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17

- name: gradlew 권한 부여
run: chmod +x ./gradlew

# Redis 컨테이너 실행
- name: Start containers
run: docker-compose -f ./docker-compose-test.yaml up -d

- name: Gradle Build
uses: gradle/gradle-build-action@v2
with:
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### ETC ###
.DS_Store

### Secrets ###
.env
15 changes: 12 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,26 @@ ext {

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'
runtimeOnly 'com.h2database:h2'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'

// Spring Security
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'

// 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'
}

tasks.named('test') {
Expand Down
9 changes: 9 additions & 0 deletions docker-compose-test.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
version: "3.8"

services:
redis:
image: "redis:alpine"
ports:
- "6379:6379"
environment:
- TZ=Asia/Seoul
8 changes: 8 additions & 0 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,11 @@ services:
- .env
environment:
- TZ=Asia/Seoul
redis:
image: "redis:alpine"
container_name: redis
ports:
- "6379:6379"
environment:
- TZ=Asia/Seoul
network_mode: host

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package com.gdschongik.gdsc.domain.auth.application;

import static com.gdschongik.gdsc.global.common.constant.SecurityConstant.*;

import com.gdschongik.gdsc.domain.auth.dao.RefreshTokenRepository;
import com.gdschongik.gdsc.domain.auth.domain.RefreshToken;
import com.gdschongik.gdsc.domain.auth.dto.AccessTokenDto;
import com.gdschongik.gdsc.domain.auth.dto.RefreshTokenDto;
import com.gdschongik.gdsc.domain.member.domain.MemberRole;
import com.gdschongik.gdsc.global.util.JwtUtil;
import io.jsonwebtoken.ExpiredJwtException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@Service
@RequiredArgsConstructor
public class JwtService {

private final JwtUtil jwtUtil;
private final RefreshTokenRepository refreshTokenRepository;

public AccessTokenDto createAccessToken(Long memberId, MemberRole memberRole) {
return jwtUtil.generateAccessToken(memberId, memberRole);
}

public RefreshTokenDto createRefreshToken(Long memberId) {
RefreshTokenDto refreshTokenDto = jwtUtil.generateRefreshToken(memberId);
saveRefreshTokenToRedis(refreshTokenDto);
return refreshTokenDto;
}

private void saveRefreshTokenToRedis(RefreshTokenDto refreshTokenDto) {
RefreshToken refreshToken = RefreshToken.builder()
.memberId(refreshTokenDto.memberId())
.token(refreshTokenDto.tokenValue())
.ttl(refreshTokenDto.ttl())
.build();
refreshTokenRepository.save(refreshToken);
}

public AccessTokenDto retrieveAccessToken(String accessTokenValue) {
try {
return jwtUtil.parseAccessToken(accessTokenValue);
} catch (Exception e) {
return null;
}
}

public RefreshTokenDto retrieveRefreshToken(String refreshTokenValue) {
RefreshTokenDto refreshTokenDto = parseRefreshToken(refreshTokenValue);

if (refreshTokenDto == null) {
return null;
}

// 파싱된 DTO와 일치하는 토큰이 Redis에 저장되어 있는지 확인
Optional<RefreshToken> refreshToken = getRefreshTokenFromRedis(refreshTokenDto.memberId());

// Redis에 토큰이 존재하고, 쿠키의 토큰과 값이 일치하면 DTO 반환
if (refreshToken.isPresent()
&& refreshTokenDto.tokenValue().equals(refreshToken.get().getToken())) {
return refreshTokenDto;
}

// Redis에 토큰이 존재하지 않거나, 쿠키의 토큰과 값이 일치하지 않으면 null 반환
return null;
}

private Optional<RefreshToken> getRefreshTokenFromRedis(Long memberId) {
// TODO: CustomException으로 바꾸기
return refreshTokenRepository.findByMemberId(memberId);
}

private RefreshTokenDto parseRefreshToken(String refreshTokenValue) {
try {
return jwtUtil.parseRefreshToken(refreshTokenValue);
} catch (Exception e) {
return null;
}
}

public AccessTokenDto reissueAccessTokenIfExpired(String accessTokenValue) {
// AT가 만료된 경우 AT를 재발급, 만료되지 않은 경우 null 반환
try {
jwtUtil.parseAccessToken(accessTokenValue);
return null;
} catch (ExpiredJwtException e) {
Long memberId = Long.parseLong(e.getClaims().getSubject());
MemberRole memberRole = MemberRole.valueOf(e.getClaims().get(TOKEN_ROLE_NAME, String.class));
return createAccessToken(memberId, memberRole);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gdschongik.gdsc.domain.auth.dao;

import com.gdschongik.gdsc.domain.auth.domain.RefreshToken;
import java.util.Optional;
import org.springframework.data.repository.CrudRepository;

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, Long> {
Optional<RefreshToken> findByMemberId(Long aLong);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.gdschongik.gdsc.domain.auth.domain;

import lombok.Builder;
import lombok.Getter;
import org.springframework.data.annotation.Id;
import org.springframework.data.redis.core.RedisHash;
import org.springframework.data.redis.core.TimeToLive;

@Getter
@RedisHash(value = "refreshToken")
public class RefreshToken {

@Id
private Long memberId;

private String token;

@TimeToLive
private long ttl;

@Builder
public RefreshToken(Long memberId, String token, long ttl) {
this.memberId = memberId;
this.token = token;
this.ttl = ttl;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.gdschongik.gdsc.domain.auth.dto;

import com.gdschongik.gdsc.domain.member.domain.MemberRole;

public record AccessTokenDto(Long memberId, MemberRole memberRole, String tokenValue) {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package com.gdschongik.gdsc.domain.auth.dto;

public record RefreshTokenDto(Long memberId, String tokenValue, Long ttl) {}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.gdschongik.gdsc.common.model;
package com.gdschongik.gdsc.domain.common.model;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gdschongik.gdsc.domain.member.dao;

import com.gdschongik.gdsc.domain.member.domain.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByOauthId(String oauthId);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package com.gdschongik.gdsc.domain.member.domain;

import com.gdschongik.gdsc.common.model.BaseTimeEntity;
import com.gdschongik.gdsc.domain.common.model.BaseTimeEntity;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.gdschongik.gdsc.global.common.constant;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public enum JwtConstant {
ACCESS_TOKEN(Constants.ACCESS_TOKEN_COOKIE_NAME),
REFRESH_TOKEN(Constants.REFRESH_TOKEN_COOKIE_NAME);

private final String cookieName;

private static class Constants {
public static final String ACCESS_TOKEN_COOKIE_NAME = "accessToken";
public static final String REFRESH_TOKEN_COOKIE_NAME = "refreshToken";
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.gdschongik.gdsc.global.common.constant;

public class SecurityConstant {

public static final String REGISTRATION_REQUIRED_HEADER = "Registration-Required";
public static final String TOKEN_ROLE_NAME = "role";

private SecurityConstant() {}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.gdschongik.gdsc.common.config;
package com.gdschongik.gdsc.global.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gdschongik.gdsc.global.config;

import com.gdschongik.gdsc.global.property.JwtProperty;
import com.gdschongik.gdsc.global.property.RedisProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration;

@EnableConfigurationProperties({JwtProperty.class, RedisProperty.class})
@Configuration
public class PropertyConfig {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.gdschongik.gdsc.global.config;

import com.gdschongik.gdsc.global.property.RedisProperty;
import java.time.Duration;
import lombok.RequiredArgsConstructor;
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.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceClientConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;

@RequiredArgsConstructor
@Configuration
public class RedisConfig {

private final RedisProperty redisProperty;

@Bean
public RedisConnectionFactory redisConnectionFactory() {
RedisStandaloneConfiguration redisConfig =
new RedisStandaloneConfiguration(redisProperty.getHost(), redisProperty.getPort());

if (!redisProperty.getPassword().isBlank()) {
redisConfig.setPassword(redisProperty.getPassword());
}

LettuceClientConfiguration clientConfig = LettuceClientConfiguration.builder()
.commandTimeout(Duration.ofSeconds(1))
.shutdownTimeout(Duration.ZERO)
.build();

return new LettuceConnectionFactory(redisConfig, clientConfig);
}
}
Loading
Loading