Skip to content

Commit

Permalink
Merge pull request #12 from TeamPINGLE/feat/6
Browse files Browse the repository at this point in the history
[feat] 소셜 로그인 + JWT 구현
  • Loading branch information
tkdwns414 authored Jan 6, 2024
2 parents 011eeb4 + 054d797 commit 3ea2024
Show file tree
Hide file tree
Showing 36 changed files with 841 additions and 8 deletions.
16 changes: 15 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.7'
id 'org.springframework.boot' version '3.2.0'
id 'io.spring.dependency-management' version '1.1.4'
}

Expand All @@ -25,6 +25,9 @@ dependencies {
// Spring
implementation 'org.springframework.boot:spring-boot-starter-web'

// Validation
implementation 'org.springframework.boot:spring-boot-starter-validation'

// Database
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand All @@ -41,6 +44,17 @@ dependencies {

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

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

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

// Social Login
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign:4.1.0'
}

tasks.named('bootBuildImage') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,9 @@

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.security.servlet.UserDetailsServiceAutoConfiguration;

@SpringBootApplication
@SpringBootApplication(exclude = {UserDetailsServiceAutoConfiguration.class})
public class PingleserverApplication {

public static void main(String[] args) {
Expand Down
11 changes: 11 additions & 0 deletions src/main/java/org/pingle/pingleserver/annotation/UserId.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.pingle.pingleserver.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface UserId {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.pingle.pingleserver.config;

import org.pingle.pingleserver.PingleserverApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableFeignClients(basePackageClasses = PingleserverApplication.class)
public class FeignClientConfig {
}
23 changes: 23 additions & 0 deletions src/main/java/org/pingle/pingleserver/config/WebMVCConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package org.pingle.pingleserver.config;

import lombok.RequiredArgsConstructor;
import org.pingle.pingleserver.interceptor.pre.UserIdArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@EnableWebMvc
@RequiredArgsConstructor
public class WebMVCConfig implements WebMvcConfigurer {
private final UserIdArgumentResolver userIdArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
WebMvcConfigurer.super.addArgumentResolvers(resolvers);
resolvers.add(this.userIdArgumentResolver);
}
}
9 changes: 9 additions & 0 deletions src/main/java/org/pingle/pingleserver/constant/Constants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package org.pingle.pingleserver.constant;

public class Constants {
public static String USER_ID_CLAIM_NAME = "uid";
public static String USER_ROLE_CLAIM_NAME = "rol";
public static String BEARER_PREFIX = "Bearer ";
public static String AUTHORIZATION_HEADER = "Authorization";

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.pingle.pingleserver.controller;

import jakarta.validation.Valid;
import jakarta.validation.constraints.NotNull;
import lombok.RequiredArgsConstructor;
import org.pingle.pingleserver.dto.common.ApiResponse;
import org.pingle.pingleserver.dto.request.LoginRequest;
import org.pingle.pingleserver.dto.response.JwtTokenResponse;
import org.pingle.pingleserver.dto.type.SuccessMessage;
import org.pingle.pingleserver.service.AuthService;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/v1/auth")
@RequiredArgsConstructor
public class AuthController {

private final AuthService authService;

@PostMapping("/login")
public ApiResponse<JwtTokenResponse> login(
@NotNull @RequestHeader("Provider-Token") String providerToken,
@Valid @RequestBody LoginRequest request){
return ApiResponse.success(SuccessMessage.OK, authService.login(providerToken, request));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package org.pingle.pingleserver.controller;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.pingle.pingleserver.annotation.UserId;
import org.pingle.pingleserver.domain.enums.URole;
import org.pingle.pingleserver.dto.response.JwtTokenResponse;
import org.pingle.pingleserver.utils.JwtUtil;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/test")
public class TestController {

private final JwtUtil jwtUtil;
@GetMapping("/token")
public JwtTokenResponse testToken() {
return jwtUtil.generateTokens(1L, URole.ADMIN);
}

@GetMapping("/user-test")
public ResponseEntity<Long> testUser(@UserId Long userId) {
return ResponseEntity.ok(userId);
}

}
57 changes: 54 additions & 3 deletions src/main/java/org/pingle/pingleserver/domain/User.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,66 @@

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.pingle.pingleserver.domain.enums.Provider;
import org.pingle.pingleserver.domain.enums.URole;

import java.time.LocalDateTime;

@Entity
@Getter
@Table(name = "users")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseTimeEntity {
public class User {
private static final Long MEMBER_INFO_RETENTION_PERIOD = 180L;

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

@Column(nullable = false)
private String serialId;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private URole role;

private String name;

@Column(nullable = false)
@Enumerated(EnumType.STRING)
private Provider provider;

private String refreshToken;

private String email;

private boolean isDeleted = false;

private LocalDateTime deletedAt;

@Builder
public User(String serialId, String name, String email, Provider provider, URole role, String refreshToken) {
this.serialId = serialId;
this.name = name;
this.email = email;
this.provider = provider;
this.role = role;
this.refreshToken = refreshToken;
}

public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

public void softDelete() {
this.isDeleted = true;
this.deletedAt = LocalDateTime.now().plusDays(MEMBER_INFO_RETENTION_PERIOD);
}

public void recover() {
this.isDeleted = false;
this.deletedAt = null;
}
}
18 changes: 18 additions & 0 deletions src/main/java/org/pingle/pingleserver/domain/enums/Provider.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package org.pingle.pingleserver.domain.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;

@Getter
@RequiredArgsConstructor
public enum Provider {
KAKAO("KAKAO"),
APPLE("APPLE");

private final String name;

@Override
public String toString() {
return name;
}
}
14 changes: 14 additions & 0 deletions src/main/java/org/pingle/pingleserver/domain/enums/URole.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.pingle.pingleserver.domain.enums;

import lombok.Getter;
import lombok.RequiredArgsConstructor;


@Getter
@RequiredArgsConstructor
public enum URole {
USER("USER"),
ADMIN("ADMIN");

private final String name;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package org.pingle.pingleserver.dto.request;

import jakarta.validation.constraints.NotNull;
import org.pingle.pingleserver.domain.enums.Provider;

public record LoginRequest(@NotNull Provider provider, String name) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package org.pingle.pingleserver.dto.response;

import jakarta.validation.constraints.NotNull;
import lombok.Builder;

@Builder
public record JwtTokenResponse (@NotNull String accessToken, @NotNull String refreshToken){
public static JwtTokenResponse of(String accessToken, String refreshToken) {
return JwtTokenResponse.builder()
.accessToken(accessToken)
.refreshToken(refreshToken)
.build();
}
}
20 changes: 20 additions & 0 deletions src/main/java/org/pingle/pingleserver/dto/type/ErrorMessage.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,28 @@
@Getter
@AllArgsConstructor
public enum ErrorMessage {
// Apple Login Error
INVALID_APPLE_PUBLIC_KEY(HttpStatus.BAD_REQUEST, "유효하지 않은 Apple Public Key입니다."),
INVALID_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "유효하지 않은 Apple Identity Token입니다."),
EXPIRED_APPLE_IDENTITY_TOKEN(HttpStatus.BAD_REQUEST, "만료된 Apple Identity Token입니다."),
CREATE_PUBLIC_KEY_EXCEPTION(HttpStatus.BAD_REQUEST, "Apple Public verify에 실패했습니다."),
// JWT Error
INVALID_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "유효하지 않은 JWT 토큰입니다."),
EXPIRED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "만료된 JWT 토큰입니다."),
UNSUPPORTED_JWT_TOKEN(HttpStatus.UNAUTHORIZED, "지원하지 않는 JWT 토큰입니다."),
JWT_TOKEN_IS_EMPTY(HttpStatus.UNAUTHORIZED, "JWT 토큰이 비어있습니다."),
// Invalid Argument Error 400
INVALID_HEADER_ERROR(HttpStatus.BAD_REQUEST, "유효하지 않은 헤더입니다."),
INVALID_PROVIDER_ERROR(HttpStatus.BAD_REQUEST, "유효하지 않은 소셜 플랫폼입니다."),
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
MISSING_REQUIRED_HEADER(HttpStatus.BAD_REQUEST, "필수 헤더가 누락되었습니다."),
// Authorization Error 401
TOKEN_MALFORMED_ERROR(HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰입니다."),
UNAUTHORIZED_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 제공되지 않았거나 유효하지 않습니다."),
NO_SUCH_USER(HttpStatus.UNAUTHORIZED, "존재하지 않는 사용자입니다."),
// Not Found Error 404
USER_NOT_FOUND_ERROR(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
NOT_FOUND_END_POINT(HttpStatus.NOT_FOUND, "존재하지 않는 API입니다."),
// Method Not Allowed Error 405
METHOD_NOT_ALLOWED(HttpStatus.METHOD_NOT_ALLOWED, "지원하지 않는 HTTP 메소드입니다."),
// Internal Server Error 500
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingRequestHeaderException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.NoHandlerFoundException;
Expand All @@ -17,8 +19,22 @@ public class GlobalExceptionHandler {
@ExceptionHandler(value = {NoHandlerFoundException.class, HttpRequestMethodNotSupportedException.class})
public ResponseEntity<ApiResponse<?>> handleNoPageFoundException(Exception e) {
log.error("handleNoPageFoundException() in GlobalExceptionHandler throw NoHandlerFoundException : {}", e.getMessage());
return ResponseEntity.status(HttpStatus.METHOD_NOT_ALLOWED)
.body(ApiResponse.fail(ErrorMessage.METHOD_NOT_ALLOWED));
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(ApiResponse.fail(ErrorMessage.NOT_FOUND_END_POINT));
}

@ExceptionHandler(value = {MethodArgumentNotValidException.class})
public ResponseEntity<ApiResponse<?>> handlerMethodArgumentNotValidException(Exception e) {
log.error("handlerMethodArgumentNotValidException() in GlobalExceptionHandler throw MethodArgumentNotValidException : {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(ErrorMessage.BAD_REQUEST));
}

@ExceptionHandler(value = {MissingRequestHeaderException.class})
public ResponseEntity<ApiResponse<?>> handlerMissingRequestHeaderException(Exception e) {
log.error("handlerMissingRequestHeaderException() in GlobalExceptionHandler throw MissingRequestHeaderException : {}", e.getMessage());
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(ErrorMessage.MISSING_REQUIRED_HEADER));
}

@ExceptionHandler(BusinessException.class)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package org.pingle.pingleserver.interceptor.pre;

import lombok.extern.slf4j.Slf4j;
import org.pingle.pingleserver.annotation.UserId;
import org.pingle.pingleserver.dto.type.ErrorMessage;
import org.pingle.pingleserver.exception.BusinessException;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import java.security.Principal;

@Slf4j
@Component
public class UserIdArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.getParameterType().equals(Long.class)
&& parameter.hasParameterAnnotation(UserId.class);
}

@Override
public Object resolveArgument(MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory) {
final Principal principal = webRequest.getUserPrincipal();
if (principal == null) {
throw new BusinessException(ErrorMessage.INVALID_JWT_TOKEN);
}
return Long.valueOf(principal.getName());
}
}
Loading

0 comments on commit 3ea2024

Please sign in to comment.