commit
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<Post> 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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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++;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<AgentUserToken, UUID> {
|
||||
|
||||
Optional<AgentUserToken> findByIdAndReissueCountLessThan(UUID id, long count);
|
||||
|
||||
Optional<AgentUserToken> findByRefreshToken(String refreshToken);
|
||||
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
/**
|
||||
* 배치 작업을 정의하는 추상 클래스입니다.
|
||||
* <p>
|
||||
@@ -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<Step> 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<Step> createSteps() {
|
||||
List<Step> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import java.lang.annotation.Target;
|
||||
* 이 어노테이션은 배치 작업의 그룹, 이름 및 cron 표현식을 설정하는 데 사용됩니다.
|
||||
* </p>
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Target({ElementType.TYPE, ElementType.METHOD})
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface BatchJobInfo {
|
||||
/**
|
||||
|
||||
@@ -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<String, BatchJobInfoData> 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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 스케줄러를 통해 실행될 작업을 정의하는 어노테이션입니다.
|
||||
*
|
||||
* <p>이 어노테이션은 메소드 레벨에서 사용되며, 해당 메소드를 Quartz 작업으로 등록합니다.</p>
|
||||
*
|
||||
* <p>주요 기능:</p>
|
||||
* <ul>
|
||||
* <li>작업의 이름 지정</li>
|
||||
* <li>작업의 실행 주기를 Cron 표현식으로 정의</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>사용 예:</p>
|
||||
* <pre>
|
||||
* {@code
|
||||
* @QuartzJob(name = "myJob", cronExpression = "0 0 12 * * ?")
|
||||
* public void myScheduledJob() {
|
||||
* // 작업 내용
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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 "";
|
||||
}
|
||||
@@ -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<ContextRefreshedE
|
||||
|
||||
private final Scheduler scheduler;
|
||||
private final ApplicationContext applicationContext;
|
||||
private final BatchJobInfoBeanPostProcessor batchJobInfoProcessor;
|
||||
|
||||
/**
|
||||
* 애플리케이션 컨텍스트가 리프레시될 때 호출되는 메소드입니다.
|
||||
@@ -56,8 +59,8 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
|
||||
@Override
|
||||
public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
|
||||
clearJobs();
|
||||
registerQuartzJobs();
|
||||
registerAbstractTasks();
|
||||
registerBatchJobs();
|
||||
registerAbstractBatchJobs();
|
||||
}
|
||||
|
||||
private void clearJobs() {
|
||||
@@ -69,78 +72,81 @@ public class QuartzJobRegistrar implements ApplicationListener<ContextRefreshedE
|
||||
}
|
||||
|
||||
/**
|
||||
* QuartzJob 어노테이션이 붙은 모든 메소드를 찾아 Quartz 스케줄러에 등록합니다.
|
||||
* BatchJobInfo 어노테이션이 붙은 모든 메소드를 찾아 Quartz 스케줄러에 등록합니다.
|
||||
*/
|
||||
private void registerQuartzJobs() {
|
||||
private void registerBatchJobs() {
|
||||
for (String beanName : applicationContext.getBeanDefinitionNames()) {
|
||||
Object bean = applicationContext.getBean(beanName);
|
||||
Class<?> 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<String, AbstractBatchTask> 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<? extends GrantedAuthority> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
}
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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<String, Object> createClaims(Authentication authentication) {
|
||||
private Map<String, Object> createClaims(UserPrincipal user) {
|
||||
Map<String, Object> 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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<SimpleGrantedAuthority> 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()
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
@@ -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<String, String> userInfo() {
|
||||
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
|
||||
Map<String, String> 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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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";
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
},
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -3,9 +3,13 @@
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/bootstrap-icons.css}">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
<script>
|
||||
const BASE_URL = /*[[${baseUrl}]]*/ '';
|
||||
const TIME_OUT = /*[[${timeout}]]*/ '';
|
||||
<script th:inline="javascript">
|
||||
/*<![CDATA[*/
|
||||
const USER_INFO = {
|
||||
userId: /*[[${userInfo?.userId ?: ''}]]*/ '',
|
||||
userName: /*[[${userInfo?.userName ?: ''}]]*/ ''
|
||||
};
|
||||
/*]]>*/
|
||||
</script>
|
||||
<script th:src="@{/js/lib/axios/axios.min.js}"></script>
|
||||
<script th:src="@{/js/lib/bootstrap/popper.min.js}"></script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:fragment="header" lang="ko" xml:lang="ko">
|
||||
<header id="header" class="header fixed-top d-flex align-items-center">
|
||||
<div class="d-flex align-items-center justify-content-between">
|
||||
<a href="index.html" class="logo d-flex align-items-center">
|
||||
<a href="/" class="logo d-flex align-items-center">
|
||||
<span class="d-none d-lg-block">NXCUS - Agent2.0</span>
|
||||
</a>
|
||||
<button class="btn toggle-sidebar-btn" id="toggleSidebar" aria-label="Toggle Sidebar">
|
||||
@@ -14,7 +14,7 @@
|
||||
<li class="nav-item dropdown pe-3">
|
||||
<button class="nav-link nav-profile d-flex align-items-center pe-0 dropdown-toggle" id="profileDropdown" data-bs-toggle="dropdown" aria-expanded="false">
|
||||
<i class="bi bi-person-circle"></i>
|
||||
<span class="d-none d-md-block ps-2">K. Anderson</span>
|
||||
<span class="d-none d-md-block ps-2" th:text="${userInfo.userName}"></span>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end w-auto" aria-labelledby="profileDropdown">
|
||||
<li>
|
||||
|
||||
Reference in New Issue
Block a user