This commit is contained in:
mindol1004
2024-09-27 17:41:37 +09:00
parent cf9d54ff85
commit 10e0f0bb97
38 changed files with 454 additions and 160 deletions

View File

@@ -5,16 +5,16 @@ import lombok.Getter;
@Getter
public class BizBaseException extends RuntimeException {
private final ExceptionRule exceptionRule;
private final ErrorRule errorRule;
public BizBaseException() {
super(ExceptionRule.SYSTE_ERROR.getMessage());
this.exceptionRule = ExceptionRule.SYSTE_ERROR;
this.errorRule = ExceptionRule.SYSTE_ERROR;
}
public BizBaseException(ExceptionRule exceptionRule) {
public BizBaseException(ErrorRule exceptionRule) {
super(exceptionRule.getMessage());
this.exceptionRule = exceptionRule;
this.errorRule = exceptionRule;
}
}

View File

@@ -10,12 +10,12 @@ import lombok.Getter;
@Getter
public class BizErrorResponse extends ErrorResponse {
public BizErrorResponse(ExceptionRule exceptionRule, List<RejectedValue> errors) {
public BizErrorResponse(ErrorRule exceptionRule, List<RejectedValue> errors) {
super(exceptionRule);
this.errors = errors;
}
public static BizErrorResponse valueOf(ExceptionRule exceptionRule) {
public static BizErrorResponse valueOf(ErrorRule exceptionRule) {
return new BizErrorResponse(exceptionRule, null);
}

View File

@@ -16,9 +16,7 @@ public enum ExceptionRule implements ErrorRule {
NOT_FOUND(HttpStatus.NOT_FOUND, "요청한 자원을 찾을 수 없습니다."),
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR, "서버에 오류가 발생했습니다."),
CONFLICT(HttpStatus.CONFLICT, "데이터 충돌이 발생했습니다."),
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다."),
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "스케쥴 정보를 찾을 수 없습니다.");
UNPROCESSABLE_ENTITY(HttpStatus.UNPROCESSABLE_ENTITY, "요청을 처리할 수 없습니다.");
private final HttpStatus status;
private String message;

View File

@@ -9,7 +9,7 @@ public class GlobalExceptionHandler {
@ExceptionHandler(BizBaseException.class)
public BizErrorResponse handleCustomException(BizBaseException e) {
return BizErrorResponse.valueOf(e.getExceptionRule());
return BizErrorResponse.valueOf(e.getErrorRule());
}
@ExceptionHandler(MethodArgumentNotValidException.class)

View File

@@ -0,0 +1,19 @@
package com.spring.common.validation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import javax.validation.Constraint;
import javax.validation.Payload;
@Constraint(validatedBy = {EnumValidator.class})
@Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface EnumValid {
String message() default "Invalid Enum Value.";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
Class<? extends java.lang.Enum<?>> target();
}

View File

@@ -0,0 +1,28 @@
package com.spring.common.validation;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
public class EnumValidator implements ConstraintValidator<EnumValid, Enum<?>> {
private EnumValid annotation;
@Override
public void initialize(EnumValid constraintAnnotation) {
this.annotation = constraintAnnotation;
}
@Override
public boolean isValid(Enum<?> value, ConstraintValidatorContext context) {
if (value == null) return false;
Object[] enumValues = this.annotation.target().getEnumConstants();
if (enumValues != null) {
for (Object enumValue : enumValues) {
if (value.toString().equals(enumValue.toString())) {
return true;
}
}
}
return false;
}
}

View File

@@ -1,16 +1,11 @@
package com.spring.domain.schedule.error;
import com.spring.common.error.BizBaseException;
import com.spring.common.error.ExceptionRule;
public class ScheduleNotFoundException extends BizBaseException {
public ScheduleNotFoundException() {
super(ExceptionRule.SCHEDULE_NOT_FOUND);
}
public ScheduleNotFoundException(ExceptionRule exceptionRule) {
super(exceptionRule);
super(ScheduleRule.SCHEDULE_NOT_FOUND);
}
}

View File

@@ -0,0 +1,29 @@
package com.spring.domain.schedule.error;
import org.springframework.http.HttpStatus;
import com.spring.common.error.ErrorRule;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum ScheduleRule implements ErrorRule {
SCHEDULE_NOT_FOUND(HttpStatus.NOT_FOUND, "스케쥴 정보를 찾을 수 없습니다.");
private final HttpStatus status;
private String message;
ScheduleRule(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
public ScheduleRule message(final String message) {
this.message = message;
return this;
}
}

View File

@@ -2,6 +2,8 @@ package com.spring.domain.user.api;
import javax.validation.Valid;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -19,6 +21,11 @@ public class SignApi {
private final SignUpService signUpService;
@GetMapping("/conflict/{userId}")
public boolean isConflictUserId(@PathVariable String userId) {
return signUpService.isConflictUserId(userId);
}
@PostMapping("/sign-up")
public void signUp(@RequestBody @Valid SignUpRequest request) {
signUpService.signUp(request);

View File

@@ -1,13 +0,0 @@
package com.spring.domain.user.dto;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public class SignInRequest {
private final String username;
private final String password;
}

View File

@@ -2,7 +2,9 @@ package com.spring.domain.user.dto;
import javax.validation.constraints.NotBlank;
import com.spring.domain.user.entity.AppUser;
import com.spring.common.validation.EnumValid;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserRole;
import lombok.AccessLevel;
import lombok.Getter;
@@ -12,21 +14,28 @@ import lombok.NoArgsConstructor;
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class SignUpRequest {
@NotBlank(message = "로그인ID는 필수값 입니다.")
private String loginId;
@NotBlank(message = "사용자ID는 필수값 입니다.")
private String userId;
@NotBlank(message = "비밀번호는 필수값 입니다.")
private String password;
private String userPassword;
@NotBlank(message = "사용자명은 필수값 입니다.")
private String userName;
@EnumValid(target = AgentUserRole.class, message = "올바른 값을 입력해주세요.")
private AgentUserRole userRole;
public void encodePassword(String password) {
this.password = password;
this.userPassword = password;
}
public AppUser toEntity() {
return AppUser.builder()
.loginId(loginId)
.password(password)
public AgentUser toEntity() {
return AgentUser.builder()
.userId(userId)
.userPassword(userPassword)
.userName(userName)
.userRole(userRole)
.build();
}

View File

@@ -0,0 +1,61 @@
package com.spring.domain.user.entity;
import java.util.UUID;
import javax.persistence.CascadeType;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToOne;
import javax.persistence.Table;
import org.hibernate.annotations.GenericGenerator;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "AGENT_USER")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class AgentUser {
@Id
@GeneratedValue(generator = "uuid2")
@GenericGenerator(name="uuid2", strategy = "uuid2")
@Column(name = "ID", nullable = false, columnDefinition = "BINARY(16)")
private UUID id;
// @OneToOne(mappedBy = "agentUser", cascade = CascadeType.ALL, orphanRemoval = true)
// private AgentUserToken agentUserToken;
@Column(name = "USER_ID", nullable = false, length = 50)
private String userId;
@Column(name = "USER_PASSWORD", nullable = false, length = 128)
private String userPassword;
@Column(name = "USER_NAME", nullable = false, length = 50)
private String userName;
@Enumerated(EnumType.STRING)
@Column(name = "USER_ROLE", nullable = false, length = 50)
private AgentUserRole userRole;
@Builder
public AgentUser(String userId, String userPassword, String userName, AgentUserRole userRole) {
this.userId = userId;
this.userPassword = userPassword;
this.userName = userName;
this.userRole = userRole;
}
}

View File

@@ -0,0 +1,31 @@
package com.spring.domain.user.entity;
import java.util.Arrays;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum AgentUserRole {
ROLE_USER("ROLE_USER", "사용자"),
ROLE_ADMIN("ROLE_ADMIN", "관리자"),
ROLE_ANONYMOUS("ROLE_ANONYMOUS", "익명사용자");
@JsonValue
private final String role;
private final String roleName;
@JsonCreator(mode = JsonCreator.Mode.DELEGATING)
public static AgentUserRole fromRole(String role) {
return Arrays.stream(values())
.filter(value -> value.getRole().equalsIgnoreCase(role))
.findFirst()
.orElse(null);
}
}

View File

@@ -5,6 +5,7 @@ import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.ForeignKey;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.MapsId;
@@ -12,23 +13,24 @@ import javax.persistence.OneToOne;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "MEMBER_REFRESH_TOKEN")
@Table(name = "AGENT_USER_TOKEN")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class MemberRefreshToken {
public class AgentUserToken {
@Id
@Column(name = "MEMBER_ID", nullable = false)
private UUID memberId;
@Column(name = "ID", nullable = false)
private UUID id;
@OneToOne(fetch = FetchType.LAZY)
@MapsId
@JoinColumn(name = "member_id")
private Member member;
@JoinColumn(name = "ID", referencedColumnName = "ID", columnDefinition = "BINARY(16)", foreignKey = @ForeignKey(name = "FK_AGENT_USER_TOKEN"))
private AgentUser agentUser;
@Column(name = "REFRESH_TOKEN", nullable = false)
private String refreshToken;
@@ -36,6 +38,13 @@ public class MemberRefreshToken {
@Column(name = "REISSUE_COUNT", nullable = false)
private int reissueCount;
@Builder
public AgentUserToken(AgentUser agentUser, String refreshToken, int reissueCount) {
this.agentUser = agentUser;
this.refreshToken = refreshToken;
this.reissueCount = reissueCount;
}
public void updateRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}

View File

@@ -1,33 +0,0 @@
package com.spring.domain.user.entity;
import java.util.UUID;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Entity
@Table(name = "MEMBER")
@Getter
@NoArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {
@Id
@Column(name = "MEMBER_ID", nullable = false)
private UUID memberId;
@Column(name = "LOGIN_ID", nullable = false, length = 50)
private String loginId;
@Column(name = "PASSWORD", nullable = false, length = 128)
private String password;
@Column(name = "MEMBER_NAME", nullable = false, length = 50)
private String userName;
}

View File

@@ -0,0 +1,11 @@
package com.spring.domain.user.error;
import com.spring.common.error.BizBaseException;
public class UserIdConflictException extends BizBaseException {
public UserIdConflictException() {
super(UserRule.USER_ID_CONFLICT);
}
}

View File

@@ -1,16 +1,11 @@
package com.spring.domain.user.error;
import com.spring.common.error.BizBaseException;
import com.spring.common.error.ExceptionRule;
public class UserNotFoundException extends BizBaseException {
public UserNotFoundException() {
super(ExceptionRule.USER_NOT_FOUND);
}
public UserNotFoundException(ExceptionRule exceptionRule) {
super(exceptionRule);
super(UserRule.USER_NOT_FOUND);
}
}

View File

@@ -0,0 +1,30 @@
package com.spring.domain.user.error;
import org.springframework.http.HttpStatus;
import com.spring.common.error.ErrorRule;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum UserRule implements ErrorRule {
USER_NOT_FOUND(HttpStatus.NOT_FOUND, "사용자를 찾을 수 없습니다."),
USER_ID_CONFLICT(HttpStatus.CONFLICT, "중복된 아이디 입니다.");
private final HttpStatus status;
private String message;
UserRule(HttpStatus status, String message) {
this.status = status;
this.message = message;
}
public UserRule message(final String message) {
this.message = message;
return this;
}
}

View File

@@ -0,0 +1,16 @@
package com.spring.domain.user.repository;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.domain.user.entity.AgentUser;
public interface AgentUserRepository extends JpaRepository<AgentUser, UUID> {
Optional<AgentUser> findByUserId(String userId);
boolean existsByUserId(String userId);
}

View File

@@ -0,0 +1,14 @@
package com.spring.domain.user.repository;
import java.util.Optional;
import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository;
import com.spring.domain.user.entity.AgentUserToken;
public interface AgentUserTokenRepository extends JpaRepository<AgentUserToken, UUID> {
Optional<AgentUserToken> findByIdAndReissueCountLessThan(UUID id, long count);
}

View File

@@ -5,7 +5,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.dto.SignUpRequest;
import com.spring.domain.user.repository.AppUserRepository;
import com.spring.domain.user.error.UserIdConflictException;
import com.spring.domain.user.repository.AgentUserRepository;
import lombok.RequiredArgsConstructor;
@@ -13,13 +14,20 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class SignUpService {
private final AppUserRepository appUserRepository;
private final AgentUserRepository agentUserRepository;
private final PasswordEncoder passwordEncoder;
@Transactional(readOnly = true)
public boolean isConflictUserId(String userId) {
boolean result = agentUserRepository.existsByUserId(userId);
if (result) throw new UserIdConflictException();
return result;
}
@Transactional
public void signUp(SignUpRequest request) {
request.encodePassword(passwordEncoder.encode(request.getPassword()));
appUserRepository.save(request.toEntity());
request.encodePassword(passwordEncoder.encode(request.getUserPassword()));
agentUserRepository.save(request.toEntity());
}
}

View File

@@ -77,7 +77,7 @@ public class QuartzConfig {
factory.setDataSource(dataSource);
factory.setTransactionManager(transactionManager);
factory.setJobFactory(jobFactory);
factory.setAutoStartup(true);
factory.setAutoStartup(false);
factory.setWaitForJobsToCompleteOnShutdown(true);
return factory;
}

View File

@@ -0,0 +1,19 @@
package com.spring.infra.security.config;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum PermittedURI {
ROOT_URI("/"),
H2_CONSOLE_URI("/h2-console/**"),
FAVICON_URI("/favicon.ico"),
USER_CONFLICT_URI("/api/user/conflict/{userId}"),
USER_SIGN_UP("/api/user/sign-up"),
USER_SIGN_IN("/sign-in"),
USER_SIGN_OUT("/sign-out");
private final String uri;
}

View File

@@ -1,6 +1,6 @@
package com.spring.infra.security.config;
import java.util.List;
import java.util.Arrays;
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
import org.springframework.context.annotation.Bean;
@@ -47,13 +47,6 @@ import com.spring.infra.security.provider.UserAuthenticationProvider;
@EnableWebSecurity
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
public class SecurityConfig {
private static final String[] PERMITTED_URI = {
"/",
"/h2-console/**",
"/favicon.ico",
"/api/user/sign-up"
};
/**
* Spring Security의 필터 체인을 구성합니다.
@@ -79,13 +72,13 @@ public class SecurityConfig {
.httpBasic(HttpBasicConfigurer::disable)
.formLogin(FormLoginConfigurer::disable)
.authorizeHttpRequests(auth -> auth
.antMatchers(PERMITTED_URI).permitAll()
.antMatchers(Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).toArray(String[]::new)).permitAll()
.anyRequest().authenticated()
)
.logout(logout -> logout
.logoutUrl("/sign-out")
.logoutUrl(PermittedURI.USER_SIGN_OUT.getUri())
.addLogoutHandler(new SignOutHandler(tokenService))
.logoutSuccessUrl("/")
.logoutSuccessUrl(PermittedURI.ROOT_URI.getUri())
.invalidateHttpSession(true))
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
@@ -95,7 +88,7 @@ public class SecurityConfig {
UsernamePasswordAuthenticationFilter.class
)
.addFilterAfter(
new JwtAuthenticationFilter(tokenService, List.of(PERMITTED_URI)),
new JwtAuthenticationFilter(tokenService),
AuthenticationProcessingFilter.class
)
.addFilterAfter(

View File

@@ -0,0 +1,14 @@
package com.spring.infra.security.config;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@Getter
@RequiredArgsConstructor
public enum SecurityURI {
REDIRECT_URI("/dashboard");
private final String uri;
}

View File

@@ -1,5 +1,6 @@
package com.spring.infra.security.domain;
import java.util.Arrays;
import java.util.Collection;
import java.util.stream.Collectors;
@@ -7,7 +8,8 @@ import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.spring.domain.user.entity.AppUser;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.entity.AgentUserRole;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
@@ -23,16 +25,16 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public final class UserPrincipal implements UserDetails {
private final transient AppUser appUser;
private final transient AgentUser agentUser;
/**
* AppUser 객체로부터 UserPrincipal 객체를 생성합니다.
* AgentUser 객체로부터 UserPrincipal 객체를 생성합니다.
*
* @param appUser 변환할 AppUser 객체
* @param agentUser 변환할 AgentUser 객체
* @return 생성된 UserPrincipal 객체
*/
public static UserPrincipal valueOf(AppUser appUser) {
return new UserPrincipal(appUser);
public static UserPrincipal valueOf(AgentUser agentUser) {
return new UserPrincipal(agentUser);
}
/**
@@ -42,10 +44,11 @@ public final class UserPrincipal implements UserDetails {
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return appUser.getAppUserRoleMap().stream()
.map(role -> role.getAppUserRole().getRoleType())
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return Arrays.stream(AgentUserRole.values())
.filter(role -> Arrays.asList(agentUser.getUserRole()).contains(role))
.map(AgentUserRole::getRole)
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
}
/**
@@ -96,7 +99,7 @@ public final class UserPrincipal implements UserDetails {
*/
@Override
public String getPassword() {
return appUser.getPassword();
return agentUser.getUserPassword();
}
/**
@@ -106,7 +109,7 @@ public final class UserPrincipal implements UserDetails {
*/
@Override
public String getUsername() {
return appUser.getLoginId();
return agentUser.getUserName();
}
}

View File

@@ -14,6 +14,7 @@ public enum SecurityExceptionRule implements ErrorRule {
UNSUPPORTED_MEDIA_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원되지 않는 유형 입니다."),
USER_BAD_REQUEST(HttpStatus.BAD_REQUEST, "사용자 정보가 올바르지 않습니다."),
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
USER_NOT_PASSWORD(HttpStatus.UNAUTHORIZED, "비밀번호가 틀립니다."),
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
JWT_TOKEN_NOT_FOUND(HttpStatus.UNAUTHORIZED, "토큰 정보가 없습니다."),

View File

@@ -17,13 +17,14 @@ import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.util.StringUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.spring.infra.security.config.PermittedURI;
import com.spring.infra.security.dto.SignInRequest;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
public class AuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {
private static final String DEFAULT_LOGIN_REQUEST_URL = "/sign-in";
private static final String DEFAULT_LOGIN_REQUEST_URL = PermittedURI.USER_SIGN_IN.getUri();
private static final String HTTP_METHOD = "POST";
private static final AntPathRequestMatcher DEFAULT_LOGIN_PATH_REQUEST_MATCHER =
new AntPathRequestMatcher(DEFAULT_LOGIN_REQUEST_URL, HTTP_METHOD);

View File

@@ -1,7 +1,7 @@
package com.spring.infra.security.filter;
import java.io.IOException;
import java.util.List;
import java.util.Arrays;
import java.util.Optional;
import javax.servlet.FilterChain;
@@ -16,6 +16,7 @@ import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;
import com.spring.infra.security.config.PermittedURI;
import com.spring.infra.security.jwt.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;
@@ -34,7 +35,6 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final JwtTokenService jwtTokenService;
private final List<String> permitAllUrls;
private static final String EXCEPTION_ATTRIBUTE = "exception";
/**
@@ -54,7 +54,9 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
) throws ServletException, IOException {
String requestURI = request.getRequestURI();
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI)) && !"/".equals(requestURI)) {
if (Arrays.stream(PermittedURI.values()).map(PermittedURI::getUri).anyMatch(uri -> pathMatcher.match(uri, requestURI)) &&
!PermittedURI.ROOT_URI.getUri().equals(requestURI))
{
filterChain.doFilter(request, response);
return;
}

View File

@@ -14,6 +14,8 @@ import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerExceptionResolver;
import com.spring.infra.security.config.PermittedURI;
import lombok.RequiredArgsConstructor;
/**
@@ -40,7 +42,7 @@ public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoin
} else if (isApiRequest(request)) {
handleApiRequest(request, response, authException);
} else {
response.sendRedirect("/");
response.sendRedirect(PermittedURI.ROOT_URI.getUri());
}
}

View File

@@ -17,6 +17,7 @@ import org.springframework.security.web.savedrequest.SavedRequest;
import org.springframework.stereotype.Component;
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.JwtTokenRule;
import com.spring.infra.security.jwt.JwtTokenService;
@@ -42,7 +43,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
jwtTokenService.generateRefreshToken(response, authentication);
SavedRequest savedRequest = requestCache.getRequest(request, response);
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : "/dashboard";
String targetUrl = (savedRequest != null) ? savedRequest.getRedirectUrl() : SecurityURI.REDIRECT_URI.getUri();
response.setHeader(HttpHeaders.AUTHORIZATION, JwtTokenRule.BEARER_PREFIX.getValue() + accessToken);
response.setContentType(MediaType.APPLICATION_JSON_VALUE);

View File

@@ -4,6 +4,7 @@ import java.security.Key;
import java.util.List;
import java.util.stream.Collectors;
import javax.persistence.EntityNotFoundException;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@@ -17,6 +18,11 @@ import org.springframework.security.core.userdetails.UserDetails;
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.repository.AgentUserRepository;
import com.spring.domain.user.repository.AgentUserTokenRepository;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.UserPrincipalService;
@@ -38,6 +44,8 @@ public class JwtTokenService {
private final JwtTokenUtil jwtTokenUtil;
private final JwtTokenGenerator jwtTokenGenerator;
private final UserPrincipalService userPrincipalService;
private final AgentUserRepository agentUserRepository;
private final AgentUserTokenRepository agentUserTokenRepository;
private final Key accessSecretKey;
private final Key refreshSecretKey;
private final long refreshExpiration;
@@ -46,11 +54,15 @@ public class JwtTokenService {
JwtTokenUtil jwtTokenUtil,
JwtTokenGenerator jwtTokenGenerator,
UserPrincipalService userPrincipalService,
AgentUserRepository agentUserRepository,
AgentUserTokenRepository agentUserTokenRepository,
JwtProperties jwtProperties
) {
this.jwtTokenUtil = jwtTokenUtil;
this.jwtTokenGenerator = jwtTokenGenerator;
this.userPrincipalService = userPrincipalService;
this.agentUserRepository = agentUserRepository;
this.agentUserTokenRepository = agentUserTokenRepository;
this.accessSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret());
this.refreshSecretKey = jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret());
this.refreshExpiration = jwtProperties.getRefreshToken().getExpiration();
@@ -76,8 +88,20 @@ public class JwtTokenService {
* @param authentication 인증 정보
* @return 생성된 리프레시 토큰
*/
@Transactional
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
UserPrincipal user = (UserPrincipal) authentication.getPrincipal();
AgentUser agentUser = agentUserRepository.findById(user.getAgentUser().getId()).orElseThrow(() -> new EntityNotFoundException("AgentUser not found"));
agentUserTokenRepository.findById(agentUser.getId())
.ifPresentOrElse(
it -> it.updateRefreshToken(refreshToken),
() -> agentUserTokenRepository.save(
AgentUserToken.builder().agentUser(agentUser).refreshToken(refreshToken).build()
)
);
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
return refreshToken;

View File

@@ -1,7 +1,6 @@
package com.spring.infra.security.provider;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
@@ -10,6 +9,8 @@ import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
import com.spring.infra.security.error.SecurityAuthException;
import com.spring.infra.security.error.SecurityExceptionRule;
import com.spring.infra.security.service.UserPrincipalService;
import lombok.RequiredArgsConstructor;
@@ -24,11 +25,11 @@ public class UserAuthenticationProvider implements AuthenticationProvider {
@Transactional(readOnly = true)
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String loginId = authentication.getName();
String username = authentication.getName();
String password = String.valueOf(authentication.getCredentials());
UserDetails user = userPrincipalService.loadUserByUsername(loginId);
UserDetails user = userPrincipalService.loadUserByUsername(username);
if (isNotMatches(password, user.getPassword())) {
throw new BadCredentialsException(loginId);
throw new SecurityAuthException(SecurityExceptionRule.USER_NOT_PASSWORD.getMessage());
}
return new UsernamePasswordAuthenticationToken(user, user.getPassword(), user.getAuthorities());
}

View File

@@ -6,8 +6,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.spring.domain.user.entity.AppUser;
import com.spring.domain.user.repository.AppUserRepository;
import com.spring.domain.user.entity.AgentUser;
import com.spring.domain.user.repository.AgentUserRepository;
import com.spring.infra.security.domain.UserPrincipal;
import com.spring.infra.security.error.SecurityExceptionRule;
@@ -17,12 +17,12 @@ import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class UserPrincipalService implements UserDetailsService {
private final AppUserRepository appUserRepository;
private final AgentUserRepository agentUserRepository;
@Transactional(readOnly = true)
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
AppUser user = appUserRepository.findByLoginId(username)
AgentUser user = agentUserRepository.findByUserId(username)
.orElseThrow(() -> new UsernameNotFoundException(SecurityExceptionRule.USER_UNAUTHORIZED.getMessage()));
return UserPrincipal.valueOf(user);
}

View File

@@ -6,6 +6,8 @@ import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import com.spring.domain.user.entity.AgentUserRole;
@Controller
@RequestMapping("/")
public class SignController {
@@ -20,6 +22,7 @@ public class SignController {
public String signIn(Model model) {
model.addAttribute("baseUrl", baseUrl);
model.addAttribute("timeout", timeout);
model.addAttribute("roles", AgentUserRole.values());
return "pages/sign/sign-in";
}

View File

@@ -14,9 +14,14 @@ const signService = {
removeTokens();
},
signUp: async (loginId, password, userName) => {
const response = await apiClient.post('/api/user/sign-up', { loginId, password, userName });
signUp: async (params) => {
const response = await apiClient.post('/api/user/sign-up', params);
return response.data;
},
isConflictUserId: async (userId) => {
const response = await apiClient.get(`/api/user/conflict/${userId}`);
return response.data.data;
}
};

View File

@@ -1,14 +1,12 @@
import signService from '../../apis/sign-api.js';
document.addEventListener('DOMContentLoaded', () => {
const signupModal = new bootstrap.Modal(document.getElementById('signupModal'));
const signInButton = document.getElementById('signIn');
const signupButton = document.getElementById('signUp');
const signupModal = new bootstrap.Modal(document.getElementById('signupModal'));
const signupSubmit = document.getElementById('signupSubmit');
const signupForm = document.getElementById('signupForm');
signInButton.addEventListener('click', (e) => {
e.preventDefault();
signInButton.addEventListener('click', () => {
const username = document.getElementById('username').value;
const password = document.getElementById('password').value;
signService.signIn(username, password).then(response => {
@@ -18,20 +16,25 @@ document.addEventListener('DOMContentLoaded', () => {
});
});
signupButton.addEventListener('click', (e) => {
e.preventDefault();
signupButton.addEventListener('click', () => {
signupModal.show();
});
signupSubmit.addEventListener('click', (e) => {
signupForm.addEventListener('submit', async (e) => {
e.preventDefault();
const loginId = document.getElementById('loginId').value;
const password = document.getElementById('loginPassword').value;
const userName = document.getElementById('userName').value;
signService.signUp(loginId, password, userName).then(() => {
alert(`회원가입이 완료 되었습니다.`);
signupModal.hide();
});
const userId = document.getElementById('userId').value;
const isConflict = await signService.isConflictUserId(userId);
if (!isConflict) {
const params = Object.fromEntries(new FormData(signupForm));
signService.signUp(params).then(() => {
alert(`회원가입이 완료 되었습니다.`);
signupModal.hide();
});
}
});
document.getElementById('signupModal').addEventListener('hidden.bs.modal', () => {
signupForm.reset();
});
});

View File

@@ -54,36 +54,44 @@
<h5 class="modal-title" id="signupModalLabel">회원가입</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form id="signupForm">
<form id="signupForm">
<div class="modal-body">
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-person"></i></span>
<input type="text" class="form-control" id="loginId" placeholder="아이디" required>
<input type="text" class="form-control" id="userId" name="userId" placeholder="아이디" required>
</div>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-key"></i></span>
<input type="password" class="form-control" id="loginPassword" placeholder="비밀번호" required>
<input type="password" class="form-control" id="userPassword" name="userPassword" placeholder="비밀번호" required>
</div>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-envelope"></i></span>
<input type="text" class="form-control" id="userName" placeholder="사용자명" required>
<span class="input-group-text"><i class="bi bi-person-vcard"></i></span>
<input type="text" class="form-control" id="userName" name="userName" placeholder="사용자명" required>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-2"></i>취소
</button>
<button type="button" class="btn btn-primary" id="signupSubmit">
<i class="bi bi-check-circle me-2"></i>가입하기
</button>
</div>
<div class="mb-3">
<div class="input-group">
<span class="input-group-text"><i class="bi bi-shield-lock"></i></span>
<select id="userRole" name="userRole" class="form-select" required>
<option th:each="role : ${roles}" th:value="${role}" th:text="${role.roleName}"></option>
</select>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
<i class="bi bi-x-circle me-2"></i>취소
</button>
<button type="submit" class="btn btn-outline-primary" id="signupSubmit">
<i class="bi bi-check-circle me-2"></i>가입하기
</button>
</div>
</form>
</div>
</div>
</div>