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] 로그인, 로그아웃, 토큰 재발급 #12

Merged
merged 26 commits into from
Sep 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6c4195a
chore: 로그인 관련 의존성 추가
heejjinkim Sep 3, 2024
4b2f2c7
fix: BaseEntity nullable false 제거
heejjinkim Sep 3, 2024
51f1553
feat: Role, Provider, Status enum 추가
heejjinkim Sep 3, 2024
ed65275
feat: Member 도메인 추가
heejjinkim Sep 3, 2024
911f3b0
feat: 로그인 관련 exception 추가
heejjinkim Sep 3, 2024
16aa78c
feat: 로그인 관련 config 추가
heejjinkim Sep 3, 2024
e902255
refactor: security -> auth 경로 이동
heejjinkim Sep 3, 2024
856e7e7
feat: kakao feign client 관련 추가
heejjinkim Sep 3, 2024
0b92373
feat: jwt 인증 구현
heejjinkim Sep 3, 2024
1883a81
feat: 로그인 request, response DTO 추가
heejjinkim Sep 3, 2024
26faf60
feat: 로그인 controller, service 구현
heejjinkim Sep 3, 2024
643d8be
feat: kakaoService 구현
heejjinkim Sep 3, 2024
c81399c
chore: yaml 수정
heejjinkim Sep 4, 2024
68f6630
chore: deploy.yaml 환경변수 추가
heejjinkim Sep 4, 2024
af0b209
feature: add generate member tag
minje0204 Sep 4, 2024
e86f24c
solve conflict
minje0204 Sep 4, 2024
c4eaa6b
fix: application.yaml 수정
heejjinkim Sep 4, 2024
f325537
Merge branch 'feature/#11-sign' of https://github.com/dnd-side-projec…
heejjinkim Sep 4, 2024
8ccc6ac
Update deploy.yaml
minje0204 Sep 4, 2024
285103e
feat: jwt exception filter 추가
heejjinkim Sep 7, 2024
e2c84ca
refactor: oidcCacheManager 삭제
heejjinkim Sep 7, 2024
6693930
feat: RedisUtil 추가
heejjinkim Sep 7, 2024
5825446
feat: token 재발급, 로그아웃 기능 구현
heejjinkim Sep 7, 2024
647493e
refactor: 오타 수정
heejjinkim Sep 7, 2024
ef7c73a
chore: redis container 추가
heejjinkim Sep 7, 2024
8811e4e
fix: subject 값 providerId로 수정
heejjinkim Sep 7, 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
30 changes: 28 additions & 2 deletions .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ on:
workflow_dispatch:

env:
S3_BUCKET_NAME: wepro
S3_BUCKET_NAME: wepro1
RESOURCE_PATH: ./src/main/resources/application.yaml
CODE_DEPLOY_APPLICATION_NAME: wepro-code-deploy
CODE_DEPLOY_DEPLOYMENT_GROUP_NAME: wepro-server
Expand All @@ -16,6 +16,20 @@ jobs:
build:
runs-on: ubuntu-latest

services:
mysql:
image: mysql:8.0
ports:
- 3306:3306
env:
MYSQL_ROOT_PASSWORD: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
MYSQL_DATABASE: wepro
options: >-
--health-cmd="mysqladmin ping --silent"
--health-interval=10s
--health-timeout=5s
--health-retries=3

steps:
- name: Checkout
uses: actions/checkout@v2
Expand All @@ -33,11 +47,23 @@ jobs:
spring.datasource.url: ${{ secrets.SPRING_DATASOURCE_URL }}
spring.datasource.username: ${{ secrets.SPRING_DATASOURCE_USERNAME }}
spring.datasource.password: ${{ secrets.SPRING_DATASOURCE_PASSWORD }}
jwt.secret: ${{ secrets.JWT_SECRET }}
login.uri: ${{ secrets.LOGIN_URI }}
kakao.client-id: ${{ secrets.KAKAO_CLIENT_ID }}
kakao.client-secret: ${{ secrets.KAKAO_CLIENT_SECRET }}
kakao.redirect-uri: ${{ secrets.KAKAO_REDIRECT_URI }}

- name: Grant execute permission for gradlew
run: chmod +x ./gradlew
shell: bash

- name: Wait for MySQL
run: |
while ! mysqladmin ping -h"127.0.0.1" --silent; do
echo "Waiting for MySQL to be ready..."
sleep 1
done

