diff --git a/build.gradle b/build.gradle index f3ce6b5..0000595 100644 --- a/build.gradle +++ b/build.gradle @@ -56,6 +56,14 @@ dependencies { //feignClient implementation "org.springframework.cloud:spring-cloud-starter-openfeign:3.1.1" compileOnly "org.springframework.cloud:spring-cloud-starter-openfeign:3.1.1" + + // 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' + + // redis + implementation 'org.springframework.boot:spring-boot-starter-data-redis' } tasks.named('test') { diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/controller/ArchivingController.java b/src/main/java/com/example/PLADIALMArchiving/archiving/controller/ArchivingController.java new file mode 100644 index 0000000..7727696 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/controller/ArchivingController.java @@ -0,0 +1,54 @@ +package com.example.PLADIALMArchiving.archiving.controller; + +import com.example.PLADIALMArchiving.archiving.dto.request.RegisterProjectReq; +import com.example.PLADIALMArchiving.archiving.dto.request.UploadMaterialReq; +import com.example.PLADIALMArchiving.archiving.service.ArchivingService; +import com.example.PLADIALMArchiving.global.resolver.Account; +import com.example.PLADIALMArchiving.global.response.ResponseCustom; +import com.example.PLADIALMArchiving.user.entity.User; +import io.swagger.annotations.Api; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.*; + +@Api(tags = "아카이빙 API") +@RestController +@RequiredArgsConstructor +@RequestMapping("/archives") +public class ArchivingController { + + private final ArchivingService archivingService; + + /** + * 프로젝트를 추가한다. + */ + @PostMapping("/projects/register") + public ResponseCustom registerProject(@RequestBody RegisterProjectReq registerProjectReq) { + archivingService.registerProject(registerProjectReq); + return ResponseCustom.OK(); + } + + /** + * 자료를 업로드한다. + */ + @PostMapping("/projects/{projectId}/upload") + public ResponseCustom uploadMaterial( + @RequestBody UploadMaterialReq uploadMaterialReq, + @PathVariable Long projectId, + @Account User user + ) + { + archivingService.uploadMaterial(uploadMaterialReq, projectId, user); + return ResponseCustom.OK(); + } + /** + * 자료목록을 조회 및 검색한다. + */ + + /** + * 자료를 삭제한다. + */ + + /** + * 자료를 다운로드한다. + */ +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/RegisterProjectReq.java b/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/RegisterProjectReq.java new file mode 100644 index 0000000..7819e21 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/RegisterProjectReq.java @@ -0,0 +1,8 @@ +package com.example.PLADIALMArchiving.archiving.dto.request; + +import lombok.Getter; + +@Getter +public class RegisterProjectReq { + private String name; +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/UploadMaterialReq.java b/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/UploadMaterialReq.java new file mode 100644 index 0000000..b4032db --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/dto/request/UploadMaterialReq.java @@ -0,0 +1,10 @@ +package com.example.PLADIALMArchiving.archiving.dto.request; + +import lombok.Getter; + +@Getter +public class UploadMaterialReq { + private String fileKey; + private String name; + private String extension; +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Material.java b/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Material.java new file mode 100644 index 0000000..ae0e4ca --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Material.java @@ -0,0 +1,60 @@ +package com.example.PLADIALMArchiving.archiving.entity; + +import com.example.PLADIALMArchiving.archiving.dto.request.UploadMaterialReq; +import com.example.PLADIALMArchiving.global.entity.BaseEntity; +import com.example.PLADIALMArchiving.user.entity.User; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Where; + +import javax.persistence.*; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@DynamicUpdate +@Where(clause = "is_enable = true") +public class Material extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long materialId; + + private String name; + + private String extension; + + private String fileKey; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, name = "project_id") + private Project project; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, name = "user_id") + private User user; + + @Builder + public Material(String name, String extension, String fileKey, Project project, User user) { + this.name = name; + this.extension = extension; + this.fileKey = fileKey; + this.project = project; + this.user = user; + } + + public static Material toEntity(UploadMaterialReq uploadMaterialReq, Project project, User user) { + return Material.builder() + .name(uploadMaterialReq.getName()) + .extension(uploadMaterialReq.getExtension()) + .fileKey(uploadMaterialReq.getFileKey()) + .project(project) + .user(user) + .build(); + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Project.java b/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Project.java new file mode 100644 index 0000000..eb9825d --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/entity/Project.java @@ -0,0 +1,44 @@ +package com.example.PLADIALMArchiving.archiving.entity; + +import com.example.PLADIALMArchiving.global.entity.BaseEntity; +import lombok.AccessLevel; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.hibernate.annotations.DynamicInsert; +import org.hibernate.annotations.DynamicUpdate; +import org.hibernate.annotations.Where; +import javax.persistence.*; +import javax.validation.constraints.NotNull; +import javax.validation.constraints.Size; +import java.util.ArrayList; +import java.util.List; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@DynamicInsert +@DynamicUpdate +@Where(clause = "is_enable = true") +public class Project extends BaseEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long projectId; + + @NotNull + @Size(max = 50) + private String name; + + @OneToMany(mappedBy = "project", fetch = FetchType.LAZY) + private List materialList = new ArrayList<>(); + + public static Project toEntity(String name) { + return Project.builder().name(name).build(); + } + + @Builder + public Project(String name) { + this.name = name; + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/repository/MaterialRepository.java b/src/main/java/com/example/PLADIALMArchiving/archiving/repository/MaterialRepository.java new file mode 100644 index 0000000..301dcc8 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/repository/MaterialRepository.java @@ -0,0 +1,9 @@ +package com.example.PLADIALMArchiving.archiving.repository; + +import com.example.PLADIALMArchiving.archiving.entity.Material; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface MaterialRepository extends JpaRepository { +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/repository/ProjectRepository.java b/src/main/java/com/example/PLADIALMArchiving/archiving/repository/ProjectRepository.java new file mode 100644 index 0000000..49dd80b --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/repository/ProjectRepository.java @@ -0,0 +1,13 @@ +package com.example.PLADIALMArchiving.archiving.repository; + +import com.example.PLADIALMArchiving.archiving.entity.Project; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +import java.util.Optional; + +@Repository +public interface ProjectRepository extends JpaRepository { + + Optional findByName(String name); +} diff --git a/src/main/java/com/example/PLADIALMArchiving/archiving/service/ArchivingService.java b/src/main/java/com/example/PLADIALMArchiving/archiving/service/ArchivingService.java new file mode 100644 index 0000000..c2b93dd --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/archiving/service/ArchivingService.java @@ -0,0 +1,41 @@ +package com.example.PLADIALMArchiving.archiving.service; + +import com.example.PLADIALMArchiving.archiving.dto.request.RegisterProjectReq; +import com.example.PLADIALMArchiving.archiving.dto.request.UploadMaterialReq; +import com.example.PLADIALMArchiving.archiving.entity.Material; +import com.example.PLADIALMArchiving.archiving.entity.Project; +import com.example.PLADIALMArchiving.archiving.repository.MaterialRepository; +import com.example.PLADIALMArchiving.archiving.repository.ProjectRepository; +import com.example.PLADIALMArchiving.global.exception.BaseException; +import com.example.PLADIALMArchiving.global.exception.BaseResponseCode; +import com.example.PLADIALMArchiving.user.entity.User; +import com.example.PLADIALMArchiving.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.InputStream; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class ArchivingService { + + private final UserRepository userRepository; + private final ProjectRepository projectRepository; + private final MaterialRepository materialRepository; + + + @Transactional + public void registerProject(RegisterProjectReq registerProjectReq) { + boolean present = projectRepository.findByName(registerProjectReq.getName()).isPresent(); + if(present) throw new BaseException(BaseResponseCode.ALREADY_REGISTERED_PROJECT); + projectRepository.save(Project.toEntity(registerProjectReq.getName())); + } + + @Transactional + public void uploadMaterial(UploadMaterialReq uploadMaterialReq, Long projectId, User user) { + Project project = projectRepository.findById(projectId).orElseThrow(() -> new BaseException(BaseResponseCode.PROJECT_NOT_FOUND)); + materialRepository.save(Material.toEntity(uploadMaterialReq, project, user)); + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/BaseEntity.java b/src/main/java/com/example/PLADIALMArchiving/global/BaseEntity.java deleted file mode 100644 index 7cb81c8..0000000 --- a/src/main/java/com/example/PLADIALMArchiving/global/BaseEntity.java +++ /dev/null @@ -1,42 +0,0 @@ -package com.example.PLADIALMArchiving.global; - -import lombok.Getter; -import lombok.Setter; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; - -import javax.persistence.Column; -import javax.persistence.MappedSuperclass; -import javax.persistence.PrePersist; -import javax.persistence.PreUpdate; -import java.io.Serializable; -import java.time.LocalDateTime; - -@Getter -@MappedSuperclass -public class BaseEntity implements Serializable { - - @CreatedDate - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; - - @LastModifiedDate - private LocalDateTime updateAt; - - @Setter - @Column(nullable = false, columnDefinition = "BOOLEAN DEFAULT true") - private Boolean isEnable = true; - - @PrePersist - public void prePersist() { - LocalDateTime now = LocalDateTime.now(); - createdAt = now; - updateAt = now; - } - - @PreUpdate - public void preUpdate() { - updateAt = LocalDateTime.now(); - } - -} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/Constants.java b/src/main/java/com/example/PLADIALMArchiving/global/Constants.java new file mode 100644 index 0000000..1ef83e3 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/Constants.java @@ -0,0 +1,19 @@ +package com.example.PLADIALMArchiving.global; + +public class Constants { + public static final String TIME_PATTERN = "HH:mm"; + public static final String DATE_PATTERN = "yyyy-MM-dd"; + public static final String DATE_TIME_PATTERN = "yyyy-MM-dd HH:mm"; + + public static class Booking{ + public static final String BOOKED_TIMES = "bookedTimes"; + } + + public static class JWT{ + public static final String AUTHORIZATION_HEADER = "Authorization"; + public static final String BEARER_PREFIX = "Bearer "; + public static final String CLAIM_NAME = "userIdx"; + public static final String LOGOUT = "logout"; + public static final String SIGNOUT = "signout"; + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/config/WebConfig.java b/src/main/java/com/example/PLADIALMArchiving/global/config/WebConfig.java index 942b391..a78bd06 100644 --- a/src/main/java/com/example/PLADIALMArchiving/global/config/WebConfig.java +++ b/src/main/java/com/example/PLADIALMArchiving/global/config/WebConfig.java @@ -1,13 +1,21 @@ package com.example.PLADIALMArchiving.global.config; +import com.example.PLADIALMArchiving.global.resolver.LoginResolver; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Configuration; import org.springframework.http.HttpMethod; +import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.servlet.config.annotation.CorsRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; +import java.util.List; + +@RequiredArgsConstructor @Configuration public class WebConfig implements WebMvcConfigurer { + private final LoginResolver loginResolver; + @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") // url 패턴 @@ -17,4 +25,9 @@ public void addCorsMappings(CorsRegistry registry) { .allowedMethods(HttpMethod.GET.name(), HttpMethod.POST.name(), HttpMethod.PATCH.name(), HttpMethod.DELETE.name(), HttpMethod.OPTIONS.name()) // 허용 method .allowedHeaders("Authorization", "Content-Type"); // 허용 header } + + @Override + public void addArgumentResolvers(List resolvers) { + resolvers.add(loginResolver); + } } diff --git a/src/main/java/com/example/PLADIALMArchiving/global/exception/BaseResponseCode.java b/src/main/java/com/example/PLADIALMArchiving/global/exception/BaseResponseCode.java index 0c939c0..aad5783 100644 --- a/src/main/java/com/example/PLADIALMArchiving/global/exception/BaseResponseCode.java +++ b/src/main/java/com/example/PLADIALMArchiving/global/exception/BaseResponseCode.java @@ -18,6 +18,15 @@ public enum BaseResponseCode { // User USER_NOT_FOUND("U0001", HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."), + // Token + NULL_TOKEN("T0001", HttpStatus.UNAUTHORIZED, "토큰 값을 입력해주세요."), + INVALID_TOKEN("T0002", HttpStatus.UNAUTHORIZED, "유효하지 않은 토큰 값입니다."), + UNSUPPORTED_TOKEN("T0003", HttpStatus.UNAUTHORIZED, "잘못된 형식의 토큰 값입니다."), + MALFORMED_TOKEN("T0004", HttpStatus.UNAUTHORIZED, "잘못된 구조의 토큰 값입니다."), + EXPIRED_TOKEN("T0005", HttpStatus.FORBIDDEN, "만료된 토큰 값입니다."), + NOT_ACCESS_HEADER("T0006", HttpStatus.INTERNAL_SERVER_ERROR, "헤더에 접근할 수 없습니다."), + BLACKLIST_TOKEN("T0007", HttpStatus.FORBIDDEN, "로그아웃 혹은 회원 탈퇴된 토큰입니다."), + // Booking DATE_OR_TIME_IS_NULL("B0001", HttpStatus.BAD_REQUEST, "날짜와 시간을 모두 입력해주세요."), MEMO_SIZE_OVER("B0002", HttpStatus.BAD_REQUEST, "요청사항은 30자 이하로 작성해주세요."), @@ -30,6 +39,10 @@ public enum BaseResponseCode { // Office OFFICE_NOT_FOUND("O0001", HttpStatus.NOT_FOUND, "존재하지 않는 회의실입니다."), + + // Archiving + ALREADY_REGISTERED_PROJECT("P0001", HttpStatus.BAD_REQUEST, "이미 등록된 프로젝트입니다."), + PROJECT_NOT_FOUND("P0002", HttpStatus.NOT_FOUND, "존재하지 않는 프로젝트입니다."), ; public final String code; diff --git a/src/main/java/com/example/PLADIALMArchiving/global/feign/feignClient/MainServerClient.java b/src/main/java/com/example/PLADIALMArchiving/global/feign/feignClient/MainServerClient.java index 053a6e7..1bac353 100644 --- a/src/main/java/com/example/PLADIALMArchiving/global/feign/feignClient/MainServerClient.java +++ b/src/main/java/com/example/PLADIALMArchiving/global/feign/feignClient/MainServerClient.java @@ -2,8 +2,6 @@ import org.springframework.cloud.openfeign.FeignClient; import org.springframework.stereotype.Component; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; @Component diff --git a/src/main/java/com/example/PLADIALMArchiving/global/resolver/Account.java b/src/main/java/com/example/PLADIALMArchiving/global/resolver/Account.java new file mode 100644 index 0000000..0101e8c --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/resolver/Account.java @@ -0,0 +1,11 @@ +package com.example.PLADIALMArchiving.global.resolver; + +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 Account { +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/resolver/LoginResolver.java b/src/main/java/com/example/PLADIALMArchiving/global/resolver/LoginResolver.java new file mode 100644 index 0000000..cf82efc --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/resolver/LoginResolver.java @@ -0,0 +1,44 @@ +package com.example.PLADIALMArchiving.global.resolver; + +import com.example.PLADIALMArchiving.global.Constants; +import com.example.PLADIALMArchiving.global.exception.BaseException; +import com.example.PLADIALMArchiving.global.exception.BaseResponseCode; +import com.example.PLADIALMArchiving.global.utils.JwtUtil; +import com.example.PLADIALMArchiving.user.entity.User; +import com.example.PLADIALMArchiving.user.repository.UserRepository; +import lombok.RequiredArgsConstructor; +import org.springframework.core.MethodParameter; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; +import org.springframework.util.StringUtils; +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; + +@RequiredArgsConstructor +@Component +public class LoginResolver implements HandlerMethodArgumentResolver { + private final JwtUtil jwtUtil; + private final UserRepository userRepository; + + @Override + public boolean supportsParameter(MethodParameter parameter) { + return parameter.hasParameterAnnotation(Account.class) && User.class.equals(parameter.getParameterType()); + } + + @Override + public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception { + + // header 에 값이 있는지 확인 + String token = webRequest.getHeader(Constants.JWT.AUTHORIZATION_HEADER); + if(!StringUtils.hasText(token)) throw new BaseException(BaseResponseCode.NULL_TOKEN); + // 추출 + token = token.substring(Constants.JWT.BEARER_PREFIX.length()); + // 유효성 검사 + jwtUtil.validateToken(token); + // 이미 로그아웃 & 회원 탈퇴가 된 토큰인지 확인 + if(!ObjectUtils.isEmpty(jwtUtil.getTokenInRedis(token))) throw new BaseException(BaseResponseCode.BLACKLIST_TOKEN); + return userRepository.findById(jwtUtil.getUserIdFromJWT(token)).orElseThrow(() -> new BaseException(BaseResponseCode.USER_NOT_FOUND)); + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/utils/AwsS3ImageUrlUtil.java b/src/main/java/com/example/PLADIALMArchiving/global/utils/AwsS3ImageUrlUtil.java new file mode 100644 index 0000000..59cd786 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/utils/AwsS3ImageUrlUtil.java @@ -0,0 +1,27 @@ +package com.example.PLADIALMArchiving.global.utils; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +@RequiredArgsConstructor +@Component +public class AwsS3ImageUrlUtil { + + public static String bucket; + public static String region; + + @Value("${aws.s3.region}") + public void setRegion(String value) { + region = value; + } + + @Value("${aws.s3.bucket}") + public void setBucket(String value) { + bucket = value; + } + + public static String toUrl(String imageKey) { + return "https://"+bucket+".s3."+region+".amazonaws.com/"+imageKey; + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/utils/JwtUtil.java b/src/main/java/com/example/PLADIALMArchiving/global/utils/JwtUtil.java new file mode 100644 index 0000000..7454a2b --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/utils/JwtUtil.java @@ -0,0 +1,113 @@ +package com.example.PLADIALMArchiving.global.utils; + +import com.example.PLADIALMArchiving.global.exception.BaseException; +import com.example.PLADIALMArchiving.global.exception.BaseResponseCode; +import com.example.PLADIALMArchiving.user.dto.TokenDto; +import com.example.PLADIALMArchiving.user.entity.Role; +import io.jsonwebtoken.*; +import io.jsonwebtoken.io.Decoders; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; +import org.springframework.util.ObjectUtils; + +import java.security.Key; +import java.time.Duration; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +import static com.example.PLADIALMArchiving.global.Constants.JWT.BEARER_PREFIX; +import static com.example.PLADIALMArchiving.global.Constants.JWT.CLAIM_NAME; + + +@Component +public class JwtUtil { + private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30; // 30분 + private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60 * 24 * 7; // 7일 + + private final Key key; + private final RedisUtil redisUtil; + + public JwtUtil(@Value("${jwt.secret}") String secretKey, RedisUtil redisUtil) { + this.redisUtil = redisUtil; + byte[] keyBytes = Decoders.BASE64.decode(secretKey); + this.key = Keys.hmacShaKeyFor(keyBytes); + } + + // 토큰 생성 + public TokenDto createToken(Long userIdx, Role role){ + long now = new Date().getTime(); + String accessToken = createAccessToken(userIdx, now).compact(); + String refreshToken = createToken(now, REFRESH_TOKEN_EXPIRE_TIME).compact(); + redisUtil.setValue(userIdx.toString(), refreshToken, Duration.ofMillis(REFRESH_TOKEN_EXPIRE_TIME)); + return TokenDto.toDto(BEARER_PREFIX + accessToken, BEARER_PREFIX + refreshToken, role); + } + + // create Token 생성 + private JwtBuilder createAccessToken(Long userIdx, long now) { + return createToken(now, JwtUtil.ACCESS_TOKEN_EXPIRE_TIME) + .claim(CLAIM_NAME, userIdx) + .setSubject(userIdx.toString()); + } + + // Token 기본 생성 및 RefreshToken 생성 + private JwtBuilder createToken(long now, long expireTime) { + return Jwts.builder() + .setExpiration(new Date(now + expireTime)) + .signWith(key, SignatureAlgorithm.HS256); + } + + // 토큰 유효성 검사 + public boolean validateToken(String token) { + try { + Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token); + return true; + } catch (io.jsonwebtoken.security.SecurityException e) { + throw new BaseException(BaseResponseCode.INVALID_TOKEN); + } catch (MalformedJwtException e){ + throw new BaseException(BaseResponseCode.MALFORMED_TOKEN); + } catch (ExpiredJwtException e) { + throw new BaseException(BaseResponseCode.EXPIRED_TOKEN); + } catch (UnsupportedJwtException e) { + throw new BaseException(BaseResponseCode.UNSUPPORTED_TOKEN); + } catch (IllegalArgumentException e) { + throw new BaseException(BaseResponseCode.NULL_TOKEN); + } + } + + // 토큰 내 정보 불러오기 + public Claims parseClaims(String accessToken) { + try { + return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody(); + } catch (ExpiredJwtException e) { + return e.getClaims(); + } + } + + // userId 불러오기 + public Long getUserIdFromJWT(String accessToken) { + String userId = String.valueOf(parseClaims(accessToken).get(CLAIM_NAME)); + return Long.parseLong(userId); + } + + // redis 내 token 가져오기 + public String getTokenInRedis(String token){ + return redisUtil.getValue(token); + } + + // blacklist + public void setBlackListToken(String token, String status) { + redisUtil.setValue(token, status, getExpiration(token), TimeUnit.MILLISECONDS); + } + + public void deleteRefreshToken(Long userId) { + if (!ObjectUtils.isEmpty(redisUtil.getValue(userId.toString()))) redisUtil.deleteValue(userId.toString()); + } + + // token의 남은 시간 계산 + private Long getExpiration(String token) { + Date expiration = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody().getExpiration(); + long now = new Date().getTime(); + return expiration.getTime() - now; + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/global/utils/RedisUtil.java b/src/main/java/com/example/PLADIALMArchiving/global/utils/RedisUtil.java new file mode 100644 index 0000000..4855c84 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/global/utils/RedisUtil.java @@ -0,0 +1,32 @@ +package com.example.PLADIALMArchiving.global.utils; + +import lombok.RequiredArgsConstructor; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.time.Duration; +import java.util.concurrent.TimeUnit; + +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class RedisUtil { + private final RedisTemplate redisTemplate; + public void setValue(String key, String value, Duration time) { + redisTemplate.opsForValue().set(key, value, time); + } + + public void setValue(String key, String value, Long exp, TimeUnit time){ + redisTemplate.opsForValue().set(key, value, exp, time); + } + + public String getValue(String key){ + return redisTemplate.opsForValue().get(key); + } + + @Transactional + public void deleteValue(String key){ + redisTemplate.delete(key); + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/user/dto/TokenDto.java b/src/main/java/com/example/PLADIALMArchiving/user/dto/TokenDto.java new file mode 100644 index 0000000..714ceb9 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/user/dto/TokenDto.java @@ -0,0 +1,25 @@ +package com.example.PLADIALMArchiving.user.dto; + +import com.example.PLADIALMArchiving.user.entity.Role; +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Data; + +@Data +@Builder +public class TokenDto { + @Schema(type = "String", description = "AccessToken", example = "bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTY3NTg2OTcsInVzZXJJZHgiOjEsInN1YiI6IjEifQ.DSBuBlStkjhT05vuzjWd-cg7naG5KikUxII734u3nUw") + private String accessToken; + @Schema(type = "String", description = "RefreshToken", example = "bearer eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTY3NTg2OTcsInVzZXJJZHgiOjEsInN1YiI6IjEifQ.DSBuBlStkjhT05vuzjWd-cg7naG5KikUxII734u3nUw") + private String refreshToken; + @Schema(type = "String", description = "사용자 역할", example = "BASIC / ADMIN") + private Role role; + + public static TokenDto toDto(String accessToken, String refreshToken, Role role){ + return TokenDto.builder() + .accessToken(accessToken) + .refreshToken(refreshToken) + .role(role) + .build(); + } +} diff --git a/src/main/java/com/example/PLADIALMArchiving/user/entity/Role.java b/src/main/java/com/example/PLADIALMArchiving/user/entity/Role.java new file mode 100644 index 0000000..79b5e94 --- /dev/null +++ b/src/main/java/com/example/PLADIALMArchiving/user/entity/Role.java @@ -0,0 +1,13 @@ +package com.example.PLADIALMArchiving.user.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum Role { + BASIC("일반"), + ADMIN("관리자"); + + private final String value; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c03f8f7..9550526 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -24,3 +24,15 @@ server: main: url: +jwt: + secret: ENC(7JHqgwmXfXySGksMK5SOUrWg7edAhDXA044D8pyPbQbsYgdgs+by47ti/tHUwlwqSCMmmD9FM3tZvazmb/0wtqTKqYXGsrNnlkD2yFO11Z6Gis49IO0SWhaAnmyUexlwy+Bscc2pZBK12M50vCeHkFQGRIe1SlPj) + +aws: + s3: + region: ENC(g5tyuGOJBDEmnezuqqberSvKxUO3sHtm) + bucket: ENC(xV+R4mKu3wiOHGoDKhWrsRyXTDOauoNy) + +redis: + host: ENC(wIh6kiQ7W/qgHC1zR1Vb7ZanSpzgLsT7) + port: ENC(52XQ9OWXFVilIpOWtV217g==) + password: ENC(12sPod7cSJbwOScRscRp3A==)