From 630d615c8c6a3c4f4fef49a22b89cf0964f2beb3 Mon Sep 17 00:00:00 2001 From: mindol1004 Date: Fri, 4 Oct 2024 18:12:59 +0900 Subject: [PATCH] commit --- .../domain/email/batch/EmailSendBatch.java | 8 +- .../domain/post/batch/PostCreateBatch.java | 7 +- .../post/batch/PostCreateBatchChunk.java | 9 ++- .../spring/domain/user/entity/AgentUser.java | 17 +++- .../domain/user/entity/AgentUserToken.java | 12 +-- .../spring/domain/user/entity/AppUser.java | 2 +- .../domain/user/entity/AppUserRole.java | 2 +- .../domain/user/entity/AppUserRoleMap.java | 2 +- .../domain/user/error/UserUnAuthorized.java | 11 +++ .../repository/AgentUserTokenRepository.java | 5 -- .../user/service/UserPrincipalService.java | 13 ++- .../user/service/UserRefreshTokenService.java | 22 ++++-- .../spring/infra/batch/AbstractBatchTask.java | 68 +++++----------- .../com/spring/infra/batch/BatchJobInfo.java | 2 +- .../batch/BatchJobInfoBeanPostProcessor.java | 79 +++++++++++++++++++ .../com/spring/infra/quartz/QuartzConfig.java | 2 +- .../com/spring/infra/quartz/QuartzJob.java | 60 -------------- .../infra/quartz/QuartzJobRegistrar.java | 66 +++++++++------- .../security/domain/JwtUserPrincipal.java | 63 +++++++++++++++ .../infra/security/domain/UserPrincipal.java | 11 +++ .../filter/JwtAuthenticationFilter.java | 15 ++-- .../security/handler/SignOutHandler.java | 3 + .../handler/SigninSuccessHandler.java | 5 +- .../infra/security/jwt/JwtTokenGenerator.java | 16 ++-- .../infra/security/jwt/JwtTokenRule.java | 12 ++- .../infra/security/jwt/JwtTokenService.java | 49 ++++++++---- .../provider/UserAuthenticationProvider.java | 4 +- .../security/service/RefreshTokenService.java | 8 +- .../web/advice/GlobalControllerAdvice.java | 32 ++++++++ .../spring/web/controller/SignController.java | 9 --- .../src/main/resources/application.yml | 22 ++++-- .../static/js/common/axios-instance.js | 24 +----- .../static/js/pages/schedule/schedule.js | 3 +- .../resources/templates/fragments/config.html | 10 ++- .../resources/templates/fragments/header.html | 4 +- 35 files changed, 421 insertions(+), 256 deletions(-) create mode 100644 batch-quartz/src/main/java/com/spring/domain/user/error/UserUnAuthorized.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java delete mode 100644 batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/domain/JwtUserPrincipal.java create mode 100644 batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java create mode 100644 batch-quartz/src/main/java/com/spring/web/advice/GlobalControllerAdvice.java diff --git a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java index 2797941..964f3ad 100644 --- a/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/email/batch/EmailSendBatch.java @@ -18,10 +18,10 @@ import com.spring.domain.post.repository.PostRepository; @Slf4j @Component @BatchJobInfo( - group = "EMAIL", - jobName = "emailSendJob", - cronExpression = "*/10 * * * * ?", - description = "이메일배치작업" + group = "${batch-info.email-send-batch.group}", + jobName = "${batch-info.email-send-batch.job-name}", + cronExpression = "${batch-info.email-send-batch.cron-expression}", + description = "${batch-info.email-send-batch.description}" ) @RequiredArgsConstructor public class EmailSendBatch extends AbstractBatchTask { diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java index 269bc6e..0472fa5 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatch.java @@ -19,9 +19,10 @@ import lombok.extern.slf4j.Slf4j; @Slf4j @Component @BatchJobInfo( - group = "POST", - jobName = "postCreateJob", - cronExpression = "0/20 * * * * ?" + group = "${batch-info.post-batch.group}", + jobName = "${batch-info.post-batch.job-name}", + cronExpression = "${batch-info.post-batch.cron-expression}", + description = "${batch-info.post-batch.description}" ) @RequiredArgsConstructor public class PostCreateBatch extends AbstractBatchTask { diff --git a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java index 4fddfc1..b610c68 100644 --- a/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java +++ b/batch-quartz/src/main/java/com/spring/domain/post/batch/PostCreateBatchChunk.java @@ -30,8 +30,8 @@ import com.spring.domain.post.entity.Post; import com.spring.domain.post.entity.PostBackUp; import com.spring.domain.post.repository.PostBackUpRepository; import com.spring.domain.post.repository.PostRepository; +import com.spring.infra.batch.BatchJobInfo; import com.spring.infra.db.orm.jpa.SecondaryJpaConfig; -import com.spring.infra.quartz.QuartzJob; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -55,7 +55,12 @@ public class PostCreateBatchChunk { private List list = new ArrayList<>(); @Bean - @QuartzJob(group = "POST", jobName = "testPostJob", cronExpression = "0/50 * * * * ?", description = "테스트배치작업") + @BatchJobInfo( + group = "${batch-info.post-create-batch.group}", + jobName = "${batch-info.post-create-batch.job-name}", + cronExpression = "${batch-info.post-create-batch.cron-expression}", + description = "${batch-info.post-create-batch.description}" + ) Job testPostJob() { return new JobBuilder("testPostJob") .repository(jobRepository) diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java index 8c31cc5..4645f76 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUser.java @@ -16,7 +16,8 @@ import javax.persistence.Table; import org.hibernate.annotations.GenericGenerator; import org.springframework.security.core.GrantedAuthority; import org.springframework.security.core.authority.SimpleGrantedAuthority; -import org.springframework.security.core.userdetails.UserDetails; + +import com.spring.infra.security.domain.UserPrincipal; import lombok.AccessLevel; import lombok.Builder; @@ -26,8 +27,8 @@ import lombok.NoArgsConstructor; @Entity @Table(name = "AGENT_USER") @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) -public class AgentUser implements UserDetails { +@NoArgsConstructor(access = AccessLevel.PROTECTED) +public class AgentUser implements UserPrincipal { @Id @GeneratedValue(generator = "uuid2") @@ -95,4 +96,14 @@ public class AgentUser implements UserDetails { return this.userId; } + @Override + public String getKey() { + return this.id.toString(); + } + + @Override + public String getMemberName() { + return this.userName; + } + } diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUserToken.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUserToken.java index 244deba..0c43d62 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUserToken.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AgentUserToken.java @@ -20,7 +20,7 @@ import lombok.NoArgsConstructor; @Entity @Table(name = "AGENT_USER_TOKEN") @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AgentUserToken { @Id @@ -35,14 +35,10 @@ public class AgentUserToken { @Column(name = "REFRESH_TOKEN", nullable = false) private String refreshToken; - @Column(name = "REISSUE_COUNT", nullable = false) - private int reissueCount; - @Builder - public AgentUserToken(AgentUser agentUser, String refreshToken, int reissueCount) { + public AgentUserToken(AgentUser agentUser, String refreshToken) { this.agentUser = agentUser; this.refreshToken = refreshToken; - this.reissueCount = reissueCount; } public void updateRefreshToken(String refreshToken) { @@ -53,8 +49,4 @@ public class AgentUserToken { return this.refreshToken.equals(refreshToken); } - public void increaseReissueCount() { - reissueCount++; - } - } diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java index 7a67370..2e103c1 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUser.java @@ -20,7 +20,7 @@ import lombok.NoArgsConstructor; @Entity @Table(name = "APP_USER") @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AppUser { @Id diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java index 8ea2019..c9c8775 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRole.java @@ -19,7 +19,7 @@ import lombok.NoArgsConstructor; @Entity @Table(name = "APP_USER_ROLE") @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AppUserRole { @Id diff --git a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java index 399302c..f49410c 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/entity/AppUserRoleMap.java @@ -19,7 +19,7 @@ import lombok.NoArgsConstructor; @Entity @Table(name = "APP_USER_ROLE_MAP") @Getter -@NoArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) public class AppUserRoleMap { @EmbeddedId diff --git a/batch-quartz/src/main/java/com/spring/domain/user/error/UserUnAuthorized.java b/batch-quartz/src/main/java/com/spring/domain/user/error/UserUnAuthorized.java new file mode 100644 index 0000000..702c922 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/domain/user/error/UserUnAuthorized.java @@ -0,0 +1,11 @@ +package com.spring.domain.user.error; + +import com.spring.common.error.BizBaseException; + +public class UserUnAuthorized extends BizBaseException { + + public UserUnAuthorized() { + super(UserRule.USER_UNAUTHORIZED); + } + +} diff --git a/batch-quartz/src/main/java/com/spring/domain/user/repository/AgentUserTokenRepository.java b/batch-quartz/src/main/java/com/spring/domain/user/repository/AgentUserTokenRepository.java index af1dbbf..b19b08e 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/repository/AgentUserTokenRepository.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/repository/AgentUserTokenRepository.java @@ -1,6 +1,5 @@ package com.spring.domain.user.repository; -import java.util.Optional; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; @@ -10,8 +9,4 @@ import com.spring.domain.user.entity.AgentUserToken; public interface AgentUserTokenRepository extends JpaRepository { - Optional findByIdAndReissueCountLessThan(UUID id, long count); - - Optional findByRefreshToken(String refreshToken); - } diff --git a/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java b/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java index 570bd3a..301b691 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/service/UserPrincipalService.java @@ -1,13 +1,16 @@ package com.spring.domain.user.service; -import org.springframework.security.core.userdetails.UserDetails; +import java.util.UUID; + import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.spring.domain.user.error.UserNotFoundException; import com.spring.domain.user.error.UserRule; import com.spring.domain.user.repository.AgentUserRepository; +import com.spring.infra.security.domain.UserPrincipal; import lombok.RequiredArgsConstructor; @@ -19,9 +22,15 @@ public class UserPrincipalService implements UserDetailsService { @Transactional(readOnly = true) @Override - public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { + public UserPrincipal loadUserByUsername(String username) throws UsernameNotFoundException { return agentUserRepository.findByUserId(username) .orElseThrow(() -> new UsernameNotFoundException(UserRule.USER_UNAUTHORIZED.getMessage())); } + + @Transactional(readOnly = true) + public UserPrincipal getUser(String key) { + return agentUserRepository.findById(UUID.fromString(key)) + .orElseThrow(UserNotFoundException::new); + } } diff --git a/batch-quartz/src/main/java/com/spring/domain/user/service/UserRefreshTokenService.java b/batch-quartz/src/main/java/com/spring/domain/user/service/UserRefreshTokenService.java index 527f161..26dc953 100644 --- a/batch-quartz/src/main/java/com/spring/domain/user/service/UserRefreshTokenService.java +++ b/batch-quartz/src/main/java/com/spring/domain/user/service/UserRefreshTokenService.java @@ -1,12 +1,14 @@ package com.spring.domain.user.service; -import org.springframework.security.core.userdetails.UserDetails; +import java.util.UUID; + import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.spring.domain.user.entity.AgentUser; import com.spring.domain.user.entity.AgentUserToken; import com.spring.domain.user.error.UserNotFoundException; +import com.spring.domain.user.error.UserUnAuthorized; import com.spring.domain.user.repository.AgentUserRepository; import com.spring.domain.user.repository.AgentUserTokenRepository; import com.spring.infra.security.service.RefreshTokenService; @@ -22,8 +24,8 @@ public class UserRefreshTokenService implements RefreshTokenService { @Transactional @Override - public void saveRefreshToken(UserDetails user, String refreshToken) { - AgentUser agentUser = agentUserRepository.findByUserId(user.getUsername()).orElseThrow(UserNotFoundException::new); + public void saveRefreshToken(String userId, String refreshToken) { + AgentUser agentUser = agentUserRepository.findByUserId(userId).orElseThrow(UserNotFoundException::new); agentUserTokenRepository.findById(agentUser.getId()) .ifPresentOrElse( it -> it.updateRefreshToken(refreshToken), @@ -36,9 +38,19 @@ public class UserRefreshTokenService implements RefreshTokenService { ); } + @Transactional(readOnly = true) @Override - public String findByRefreshToken(String refreshToken) { - throw new UnsupportedOperationException("Unimplemented method 'findByRefreshToken'"); + public String getRefreshToken(String key) { + return agentUserTokenRepository.findById(UUID.fromString(key)).stream() + .map(AgentUserToken::getRefreshToken) + .findFirst() + .orElseThrow(UserUnAuthorized::new); + } + + @Transactional + @Override + public void deleteRefreshToken(String key) { + agentUserTokenRepository.deleteById(UUID.fromString(key)); } } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java index fcf49c5..bcf59b3 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/AbstractBatchTask.java @@ -11,6 +11,7 @@ import org.springframework.batch.core.repository.JobRepository; import org.springframework.batch.core.step.builder.StepBuilder; import org.springframework.batch.core.step.tasklet.Tasklet; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -22,6 +23,8 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.NonNull; import org.springframework.transaction.PlatformTransactionManager; +import com.spring.infra.batch.BatchJobInfoBeanPostProcessor.BatchJobInfoData; + /** * 배치 작업을 정의하는 추상 클래스입니다. *

@@ -32,9 +35,11 @@ import org.springframework.transaction.PlatformTransactionManager; * @version 1.0 */ @Configuration -public abstract class AbstractBatchTask implements ApplicationContextAware { +public abstract class AbstractBatchTask implements ApplicationContextAware, InitializingBean { private final BatchJobInfo batchJobInfo; + private BatchJobInfoBeanPostProcessor batchJobInfoProcessor; + private BatchJobInfoData batchJobInfoData; private ApplicationContext applicationContext; private JobRepository jobRepository; private PlatformTransactionManager transactionManager; @@ -52,18 +57,23 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { } } - /** - * ApplicationContext를 설정합니다. - * - * @param applicationContext 설정할 ApplicationContext - * @throws BeansException Beans 예외 - */ @Override public void setApplicationContext(@NonNull ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; + } + + @Override + public void afterPropertiesSet() throws Exception { + this.batchJobInfoProcessor = applicationContext.getBean(BatchJobInfoBeanPostProcessor.class); + initializeBatchJobInfo(); registerJobBean(); } + private void initializeBatchJobInfo() { + String beanName = applicationContext.getBeanNamesForType(this.getClass())[0]; + this.batchJobInfoData = batchJobInfoProcessor.getBatchJobInfo(beanName); + } + /** * 배치 작업을 Spring의 Bean으로 등록합니다. */ @@ -71,7 +81,7 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { var beanFactory = ((ConfigurableApplicationContext) applicationContext).getBeanFactory(); var registry = (BeanDefinitionRegistry) beanFactory; var beanDefinitionBuilder = BeanDefinitionBuilder.genericBeanDefinition(Job.class, this::createJob); - registry.registerBeanDefinition(jobName(), beanDefinitionBuilder.getBeanDefinition()); + registry.registerBeanDefinition(batchJobInfoData.getJobName(), beanDefinitionBuilder.getBeanDefinition()); } /** @@ -103,9 +113,9 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { private Job createJob() { List steps = createSteps(); if (steps.isEmpty()) { - throw new IllegalStateException("No steps defined for job: " + jobName()); + throw new IllegalStateException("No steps defined for job: " + batchJobInfoData.getJobName()); } - var jobBuilder = new JobBuilder(jobName()) + var jobBuilder = new JobBuilder(batchJobInfoData.getJobName()) .incrementer(new RunIdIncrementer()) .repository(jobRepository) .start(steps.get(0)); @@ -122,7 +132,7 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { */ protected List createSteps() { List steps = new ArrayList<>(); - steps.add(addStep(jobName() + "Step", createTasklet())); + steps.add(addStep(batchJobInfoData.getJobName() + "Step", createTasklet())); return steps; } @@ -148,40 +158,4 @@ public abstract class AbstractBatchTask implements ApplicationContextAware { */ protected abstract Tasklet createTasklet(); - /** - * 배치 작업 그룹을 반환합니다. - * - * @return 배치 작업 그룹 이름 - */ - public String group() { - return batchJobInfo.group(); - } - - /** - * 배치 작업 이름을 반환합니다. - * - * @return 배치 작업 이름 - */ - public String jobName() { - return batchJobInfo.jobName(); - } - - /** - * cron 표현식을 반환합니다. - * - * @return cron 표현식 - */ - public String cronExpression() { - return batchJobInfo.cronExpression(); - } - - /** - * 배치 작업의 설명을 반환합니다. - * - * @return 배치 작업의 설명 - */ - public String description() { - return batchJobInfo.description(); - } - } diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java index b80d7c2..08c45ca 100644 --- a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfo.java @@ -11,7 +11,7 @@ import java.lang.annotation.Target; * 이 어노테이션은 배치 작업의 그룹, 이름 및 cron 표현식을 설정하는 데 사용됩니다. *

*/ -@Target(ElementType.TYPE) +@Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface BatchJobInfo { /** diff --git a/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java new file mode 100644 index 0000000..77358e6 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/batch/BatchJobInfoBeanPostProcessor.java @@ -0,0 +1,79 @@ +package com.spring.infra.batch; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.InstantiationAwareBeanPostProcessor; +import org.springframework.core.env.Environment; +import org.springframework.lang.NonNull; +import org.springframework.lang.Nullable; +import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Component +@RequiredArgsConstructor +public class BatchJobInfoBeanPostProcessor implements InstantiationAwareBeanPostProcessor { + + private final Environment environment; + private final Map batchJobInfoMap = new ConcurrentHashMap<>(); + + private static final String BASE_PACKAGE = "com.spring"; + + @Override + @Nullable + public Object postProcessBeforeInstantiation(@NonNull Class beanClass, @NonNull String beanName) throws BeansException { + if (beanClass.getPackage().getName().startsWith(BASE_PACKAGE)) { + processAnnotations(beanClass, beanName); + } + return null; + } + + private void processAnnotations(Class beanClass, String beanName) { + if (beanClass.isAnnotationPresent(BatchJobInfo.class)) { + BatchJobInfo batchJobInfo = beanClass.getAnnotation(BatchJobInfo.class); + setBatchJobInfoData(batchJobInfo, beanName); + } + + ReflectionUtils.doWithMethods(beanClass, method -> { + if (method.isAnnotationPresent(BatchJobInfo.class)) { + BatchJobInfo batchJobInfo = method.getAnnotation(BatchJobInfo.class); + String jobBeanName = method.getName(); + setBatchJobInfoData(batchJobInfo, jobBeanName); + } + }); + } + + private void setBatchJobInfoData(BatchJobInfo batchJobInfo, String beanName) { + batchJobInfoMap.put(beanName, processBatchJobInfo(batchJobInfo)); + } + + private BatchJobInfoData processBatchJobInfo(BatchJobInfo batchJobInfo) { + return new BatchJobInfoData( + resolvePlaceHolder(batchJobInfo.group()), + resolvePlaceHolder(batchJobInfo.jobName()), + resolvePlaceHolder(batchJobInfo.cronExpression()), + resolvePlaceHolder(batchJobInfo.description()) + ); + } + + private String resolvePlaceHolder(String value) { + return environment.resolvePlaceholders(value); + } + + public BatchJobInfoData getBatchJobInfo(String beanName) { + return batchJobInfoMap.get(beanName); + } + + @Getter + @RequiredArgsConstructor + public static class BatchJobInfoData { + private final String jobGroup; + private final String jobName; + private final String cronExpression; + private final String description; + } +} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java index fd7549c..74a7fef 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzConfig.java @@ -77,7 +77,7 @@ public class QuartzConfig { factory.setDataSource(dataSource); factory.setTransactionManager(transactionManager); factory.setJobFactory(jobFactory); - factory.setAutoStartup(false); + factory.setAutoStartup(true); factory.setWaitForJobsToCompleteOnShutdown(true); return factory; } diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java deleted file mode 100644 index a61f3b4..0000000 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJob.java +++ /dev/null @@ -1,60 +0,0 @@ -package com.spring.infra.quartz; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Quartz 스케줄러를 통해 실행될 작업을 정의하는 어노테이션입니다. - * - *

이 어노테이션은 메소드 레벨에서 사용되며, 해당 메소드를 Quartz 작업으로 등록합니다.

- * - *

주요 기능:

- *
    - *
  • 작업의 이름 지정
  • - *
  • 작업의 실행 주기를 Cron 표현식으로 정의
  • - *
- * - *

사용 예:

- *
- * {@code
- * @QuartzJob(name = "myJob", cronExpression = "0 0 12 * * ?")
- * public void myScheduledJob() {
- *     // 작업 내용
- * }
- * }
- * 
- * - * @author mindol - * @version 1.0 - */ -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.METHOD) -public @interface QuartzJob { - /** - * Quartz 작업의 그룹을 지정합니다. - * - * @return 그룹의 이름 - */ - String group() default "DEFAULT"; - /** - * Quartz 작업의 이름을 지정합니다. - * - * @return 작업의 이름 - */ - String jobName() default ""; - - /** - * 작업의 실행 주기를 Cron 표현식으로 지정합니다. - * - * @return Cron 표현식 - */ - String cronExpression() default ""; - /** - * Quartz 작업의 설명을 지정합니다. - * - * @return 작업의 설명 - */ - String description() default ""; -} diff --git a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java index 958b807..aee3192 100644 --- a/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java +++ b/batch-quartz/src/main/java/com/spring/infra/quartz/QuartzJobRegistrar.java @@ -1,6 +1,5 @@ package com.spring.infra.quartz; -import java.lang.reflect.Method; import java.util.Map; import org.quartz.CronScheduleBuilder; @@ -14,11 +13,14 @@ import org.quartz.TriggerBuilder; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ContextRefreshedEvent; -import org.springframework.core.annotation.AnnotationUtils; import org.springframework.lang.NonNull; import org.springframework.stereotype.Component; +import org.springframework.util.ReflectionUtils; import com.spring.infra.batch.AbstractBatchTask; +import com.spring.infra.batch.BatchJobInfo; +import com.spring.infra.batch.BatchJobInfoBeanPostProcessor; +import com.spring.infra.batch.BatchJobInfoBeanPostProcessor.BatchJobInfoData; import lombok.RequiredArgsConstructor; @@ -46,6 +48,7 @@ public class QuartzJobRegistrar implements ApplicationListener beanClass = bean.getClass(); - for (Method method : beanClass.getDeclaredMethods()) { - QuartzJob quartzJobAnnotation = AnnotationUtils.findAnnotation(method, QuartzJob.class); - if (quartzJobAnnotation != null) { - scheduleQuartzJob(quartzJobAnnotation); + + ReflectionUtils.doWithMethods(beanClass, method -> { + if (method.isAnnotationPresent(BatchJobInfo.class)) { + BatchJobInfoData batchJobInfoData = batchJobInfoProcessor.getBatchJobInfo(beanName); + if (batchJobInfoData != null) { + scheduleQuartzJob(batchJobInfoData); + } } - } + }); } } - private void scheduleQuartzJob(QuartzJob quartzJobAnnotation) { + private void scheduleQuartzJob(BatchJobInfoData batchJobInfoData) { JobDataMap jobDataMap = new JobDataMap(); - jobDataMap.put("jobName", quartzJobAnnotation.jobName()); + jobDataMap.put("jobName", batchJobInfoData.getJobName()); JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) - .withIdentity(quartzJobAnnotation.jobName(), quartzJobAnnotation.group()) + .withIdentity(batchJobInfoData.getJobName(), batchJobInfoData.getJobGroup()) .setJobData(jobDataMap) .storeDurably(true) - .withDescription(quartzJobAnnotation.description()) + .withDescription(batchJobInfoData.getDescription()) .build(); CronTrigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) - .withIdentity(quartzJobAnnotation.jobName() + "Trigger", quartzJobAnnotation.group()) - .withSchedule(CronScheduleBuilder.cronSchedule(quartzJobAnnotation.cronExpression())) - .withDescription(quartzJobAnnotation.description()) + .withIdentity(batchJobInfoData.getJobName() + "Trigger", batchJobInfoData.getJobGroup()) + .withSchedule(CronScheduleBuilder.cronSchedule(batchJobInfoData.getCronExpression())) + .withDescription(batchJobInfoData.getDescription()) .build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { - throw new IllegalStateException("Error scheduling Quartz job: " + quartzJobAnnotation.jobName(), e); + throw new IllegalStateException("Error scheduling Quartz job: " + batchJobInfoData.getJobName(), e); } } /** * AbstractBatchJob을 상속받은 모든 클래스를 찾아 Quartz 스케줄러에 등록합니다. */ - private void registerAbstractTasks() { + private void registerAbstractBatchJobs() { Map batchJobs = applicationContext.getBeansOfType(AbstractBatchTask.class); - for (AbstractBatchTask batchJob : batchJobs.values()) { - scheduleJob(batchJob); + for (String beanName : batchJobs.keySet()) { + scheduleJob(batchJobInfoProcessor.getBatchJobInfo(beanName)); } } - private void scheduleJob(AbstractBatchTask batchJob) { + private void scheduleJob(BatchJobInfoData batchJobInfoData) { JobDataMap jobDataMap = new JobDataMap(); - jobDataMap.put("jobName", batchJob.jobName()); + jobDataMap.put("jobName", batchJobInfoData.getJobName()); JobDetail jobDetail = JobBuilder.newJob(QuartzJobLauncher.class) - .withIdentity(batchJob.jobName(), batchJob.group()) + .withIdentity(batchJobInfoData.getJobName(), batchJobInfoData.getJobGroup()) .setJobData(jobDataMap) .storeDurably(true) - .withDescription(batchJob.description()) + .withDescription(batchJobInfoData.getDescription()) .build(); CronTrigger trigger = TriggerBuilder.newTrigger() .forJob(jobDetail) - .withIdentity(batchJob.jobName() + "Trigger", batchJob.group()) - .withSchedule(CronScheduleBuilder.cronSchedule(batchJob.cronExpression())) - .withDescription(batchJob.description()) + .withIdentity(batchJobInfoData.getJobName() + "Trigger", batchJobInfoData.getJobGroup()) + .withSchedule(CronScheduleBuilder.cronSchedule(batchJobInfoData.getCronExpression())) + .withDescription(batchJobInfoData.getDescription()) .build(); try { scheduler.scheduleJob(jobDetail, trigger); } catch (SchedulerException e) { - throw new IllegalStateException("Error scheduling AbstractBatchJob: " + batchJob.jobName(), e); + throw new IllegalStateException("Error scheduling AbstractBatchJob: " + batchJobInfoData.getJobName(), e); } } diff --git a/batch-quartz/src/main/java/com/spring/infra/security/domain/JwtUserPrincipal.java b/batch-quartz/src/main/java/com/spring/infra/security/domain/JwtUserPrincipal.java new file mode 100644 index 0000000..6bceb6e --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/domain/JwtUserPrincipal.java @@ -0,0 +1,63 @@ +package com.spring.infra.security.domain; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.security.core.GrantedAuthority; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public class JwtUserPrincipal implements UserPrincipal { + + private final String userId; + private final String userName; + + @Override + public String getKey() { + return null; + } + + @Override + public String getMemberName() { + return null; + } + + @Override + public Collection getAuthorities() { + return Collections.emptyList(); + } + + @Override + public String getPassword() { + return null; + } + + @Override + public String getUsername() { + return userName; + } + + @Override + public boolean isAccountNonExpired() { + return true; + } + + @Override + public boolean isAccountNonLocked() { + return true; + } + + @Override + public boolean isCredentialsNonExpired() { + return true; + } + + @Override + public boolean isEnabled() { + return true; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java b/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java new file mode 100644 index 0000000..a2d3950 --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/infra/security/domain/UserPrincipal.java @@ -0,0 +1,11 @@ +package com.spring.infra.security.domain; + +import org.springframework.security.core.userdetails.UserDetails; + +public interface UserPrincipal extends UserDetails { + + String getKey(); + + String getMemberName(); + +} diff --git a/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java b/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java index e7c1564..fd4f6e5 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/filter/JwtAuthenticationFilter.java @@ -55,14 +55,12 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter { String requestURI = request.getRequestURI(); if (Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).anyMatch(uri -> pathMatcher.match(uri, requestURI)) && - !PermittedURI.ROOT_URI.getUri().equals(requestURI)) - { + !PermittedURI.ROOT_URI.getUri().equals(requestURI)) { filterChain.doFilter(request, response); return; } try { - String accessToken = parseBearerToken(request, HttpHeaders.AUTHORIZATION); if (jwtTokenService.validateAccessToken(accessToken)) { setAuthenticationToContext(accessToken); @@ -71,11 +69,12 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter { String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX); jwtTokenService.validateToken(refreshToken); + + String reissuedAccessToken = jwtTokenService.getRefreshToken(refreshToken); + Authentication authentication = jwtTokenService.getAuthentication(reissuedAccessToken); + jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication)); - Authentication authentication = jwtTokenService.getAuthentication(refreshToken); - jwtTokenService.generateRefreshToken(response, authentication); setAuthenticationToContext(jwtTokenService.generateAccessToken(response, authentication)); - } catch (Exception e) { jwtTokenService.deleteCookie(response); request.setAttribute(EXCEPTION_ATTRIBUTE, e); @@ -91,11 +90,11 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter { * @param token 토큰 */ private void setAuthenticationToContext(final String token) { - Authentication authentication = jwtTokenService.getJwtAuthentication(token); + Authentication authentication = jwtTokenService.getAccessAuthentication(token); SecurityContextHolder.getContext().setAuthentication(authentication); } - private String parseBearerToken(HttpServletRequest request, String headerName) { + private String parseBearerToken(final HttpServletRequest request, final String headerName) { return Optional.ofNullable(request.getHeader(headerName)) .filter(token -> token.substring(0, 7).equalsIgnoreCase(JwtTokenRule.BEARER_PREFIX.getValue())) .map(token -> token.substring(7)) diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SignOutHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SignOutHandler.java index ad1f3e0..be0912e 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SignOutHandler.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SignOutHandler.java @@ -6,6 +6,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.logout.LogoutHandler; +import com.spring.infra.security.jwt.JwtTokenRule; import com.spring.infra.security.jwt.JwtTokenService; import lombok.RequiredArgsConstructor; @@ -17,6 +18,8 @@ public class SignOutHandler implements LogoutHandler { @Override public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) { + String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX); + jwtTokenService.deleteRefreshToken(jwtTokenService.getUserPk(refreshToken)); jwtTokenService.deleteCookie(response); } diff --git a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java index c7a07f9..13eeb14 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/handler/SigninSuccessHandler.java @@ -9,7 +9,6 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.http.MediaType; import org.springframework.security.core.Authentication; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.savedrequest.HttpSessionRequestCache; import org.springframework.security.web.savedrequest.RequestCache; @@ -20,7 +19,6 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.spring.infra.security.config.SecurityURI; import com.spring.infra.security.dto.SignResponse; import com.spring.infra.security.jwt.JwtTokenService; -import com.spring.infra.security.service.RefreshTokenService; import lombok.RequiredArgsConstructor; @@ -29,7 +27,6 @@ import lombok.RequiredArgsConstructor; public class SigninSuccessHandler implements AuthenticationSuccessHandler { private final JwtTokenService jwtTokenService; - private final RefreshTokenService refreshTokenService; private final ObjectMapper objectMapper; private RequestCache requestCache = new HttpSessionRequestCache(); @@ -41,7 +38,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler { ) throws IOException, ServletException { jwtTokenService.generateAccessToken(response, authentication); - refreshTokenService.saveRefreshToken((UserDetails) authentication.getPrincipal(), jwtTokenService.generateRefreshToken(response, authentication)); + jwtTokenService.saveRefreshToken(authentication.getName(), jwtTokenService.generateRefreshToken(response, authentication)); SavedRequest savedRequest = requestCache.getRequest(request, response); String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : SecurityURI.REDIRECT_URI.getUri(); diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java index 545d4bc..df6f8d0 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenGenerator.java @@ -11,6 +11,8 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Component; +import com.spring.infra.security.domain.UserPrincipal; + import io.jsonwebtoken.Jwts; import lombok.RequiredArgsConstructor; @@ -36,10 +38,11 @@ public class JwtTokenGenerator { * @return 생성된 액세스 토큰 */ public String generateAccessToken(Authentication authentication) { + UserPrincipal user = (UserPrincipal) authentication.getPrincipal(); return Jwts.builder() .setHeader(createHeader()) - .setClaims(createClaims(authentication)) - .setSubject(authentication.getName()) + .setClaims(createClaims(user)) + .setSubject(user.getUsername()) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.MINUTES))) .signWith(jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret())) @@ -53,8 +56,9 @@ public class JwtTokenGenerator { * @return 생성된 리프레시 토큰 */ public String generateRefreshToken(Authentication authentication) { + UserPrincipal user = (UserPrincipal) authentication.getPrincipal(); return Jwts.builder() - .setSubject(authentication.getName()) + .setSubject(user.getKey()) .setIssuedAt(Date.from(Instant.now())) .setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.MINUTES))) .signWith(jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret())) @@ -79,9 +83,11 @@ public class JwtTokenGenerator { * @param authentication 인증 정보 * @return JWT 클레임 맵 */ - private Map createClaims(Authentication authentication) { + private Map createClaims(UserPrincipal user) { Map claims = new HashMap<>(); - claims.put(JwtTokenRule.AUTHORITIES_KEY.getValue(), authentication.getAuthorities().stream() + claims.put(JwtTokenRule.USER_ID.getValue(), user.getUsername()); + claims.put(JwtTokenRule.USER_NAME.getValue(), user.getMemberName()); + claims.put(JwtTokenRule.AUTHORITIES_KEY.getValue(), user.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList())); return claims; diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java index 288148f..ead5f89 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenRule.java @@ -38,7 +38,17 @@ public enum JwtTokenRule { /** * JWT 클레임에서 권한 정보를 나타내는 키입니다. */ - AUTHORITIES_KEY("auth"); + AUTHORITIES_KEY("auth"), + + /** + * JWT 클레임에서 사용자ID 정보를 나타내는 키입니다. + */ + USER_ID("userId"), + + /** + * JWT 클레임에서 사용자명 정보를 나타내는 키입니다. + */ + USER_NAME("userName"); private final String value; diff --git a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java index 9f6a13d..02bd5d1 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/jwt/JwtTokenService.java @@ -14,12 +14,14 @@ import org.springframework.security.authentication.UsernamePasswordAuthenticatio import org.springframework.security.core.Authentication; import org.springframework.security.core.authority.SimpleGrantedAuthority; import org.springframework.security.core.userdetails.UserDetails; -import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.spring.domain.user.service.UserPrincipalService; +import com.spring.infra.security.domain.JwtUserPrincipal; import com.spring.infra.security.error.SecurityAuthException; import com.spring.infra.security.error.SecurityExceptionRule; +import com.spring.infra.security.service.RefreshTokenService; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; @@ -37,7 +39,8 @@ public class JwtTokenService { private final JwtTokenUtil jwtTokenUtil; private final JwtTokenGenerator jwtTokenGenerator; - private final UserDetailsService userDetailsService; + private final UserPrincipalService userPrincipalService; + private final RefreshTokenService refreshTokenService; private final Key accessSecretKey; private final Key refreshSecretKey; private final long refreshExpiration; @@ -45,12 +48,14 @@ public class JwtTokenService { public JwtTokenService( JwtTokenUtil jwtTokenUtil, JwtTokenGenerator jwtTokenGenerator, - UserDetailsService userDetailsService, + UserPrincipalService userPrincipalService, + RefreshTokenService refreshTokenService, JwtProperties jwtProperties ) { this.jwtTokenUtil = jwtTokenUtil; this.jwtTokenGenerator = jwtTokenGenerator; - this.userDetailsService = userDetailsService; + this.userPrincipalService = userPrincipalService; + this.refreshTokenService = refreshTokenService; this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret()); this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret()); this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration(); @@ -153,28 +158,40 @@ public class JwtTokenService { * @param token JWT 토큰 * @return 생성된 Authentication 객체 */ - public Authentication getJwtAuthentication(String token) { - Claims claims = getUserClaims(token); - String sub = claims.getSubject(); + public Authentication getAccessAuthentication(String token) { + Claims claims = getUserClaims(token, accessSecretKey); + String userId = claims.get(JwtTokenRule.USER_ID.getValue(), String.class); + String userName = claims.get(JwtTokenRule.USER_NAME.getValue(), String.class); List auths = claims.keySet().stream() .filter(key -> key.equals(JwtTokenRule.AUTHORITIES_KEY.getValue())) .flatMap(key -> ((List) claims.get(key)).stream()) .map(String::valueOf) .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); - return new UsernamePasswordAuthenticationToken(sub, "", auths); + return new UsernamePasswordAuthenticationToken(new JwtUserPrincipal(userId, userName), "", auths); } /** - * 토큰으로부터 인증 정보를 가져와서 DB정보를 조회한다. + * JWT 토큰으로부터 인증 정보를 생성합니다. * * @param token JWT 토큰 * @return 생성된 Authentication 객체 */ - @Transactional(readOnly = true) public Authentication getAuthentication(String token) { - UserDetails principal = userDetailsService.loadUserByUsername(getUserPk(token)); - return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities()); + UserDetails user = userPrincipalService.getUser(getUserPk(token)); + return new UsernamePasswordAuthenticationToken(user, "", null); + } + + public void saveRefreshToken(String userId, String token) { + refreshTokenService.saveRefreshToken(userId, token); + } + + public String getRefreshToken(String token) { + return refreshTokenService.getRefreshToken(getUserPk(token)); + } + + public void deleteRefreshToken(String key) { + refreshTokenService.deleteRefreshToken(key); } /** @@ -183,9 +200,9 @@ public class JwtTokenService { * @param token JWT 토큰 * @return 추출된 사용자 식별자 */ - private Claims getUserClaims(String token) { + private Claims getUserClaims(String token, Key key) { return Jwts.parserBuilder() - .setSigningKey(accessSecretKey) + .setSigningKey(key) .build() .parseClaimsJws(token) .getBody(); @@ -197,9 +214,9 @@ public class JwtTokenService { * @param token JWT 토큰 * @return 추출된 사용자 식별자 */ - private String getUserPk(String token) { + public String getUserPk(String token) { return Jwts.parserBuilder() - .setSigningKey(accessSecretKey) + .setSigningKey(refreshSecretKey) .build() .parseClaimsJws(token) .getBody() diff --git a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java index 116f20d..bacf88e 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/provider/UserAuthenticationProvider.java @@ -4,12 +4,12 @@ import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; -import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Transactional; +import com.spring.infra.security.domain.UserPrincipal; import com.spring.infra.security.error.SecurityAuthException; import com.spring.infra.security.error.SecurityExceptionRule; @@ -27,7 +27,7 @@ public class UserAuthenticationProvider implements AuthenticationProvider { public Authentication authenticate(Authentication authentication) throws AuthenticationException { String username = authentication.getName(); String password = String.valueOf(authentication.getCredentials()); - UserDetails user = userDetailsService.loadUserByUsername(username); + UserPrincipal user = (UserPrincipal) userDetailsService.loadUserByUsername(username); if (isNotMatches(password, user.getPassword())) { throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD.getMessage()); } diff --git a/batch-quartz/src/main/java/com/spring/infra/security/service/RefreshTokenService.java b/batch-quartz/src/main/java/com/spring/infra/security/service/RefreshTokenService.java index 43e0bac..fdbe28f 100644 --- a/batch-quartz/src/main/java/com/spring/infra/security/service/RefreshTokenService.java +++ b/batch-quartz/src/main/java/com/spring/infra/security/service/RefreshTokenService.java @@ -1,11 +1,11 @@ package com.spring.infra.security.service; -import org.springframework.security.core.userdetails.UserDetails; - public interface RefreshTokenService { - void saveRefreshToken(UserDetails user, String refreshToken); + void saveRefreshToken(String userId, String refreshToken); - String findByRefreshToken(String refreshToken); + String getRefreshToken(String refreshToken); + + void deleteRefreshToken(String key); } diff --git a/batch-quartz/src/main/java/com/spring/web/advice/GlobalControllerAdvice.java b/batch-quartz/src/main/java/com/spring/web/advice/GlobalControllerAdvice.java new file mode 100644 index 0000000..2cf47da --- /dev/null +++ b/batch-quartz/src/main/java/com/spring/web/advice/GlobalControllerAdvice.java @@ -0,0 +1,32 @@ +package com.spring.web.advice; + +import java.util.HashMap; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.web.bind.annotation.ControllerAdvice; +import org.springframework.web.bind.annotation.ModelAttribute; + +import com.spring.infra.security.domain.JwtUserPrincipal; +import com.spring.infra.security.domain.UserPrincipal; + +@ControllerAdvice(basePackages = "com.spring.web.controller") +public class GlobalControllerAdvice { + + @ModelAttribute("userInfo") + public Map userInfo() { + Authentication auth = SecurityContextHolder.getContext().getAuthentication(); + Map userInfo = new HashMap<>(); + if (auth != null && auth.isAuthenticated() && auth.getPrincipal() instanceof UserPrincipal) { + JwtUserPrincipal user = (JwtUserPrincipal) auth.getPrincipal(); + userInfo.put("userId", user.getUserId()); + userInfo.put("userName", user.getUsername()); + } else { + userInfo.put("userId", ""); + userInfo.put("userName", ""); + } + return userInfo; + } + +} diff --git a/batch-quartz/src/main/java/com/spring/web/controller/SignController.java b/batch-quartz/src/main/java/com/spring/web/controller/SignController.java index 4d8e85b..7bc7bc6 100644 --- a/batch-quartz/src/main/java/com/spring/web/controller/SignController.java +++ b/batch-quartz/src/main/java/com/spring/web/controller/SignController.java @@ -1,6 +1,5 @@ package com.spring.web.controller; -import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Controller; import org.springframework.ui.Model; import org.springframework.web.bind.annotation.GetMapping; @@ -11,17 +10,9 @@ import com.spring.domain.user.entity.AgentUserRole; @Controller @RequestMapping("/") public class SignController { - - @Value("${front.base.url}") - private String baseUrl; - - @Value("${front.base.timeout}") - private int timeout; @GetMapping public String signIn(Model model) { - model.addAttribute("baseUrl", baseUrl); - model.addAttribute("timeout", timeout); model.addAttribute("roles", AgentUserRole.values()); return "pages/sign/sign-in"; } diff --git a/batch-quartz/src/main/resources/application.yml b/batch-quartz/src/main/resources/application.yml index 67a449c..920f8dd 100644 --- a/batch-quartz/src/main/resources/application.yml +++ b/batch-quartz/src/main/resources/application.yml @@ -91,6 +91,23 @@ spring: settings: web-allow-others: true +batch-info: + email-send-batch: + group: "EMAIL" + job-name: "emailSendJob" + cron-expression: "*/10 * * * * ?" + description: "이메일배치작업" + post-batch: + group: "POST" + job-name: "postJob" + cron-expression: "0/20 * * * * ?" + description: "POST배치작업" + post-create-batch: + group: "POST" + job-name: "postCreateJob" + cron-expression: "0/50 * * * * ?" + description: "테스트배치작업" + jwt: access-token: secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc= @@ -99,11 +116,6 @@ jwt: secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc= expiration: 10080 -front: - base: - url: http://localhost:8081 - timeout: 100000 - logging: level: org: diff --git a/batch-quartz/src/main/resources/static/js/common/axios-instance.js b/batch-quartz/src/main/resources/static/js/common/axios-instance.js index 4ef78d4..01e8d36 100644 --- a/batch-quartz/src/main/resources/static/js/common/axios-instance.js +++ b/batch-quartz/src/main/resources/static/js/common/axios-instance.js @@ -1,31 +1,15 @@ -const baseUrl = window.BASE_URL || ''; -const timeOut = window.TIME_OUT || 100000; -const setAuthorizationHeader = (token) => { - if (token) { - apiClient.defaults.headers['Authorization'] = `Bearer ${token}`; - } else { - delete apiClient.defaults.headers['Authorization']; - } -}; +export const getAccessToken = () => localStorage.getItem('accessToken'); -const getAccessToken = () => localStorage.getItem('accessToken'); - -export const saveAccessToken = (token) => { - localStorage.setItem('accessToken', token); - setAuthorizationHeader(token); -}; +export const saveAccessToken = (token) => localStorage.setItem('accessToken', token); export const removeTokens = () => { localStorage.removeItem('accessToken'); - setAuthorizationHeader(null); location.href = "/"; }; -// Axios apiClient 생성 const apiClient = axios.create({ - baseURL: baseUrl, - timeout: timeOut, + timeout: 100000, headers: { 'Content-Type': 'application/json', }, @@ -36,7 +20,7 @@ apiClient.interceptors.request.use( (config) => { const token = getAccessToken(); if (token) { - setAuthorizationHeader(token); + config.headers['Authorization'] = `Bearer ${token}`; } return config; }, diff --git a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js index a560f62..f1a76a8 100644 --- a/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js +++ b/batch-quartz/src/main/resources/static/js/pages/schedule/schedule.js @@ -4,7 +4,8 @@ import scheduleService from '../../apis/schedule-api.js'; document.addEventListener('DOMContentLoaded', () => { fetchDataAndRender(); - document.getElementById('searchForm').addEventListener('submit', async (e) => { + const searchForm = document.getElementById('searchForm'); + searchForm.addEventListener('submit', async (e) => { e.preventDefault(); fetchDataAndRender(); }); diff --git a/batch-quartz/src/main/resources/templates/fragments/config.html b/batch-quartz/src/main/resources/templates/fragments/config.html index a5aab75..40a0c7d 100644 --- a/batch-quartz/src/main/resources/templates/fragments/config.html +++ b/batch-quartz/src/main/resources/templates/fragments/config.html @@ -3,9 +3,13 @@ - diff --git a/batch-quartz/src/main/resources/templates/fragments/header.html b/batch-quartz/src/main/resources/templates/fragments/header.html index 75b1614..0aeab69 100644 --- a/batch-quartz/src/main/resources/templates/fragments/header.html +++ b/batch-quartz/src/main/resources/templates/fragments/header.html @@ -2,7 +2,7 @@