Skip to content

Commit

Permalink
Merge pull request #12 from dnd-side-project/feature/#11-sign
Browse files Browse the repository at this point in the history
[FEAT] 로그인, 로그아웃, 토큰 재발급
  • Loading branch information
heejjinkim authored Sep 10, 2024
2 parents 27fea58 + 8811e4e commit d398159
Show file tree
Hide file tree
Showing 32 changed files with 1,022 additions and 5 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -88,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

0 comments on commit d398159

Please sign in to comment.