- name: Build with Gradle
run: ./gradlew build
shell: bash
Expand All @@ -62,4 +88,4 @@ jobs:
--deployment-config-name CodeDeployDefault.AllAtOnce \
--application-name ${{ env.CODE_DEPLOY_APPLICATION_NAME }} \
--deployment-group-name ${{ env.CODE_DEPLOY_DEPLOYMENT_GROUP_NAME }} \
--s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
--s3-location bucket=$S3_BUCKET_NAME,bundleType=zip,key=$GITHUB_SHA.zip
16 changes: 16 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ dependencies {
// test
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// security
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

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

// feign
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
implementation platform("org.springframework.cloud:spring-cloud-dependencies:2023.0.2")

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

tasks.named('test') {
Expand Down
9 changes: 9 additions & 0 deletions db/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,12 @@ services:
- ${DEFAULT_PATH}/mysql/data:/var/lib/mysql
- ${DEFAULT_PATH}/mysql/initdb.d:/docker-entrypoint-initdb.d
restart: always
redis:
container_name: "redis"
image: redis:latest
command: redis-server --port 6379
ports:
- "6379:6379"
volumes:
- ${DEFAULT_PATH}/redis/data:/data
restart: always
29 changes: 29 additions & 0 deletions src/main/java/com/_119/wepro/auth/client/KakaoOauthClient.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package com._119.wepro.auth.client;

import com._119.wepro.auth.dto.response.KakaoTokenResponse;
import com._119.wepro.auth.dto.response.OIDCPublicKeyResponse;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

@FeignClient(
name = "KakaoOauthClient",
url = "https://kauth.kakao.com"
)
public interface KakaoOauthClient {

// 만약 클라이언트로부터 code 받을 경우,
@PostMapping(
"/oauth/token?grant_type=authorization_code&client_id={CLIENT_ID}&redirect_uri={REDIRECT_URI}&code={CODE}&client_secret={CLIENT_SECRET}")
KakaoTokenResponse kakaoAuth(
@PathVariable("CLIENT_ID") String clientId,
@PathVariable("REDIRECT_URI") String redirectUri,
@PathVariable("CODE") String code,
@PathVariable("CLIENT_SECRET") String client_secret);

// oidc 공개 키 받아 오기 - 안 쓸 예정
// @Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager") // 공개키 자주 요청할 거 같으면, 캐싱하기
@GetMapping("/.well-known/jwks.json")
OIDCPublicKeyResponse getOIDCPublicKey();
}
49 changes: 49 additions & 0 deletions src/main/java/com/_119/wepro/auth/dto/request/AuthRequest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package com._119.wepro.auth.dto.request;

import com._119.wepro.global.enums.Provider;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;

public class AuthRequest {

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SignInRequest {

@NotNull
@Enumerated(EnumType.STRING)
private Provider provider;

@NotNull
private String idToken;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class RefreshRequest {
@NotNull
private String accessToken;

@NotNull
private String refreshToken;
}

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class SignUpRequest {

@NotNull
private String position;
}
}
16 changes: 16 additions & 0 deletions src/main/java/com/_119/wepro/auth/dto/response/AuthResponse.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com._119.wepro.auth.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

public class AuthResponse {

@Getter
@AllArgsConstructor
public static class SignInResponse {

private boolean newMember;
private TokenInfo tokenInfo;
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com._119.wepro.auth.dto.response;

import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
import com.fasterxml.jackson.databind.annotation.JsonNaming;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@JsonNaming(SnakeCaseStrategy.class)
public class KakaoTokenResponse {
private String accessToken;
private String refreshToken;
private String idToken;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com._119.wepro.auth.dto.response;

import java.util.List;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
public class OIDCPublicKeyResponse {

private List<OIDCPublicKey> keys;

@Getter
@NoArgsConstructor
public static class OIDCPublicKey {

private String kid;
private String alg;
private String use;
private String n;
private String e;
}
}
12 changes: 12 additions & 0 deletions src/main/java/com/_119/wepro/auth/dto/response/TokenInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com._119.wepro.auth.dto.response;

import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class TokenInfo {
private String type;
private String accessToken;
private String refreshToken;
}
50 changes: 50 additions & 0 deletions src/main/java/com/_119/wepro/auth/jwt/JwtTokenExceptionFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com._119.wepro.auth.jwt;

import com._119.wepro.global.dto.ErrorResponseDto;
import com._119.wepro.global.exception.RestApiException;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
public class JwtTokenExceptionFilter extends OncePerRequestFilter {

private final ObjectMapper objectMapper = new ObjectMapper();

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (RestApiException e) {
logClientIpAndRequestUri(request);
sendErrorResponse(response, e);
}
}

private void logClientIpAndRequestUri(HttpServletRequest request) {
String clientIp = request.getHeader("X-Forwarded-For");
if (clientIp == null) {
clientIp = request.getRemoteAddr();
}
log.error("Invalid token for requestURI: {}, Access from IP: {}", request.getRequestURI(),
clientIp);
}

private void sendErrorResponse(HttpServletResponse response, RestApiException e)
throws IOException {
ErrorResponseDto errorResponseDto = ErrorResponseDto.builder()
.code(e.getErrorCode().name())
.message(e.getErrorCode().getMessage())
.build();

response.setStatus(e.getErrorCode().getHttpStatus().value());
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(errorResponseDto));
}
}
55 changes: 55 additions & 0 deletions src/main/java/com/_119/wepro/auth/jwt/JwtTokenFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com._119.wepro.auth.jwt;

import static com._119.wepro.global.exception.errorcode.CommonErrorCode.NOT_EXIST_BEARER_SUFFIX;

import com._119.wepro.global.exception.RestApiException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.filter.OncePerRequestFilter;

@Slf4j
@RequiredArgsConstructor
public class JwtTokenFilter extends OncePerRequestFilter {

private final JwtTokenProvider jwtTokenProvider;
private final String accessHeader = "Authorization";
private final String grantType = "Bearer";

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {

Optional<String> token = getTokensFromHeader(request, accessHeader);

token.ifPresent(t -> {
String accessToken = getAccessToken(t);

Authentication authentication = jwtTokenProvider.getAuthentication(accessToken);

SecurityContextHolder.getContext().setAuthentication(authentication);
});
filterChain.doFilter(request, response);
}

private Optional<String> getTokensFromHeader(HttpServletRequest request, String header) {
return Optional.ofNullable(request.getHeader(header));
}

private String getAccessToken(String token) {
String suffix = grantType + " ";

if (!token.startsWith(suffix)) {
throw new RestApiException(NOT_EXIST_BEARER_SUFFIX);
}

return token.replace(suffix, "");
}
}
Loading
Loading