commit
This commit is contained in:
@@ -65,6 +65,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-thymeleaf</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>nz.net.ultraq.thymeleaf</groupId>
|
||||
<artifactId>thymeleaf-layout-dialect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.spring.common.advice;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class ApiResponse<T> {
|
||||
|
||||
private final LocalDateTime timestamp = LocalDateTime.now();
|
||||
private int statusCode;
|
||||
private String path;
|
||||
private T data;
|
||||
private String message;
|
||||
|
||||
@Builder
|
||||
private ApiResponse(int statusCode, String path, T data, String message) {
|
||||
this.statusCode = statusCode;
|
||||
this.path = path;
|
||||
this.data = data;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.spring.common.advice;
|
||||
|
||||
import org.springframework.core.MethodParameter;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
|
||||
|
||||
import com.spring.common.converter.CommHttpMessageConverter;
|
||||
import com.spring.common.error.ErrorResponse;
|
||||
import com.spring.common.error.ErrorRule;
|
||||
import com.spring.common.error.GlobalExceptionHandler;
|
||||
import com.spring.infra.security.error.SecurityExceptionHandler;
|
||||
|
||||
@RestControllerAdvice(
|
||||
basePackages = "com.spring.domain.*.api",
|
||||
basePackageClasses = { GlobalExceptionHandler.class, SecurityExceptionHandler.class }
|
||||
)
|
||||
public class ResponseWrapper implements ResponseBodyAdvice<Object> {
|
||||
|
||||
@Override
|
||||
public boolean supports(
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> converterType
|
||||
) {
|
||||
return CommHttpMessageConverter.class.isAssignableFrom(converterType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object beforeBodyWrite(
|
||||
@Nullable Object body,
|
||||
@NonNull MethodParameter returnType,
|
||||
@NonNull MediaType selectedContentType,
|
||||
@NonNull Class<? extends HttpMessageConverter<?>> selectedConverterType,
|
||||
@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response
|
||||
) {
|
||||
String path = request.getURI().getPath();
|
||||
|
||||
if (body instanceof ErrorResponse) {
|
||||
ErrorResponse errorData = ((ErrorResponse) body);
|
||||
ErrorRule errorRule = errorData.getErrorRule();
|
||||
response.setStatusCode(errorRule.getStatus());
|
||||
|
||||
return ApiResponse.builder()
|
||||
.statusCode(errorRule.getStatus().value())
|
||||
.path(path)
|
||||
.data(errorData.getErrors())
|
||||
.message(errorRule.getMessage())
|
||||
.build();
|
||||
}
|
||||
|
||||
return ApiResponse.builder()
|
||||
.statusCode(HttpStatus.OK.value())
|
||||
.path(path)
|
||||
.data(body)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.spring.common.config;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.spring.common.converter.CommHttpMessageConverter;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Configuration
|
||||
@RequiredArgsConstructor
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
@Override
|
||||
public void extendMessageConverters(@NonNull List<HttpMessageConverter<?>> converters) {
|
||||
converters.add(0, new CommHttpMessageConverter(objectMapper));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.spring.common.converter;
|
||||
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
|
||||
public class CommHttpMessageConverter extends MappingJackson2HttpMessageConverter {
|
||||
|
||||
public CommHttpMessageConverter(ObjectMapper objectMapper) {
|
||||
super(objectMapper);
|
||||
objectMapper.registerModule(new JavaTimeModule());
|
||||
setObjectMapper(objectMapper);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canWrite(@NonNull Class<?> clazz, @Nullable MediaType mediaType) {
|
||||
return canWrite(mediaType);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class BizBaseException extends RuntimeException {
|
||||
|
||||
private final ExceptionRule exceptionRule;
|
||||
|
||||
public BizBaseException() {
|
||||
super(ExceptionRule.BAD_REQUEST.getMessage());
|
||||
this.exceptionRule = ExceptionRule.BAD_REQUEST;
|
||||
}
|
||||
|
||||
public BizBaseException(ExceptionRule exceptionRule) {
|
||||
super(exceptionRule.getMessage());
|
||||
this.exceptionRule = exceptionRule;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.validation.FieldError;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class BizErrorResponse extends ErrorResponse {
|
||||
|
||||
public BizErrorResponse(ExceptionRule exceptionRule, List<RejectedValue> errors) {
|
||||
super(exceptionRule);
|
||||
this.errors = errors;
|
||||
}
|
||||
|
||||
public static BizErrorResponse valueOf(ExceptionRule exceptionRule) {
|
||||
return new BizErrorResponse(exceptionRule, null);
|
||||
}
|
||||
|
||||
public static BizErrorResponse fromFieldError(List<FieldError> fieldErrors) {
|
||||
List<RejectedValue> errors = fieldErrors.stream()
|
||||
.map(fieldError -> RejectedValue.of(fieldError.getField(), fieldError.getRejectedValue(), fieldError.getDefaultMessage()))
|
||||
.collect(Collectors.toList());
|
||||
return new BizErrorResponse(ExceptionRule.UNPROCESSABLE_ENTITY, errors);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
public class BizException extends RuntimeException {
|
||||
|
||||
public BizException(String message) {
|
||||
super(message);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public abstract class ErrorResponse {
|
||||
|
||||
protected final ErrorRule errorRule;
|
||||
protected List<RejectedValue> errors;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
public interface ErrorRule {
|
||||
HttpStatus getStatus();
|
||||
String getMessage();
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum ExceptionRule implements ErrorRule {
|
||||
|
||||
BAD_REQUEST(HttpStatus.BAD_REQUEST, "잘못된 요청입니다."),
|
||||
UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "인증이 필요합니다."),
|
||||
FORBIDDEN(HttpStatus.FORBIDDEN, "접근이 금지되었습니다."),
|
||||
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, "사용자를 찾을 수 없습니다.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
ExceptionRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public ExceptionRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import javax.servlet.RequestDispatcher;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import org.springframework.boot.web.servlet.error.ErrorController;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class GlobalErrorController implements ErrorController {
|
||||
|
||||
@ExceptionHandler(Throwable.class)
|
||||
@GetMapping("/error")
|
||||
public String handleError(HttpServletRequest request, Model model) {
|
||||
Object status = request.getAttribute(RequestDispatcher.ERROR_STATUS_CODE);
|
||||
String statusMsg = status.toString();
|
||||
HttpStatus httpStatus = HttpStatus.valueOf(Integer.valueOf(statusMsg));
|
||||
model.addAttribute("message", statusMsg + " " + httpStatus.getReasonPhrase());
|
||||
return "pages/error/error";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import org.springframework.web.bind.MethodArgumentNotValidException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(BizBaseException.class)
|
||||
public BizErrorResponse handleCustomException(BizBaseException e) {
|
||||
return BizErrorResponse.valueOf(e.getExceptionRule());
|
||||
}
|
||||
|
||||
@ExceptionHandler(MethodArgumentNotValidException.class)
|
||||
public BizErrorResponse methodArgumentNotValidException(MethodArgumentNotValidException e) {
|
||||
return BizErrorResponse.fromFieldError(e.getFieldErrors());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.spring.common.error;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public class RejectedValue {
|
||||
|
||||
private final String field;
|
||||
private final Object value;
|
||||
private final String reason;
|
||||
|
||||
public static RejectedValue of(String field, Object value, String reason) {
|
||||
return new RejectedValue(field, value, reason);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.spring.domain.user.api;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import com.spring.domain.user.dto.SignUpRequest;
|
||||
import com.spring.domain.user.service.SignUpService;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api/user")
|
||||
public class SignUpApi {
|
||||
|
||||
private final SignUpService signUpService;
|
||||
|
||||
@PostMapping("/sign-up")
|
||||
public void signUp(@RequestBody @Valid SignUpRequest request) {
|
||||
signUpService.signUp(request);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.spring.domain.user.dto;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
import com.spring.domain.user.entity.AppUser;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class SignUpRequest {
|
||||
|
||||
@NotBlank(message = "로그인ID는 필수값 입니다.")
|
||||
private String loginId;
|
||||
@NotBlank(message = "비밀번호는 필수값 입니다.")
|
||||
private String password;
|
||||
private String userName;
|
||||
|
||||
public void encodePassword(String password) {
|
||||
this.password = password;
|
||||
}
|
||||
|
||||
public AppUser toEntity() {
|
||||
return AppUser.builder()
|
||||
.loginId(loginId)
|
||||
.password(password)
|
||||
.userName(userName)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -12,12 +12,16 @@ import javax.persistence.Id;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "APP_USER")
|
||||
@Getter
|
||||
public final class AppUser {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class AppUser {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@@ -36,4 +40,12 @@ public final class AppUser {
|
||||
@OneToMany(mappedBy = "appUser", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
|
||||
private Set<AppUserRoleMap> appUserRoleMap;
|
||||
|
||||
@Builder
|
||||
public AppUser(String loginId, String password, String userName, Set<AppUserRoleMap> appUserRoleMap) {
|
||||
this.loginId = loginId;
|
||||
this.password = password;
|
||||
this.userName = userName;
|
||||
this.appUserRoleMap = appUserRoleMap;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -12,12 +12,15 @@ import javax.persistence.Id;
|
||||
import javax.persistence.OneToMany;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "APP_USER_ROLE")
|
||||
@Getter
|
||||
public final class AppUserRole {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class AppUserRole {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
|
||||
@@ -11,13 +11,16 @@ import javax.persistence.ManyToOne;
|
||||
import javax.persistence.MapsId;
|
||||
import javax.persistence.Table;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.Getter;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
@Entity
|
||||
@Table(name = "APP_USER_ROLE_MAP")
|
||||
@Getter
|
||||
public final class AppUserRoleMap {
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class AppUserRoleMap {
|
||||
|
||||
@EmbeddedId
|
||||
private AppUserRoleMapId id;
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.spring.domain.user.service;
|
||||
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
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 lombok.RequiredArgsConstructor;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class SignUpService {
|
||||
|
||||
private final AppUserRepository appUserRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
@Transactional
|
||||
public void signUp(SignUpRequest request) {
|
||||
request.encodePassword(passwordEncoder.encode(request.getPassword()));
|
||||
appUserRepository.save(request.toEntity());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package com.spring.domain.user.view;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/user")
|
||||
public class UserView {
|
||||
|
||||
@GetMapping("/sign-in")
|
||||
public String signin() {
|
||||
return "/views/user/signIn";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -5,16 +5,18 @@ import java.util.List;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.PathRequest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.ProviderManager;
|
||||
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
|
||||
import org.springframework.security.config.annotation.web.configurers.HttpBasicConfigurer;
|
||||
import org.springframework.security.config.annotation.web.configurers.RequestCacheConfigurer;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
@@ -41,10 +43,16 @@ import com.spring.infra.security.provider.UserAuthenticationProvider;
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
@EnableMethodSecurity
|
||||
@EnableMethodSecurity(securedEnabled = true, prePostEnabled = true)
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final String[] PERMITTED_URI = {"/favicon.ico", "/api/auth/**", "/user/sign-in", "/h2-console/**"};
|
||||
private static final String[] PERMITTED_URI = {
|
||||
"/",
|
||||
"/h2-console/**",
|
||||
"/favicon.ico",
|
||||
"/sign-up",
|
||||
"/api/user/sign-up"
|
||||
};
|
||||
|
||||
/**
|
||||
* Spring Security의 필터 체인을 구성합니다.
|
||||
@@ -56,6 +64,7 @@ public class SecurityConfig {
|
||||
* @return 구성된 SecurityFilterChain
|
||||
*/
|
||||
@Bean
|
||||
@Order(1)
|
||||
SecurityFilterChain securityFilterChain(
|
||||
HttpSecurity http,
|
||||
JwtTokenService tokenService,
|
||||
@@ -73,7 +82,7 @@ public class SecurityConfig {
|
||||
.anyRequest().authenticated()
|
||||
)
|
||||
.logout(logout -> logout
|
||||
.logoutSuccessUrl("/user/sign-in")
|
||||
.logoutSuccessUrl("/")
|
||||
.invalidateHttpSession(true))
|
||||
.sessionManagement(session -> session
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
|
||||
@@ -96,12 +105,18 @@ public class SecurityConfig {
|
||||
/**
|
||||
* 특정 요청에 대해 보안 검사를 무시하도록 설정합니다.
|
||||
*
|
||||
* @return WebSecurityCustomizer 객체
|
||||
* @return 구성된 SecurityFilterChain
|
||||
*/
|
||||
@Bean
|
||||
WebSecurityCustomizer ignoringCustomizer() {
|
||||
return web -> web.ignoring()
|
||||
.requestMatchers(PathRequest.toStaticResources().atCommonLocations());
|
||||
@Order(0)
|
||||
SecurityFilterChain resources(HttpSecurity http) throws Exception {
|
||||
return http.requestMatchers(matchers -> matchers
|
||||
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()))
|
||||
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll())
|
||||
.requestCache(RequestCacheConfigurer::disable)
|
||||
.securityContext(AbstractHttpConfigurer::disable)
|
||||
.sessionManagement(AbstractHttpConfigurer::disable)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -96,7 +96,7 @@ public final class UserPrincipal implements UserDetails {
|
||||
*/
|
||||
@Override
|
||||
public String getPassword() {
|
||||
return null;
|
||||
return appUser.getPassword();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.spring.infra.security.error;
|
||||
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class SecurityAuthException extends AuthenticationException {
|
||||
|
||||
private final SecurityExceptionRule exceptionRule;
|
||||
|
||||
public SecurityAuthException(String msg) {
|
||||
super(msg);
|
||||
this.exceptionRule = null;
|
||||
}
|
||||
|
||||
public SecurityAuthException(SecurityExceptionRule exceptionRule) {
|
||||
super(exceptionRule.getMessage());
|
||||
this.exceptionRule = exceptionRule;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package com.spring.infra.security.error;
|
||||
|
||||
import com.spring.common.error.ErrorResponse;
|
||||
|
||||
public class SecurityErrorResponse extends ErrorResponse {
|
||||
|
||||
protected SecurityErrorResponse(SecurityExceptionRule exceptionRule) {
|
||||
super(exceptionRule);
|
||||
}
|
||||
|
||||
public static SecurityErrorResponse valueOf(SecurityExceptionRule securityExceptionRule) {
|
||||
return new SecurityErrorResponse(securityExceptionRule);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.spring.infra.security.error;
|
||||
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.security.SignatureException;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class SecurityExceptionHandler {
|
||||
|
||||
@ExceptionHandler(SecurityAuthException.class)
|
||||
public SecurityErrorResponse handleAuthenticationException(SecurityAuthException e) {
|
||||
return SecurityErrorResponse.valueOf(e.getExceptionRule());
|
||||
}
|
||||
|
||||
@ExceptionHandler(AccessDeniedException.class)
|
||||
public SecurityErrorResponse handleAccessDeniedException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_FORBIDDEN);
|
||||
}
|
||||
|
||||
@ExceptionHandler(AuthenticationException.class)
|
||||
public SecurityErrorResponse handleAuthenticationException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.USER_UNAUTHORIZED);
|
||||
}
|
||||
|
||||
@ExceptionHandler(SignatureException.class)
|
||||
public SecurityErrorResponse handleSignatureException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.SIGNATURE_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(MalformedJwtException.class)
|
||||
public SecurityErrorResponse handleMalformedJwtException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.MALFORMED_JWT_ERROR);
|
||||
}
|
||||
|
||||
@ExceptionHandler(ExpiredJwtException.class)
|
||||
public SecurityErrorResponse handleExpiredJwtException() {
|
||||
return SecurityErrorResponse.valueOf(SecurityExceptionRule.EXPIRED_JWT_ERROR);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.spring.infra.security.error;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
import com.spring.common.error.ErrorRule;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@Getter
|
||||
@RequiredArgsConstructor
|
||||
public enum SecurityExceptionRule implements ErrorRule {
|
||||
|
||||
UNSUPPORTED_MEDIA_ERROR(HttpStatus.UNSUPPORTED_MEDIA_TYPE, "지원되지 않는 유형 입니다."),
|
||||
USER_BAD_REQUEST(HttpStatus.BAD_REQUEST, "사용자 정보가 올바르지 않습니다."),
|
||||
USER_UNAUTHORIZED(HttpStatus.UNAUTHORIZED, "사용자 인증에 실패 하였습니다."),
|
||||
USER_FORBIDDEN(HttpStatus.FORBIDDEN, "사용자 권한이 없습니다."),
|
||||
JWT_TOKEN_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 잘못되었습니다."),
|
||||
SIGNATURE_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 유효하지 않습니다."),
|
||||
MALFORMED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "올바르지 않은 토큰입니다."),
|
||||
EXPIRED_JWT_ERROR(HttpStatus.UNAUTHORIZED, "토큰이 만료되었습니다. 다시 로그인해주세요.");
|
||||
|
||||
private final HttpStatus status;
|
||||
private String message;
|
||||
|
||||
SecurityExceptionRule(HttpStatus status, String message) {
|
||||
this.status = status;
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public SecurityExceptionRule message(final String message) {
|
||||
this.message = message;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -18,6 +18,8 @@ import org.springframework.util.StringUtils;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
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 {
|
||||
|
||||
@@ -37,11 +39,11 @@ public class AuthenticationProcessingFilter extends AbstractAuthenticationProces
|
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
|
||||
throws AuthenticationException, IOException, ServletException {
|
||||
if (!isValidRequestType(request)) {
|
||||
throw new IllegalStateException("request is not supported. check request method and content-type");
|
||||
throw new SecurityAuthException(SecurityExceptionRule.UNSUPPORTED_MEDIA_ERROR);
|
||||
}
|
||||
var signInRequest = objectMapper.readValue(request.getReader(), SignInRequest.class);
|
||||
if (!isValidRequest(signInRequest)) {
|
||||
throw new IllegalArgumentException("Ussername & Password are not empty!!");
|
||||
throw new SecurityAuthException(SecurityExceptionRule.USER_BAD_REQUEST);
|
||||
}
|
||||
var token = new UsernamePasswordAuthenticationToken(signInRequest.getUsername(), signInRequest.getPassword());
|
||||
return this.getAuthenticationManager().authenticate(token);
|
||||
|
||||
@@ -11,6 +11,8 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.context.SecurityContextHolder;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import com.spring.infra.security.jwt.JwtTokenRule;
|
||||
@@ -29,8 +31,10 @@ import lombok.RequiredArgsConstructor;
|
||||
@RequiredArgsConstructor
|
||||
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";
|
||||
|
||||
/**
|
||||
* 요청마다 실행되는 필터 메소드입니다.
|
||||
@@ -51,7 +55,7 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
) throws ServletException, IOException {
|
||||
|
||||
String requestURI = request.getRequestURI();
|
||||
if (permitAllUrls.stream().anyMatch(requestURI::startsWith)) {
|
||||
if (permitAllUrls.stream().anyMatch(url -> pathMatcher.match(url, requestURI))) {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
@@ -64,16 +68,27 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
}
|
||||
|
||||
String refreshToken = jwtTokenService.resolveTokenFromCookie(request, JwtTokenRule.REFRESH_PREFIX);
|
||||
if (jwtTokenService.validateRefreshToken(refreshToken)) {
|
||||
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
|
||||
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
|
||||
jwtTokenService.generateRefreshToken(response, authentication);
|
||||
setAuthenticationToContext(reissuedAccessToken);
|
||||
if (StringUtils.hasText(refreshToken)) {
|
||||
if (validateToken(refreshToken, request)) {
|
||||
try {
|
||||
Authentication authentication = jwtTokenService.getAuthentication(refreshToken);
|
||||
String reissuedAccessToken = jwtTokenService.generateAccessToken(response, authentication);
|
||||
jwtTokenService.generateRefreshToken(response, authentication);
|
||||
setAuthenticationToContext(reissuedAccessToken);
|
||||
} catch (Exception e) {
|
||||
jwtTokenService.deleteCookie(response);
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||
}
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
jwtTokenService.deleteCookie(response);
|
||||
filterChain.doFilter(request, response);
|
||||
} else {
|
||||
filterChain.doFilter(request, response);
|
||||
return;
|
||||
}
|
||||
|
||||
jwtTokenService.deleteCookie(response);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -81,9 +96,19 @@ public final class JwtAuthenticationFilter extends OncePerRequestFilter {
|
||||
*
|
||||
* @param token 토큰
|
||||
*/
|
||||
private void setAuthenticationToContext(String token) {
|
||||
Authentication authentication = jwtTokenService.getAuthentication(token);
|
||||
private void setAuthenticationToContext(final String token) {
|
||||
Authentication authentication = jwtTokenService.getJwtAuthentication(token);
|
||||
SecurityContextHolder.getContext().setAuthentication(authentication);
|
||||
}
|
||||
|
||||
private boolean validateToken(final String token, HttpServletRequest request) {
|
||||
try {
|
||||
jwtTokenService.validateToken(token);
|
||||
return true;
|
||||
} catch (Exception e) {
|
||||
request.setAttribute(EXCEPTION_ATTRIBUTE, e);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,9 +6,11 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.web.access.AccessDeniedHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
|
||||
/**
|
||||
* JWT 인증에서 접근 거부 상황을 처리하는 핸들러 클래스입니다.
|
||||
@@ -22,6 +24,12 @@ import org.springframework.stereotype.Component;
|
||||
@Component
|
||||
public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
|
||||
private final HandlerExceptionResolver resolver;
|
||||
|
||||
public SecurityAccessDeniedHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
/**
|
||||
* 접근 거부 상황을 처리합니다.
|
||||
*
|
||||
@@ -37,7 +45,7 @@ public class SecurityAccessDeniedHandler implements AccessDeniedHandler {
|
||||
@Override
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response,
|
||||
AccessDeniedException accessDeniedException) throws IOException, ServletException {
|
||||
response.sendError(HttpServletResponse.SC_FORBIDDEN);
|
||||
resolver.resolveException(request, response, null, accessDeniedException);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,38 +6,36 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.AuthenticationEntryPoint;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
|
||||
/**
|
||||
* JWT 인증에서 인증되지 않은 접근을 처리하는 진입점 클래스입니다.
|
||||
*
|
||||
* <p>이 클래스는 사용자가 인증되지 않은 상태에서 보호된 리소스에 접근을 시도할 때 호출됩니다.
|
||||
* 이런 경우 SC_UNAUTHORIZED (401) 응답을 반환합니다.</p>
|
||||
*
|
||||
* @author mindol
|
||||
* @version 1.0
|
||||
*/
|
||||
@Component
|
||||
public class SecurityAuthenticationEntryPoint implements AuthenticationEntryPoint {
|
||||
|
||||
/**
|
||||
* 인증되지 않은 접근을 처리합니다.
|
||||
*
|
||||
* <p>인증되지 않은 사용자가 보호된 리소스에 접근을 시도할 때 호출됩니다.
|
||||
* 이 메소드는 SC_UNAUTHORIZED (401) 상태 코드를 응답으로 전송합니다.</p>
|
||||
*
|
||||
* @param request 현재 HTTP 요청
|
||||
* @param response 현재 HTTP 응답
|
||||
* @param authException 발생한 인증 예외
|
||||
* @throws IOException 입출력 예외 발생 시
|
||||
* @throws ServletException 서블릿 예외 발생 시
|
||||
*/
|
||||
private final HandlerExceptionResolver resolver;
|
||||
private static final String EXCEPTION_ATTRIBUTE = "exception";
|
||||
|
||||
public SecurityAuthenticationEntryPoint(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void commence(HttpServletRequest request, HttpServletResponse response,
|
||||
AuthenticationException authException) throws IOException, ServletException {
|
||||
response.sendError(HttpServletResponse.SC_UNAUTHORIZED);
|
||||
if (request.getAttribute(EXCEPTION_ATTRIBUTE) != null) {
|
||||
resolver.resolveException(request, response, null, (Exception) request.getAttribute(EXCEPTION_ATTRIBUTE));
|
||||
return;
|
||||
}
|
||||
response.sendError(HttpServletResponse.SC_NOT_FOUND);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -6,20 +6,28 @@ import javax.servlet.ServletException;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletResponse;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.servlet.HandlerExceptionResolver;
|
||||
|
||||
@Component
|
||||
public class SigninFailureHandler implements AuthenticationFailureHandler {
|
||||
|
||||
private final HandlerExceptionResolver resolver;
|
||||
|
||||
public SigninFailureHandler(@Qualifier("handlerExceptionResolver") HandlerExceptionResolver resolver) {
|
||||
this.resolver = resolver;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAuthenticationFailure(
|
||||
HttpServletRequest request,
|
||||
HttpServletResponse response,
|
||||
AuthenticationException exception
|
||||
) throws IOException, ServletException {
|
||||
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); //401 인증 실패
|
||||
resolver.resolveException(request, response, null, exception);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ public class SigninSuccessHandler implements AuthenticationSuccessHandler {
|
||||
redirectStrategy.sendRedirect(request, response, savedRequest.getRedirectUrl());
|
||||
} else { // 로그인 버튼 눌러서 로그인한 경우 기존에 있던 페이지로 리다이렉트
|
||||
String prevPage = String.valueOf(request.getSession().getAttribute("prevPage"));
|
||||
redirectStrategy.sendRedirect(request, response, prevPage);
|
||||
redirectStrategy.sendRedirect(request, response, "null".equals(prevPage) ? "/main" : prevPage);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
package com.spring.infra.security.jwt;
|
||||
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.SignatureAlgorithm;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
@@ -30,6 +29,7 @@ import lombok.RequiredArgsConstructor;
|
||||
public class JwtTokenGenerator {
|
||||
|
||||
private final JwtProperties jwtProperties;
|
||||
private final JwtTokenUtil jwtTokenUtil;
|
||||
|
||||
/**
|
||||
* 액세스 토큰을 생성합니다.
|
||||
@@ -42,8 +42,9 @@ public class JwtTokenGenerator {
|
||||
.setHeader(createHeader())
|
||||
.setClaims(createClaims(authentication))
|
||||
.setSubject(authentication.getName())
|
||||
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.HOURS)))
|
||||
.signWith(new SecretKeySpec(jwtProperties.getAccessToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName()))
|
||||
.setIssuedAt(Timestamp.valueOf(LocalDateTime.now()))
|
||||
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getAccessToken().getExpiration(), ChronoUnit.MINUTES)))
|
||||
.signWith(jwtTokenUtil.getSigningKey(jwtProperties.getAccessToken().getSecret()))
|
||||
.compact();
|
||||
}
|
||||
|
||||
@@ -57,8 +58,8 @@ public class JwtTokenGenerator {
|
||||
return Jwts.builder()
|
||||
.setHeader(createHeader())
|
||||
.setSubject(authentication.getName())
|
||||
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.HOURS)))
|
||||
.signWith(new SecretKeySpec(jwtProperties.getRefreshToken().getSecret().getBytes(), SignatureAlgorithm.HS512.getJcaName()))
|
||||
.setExpiration(Date.from(Instant.now().plus(jwtProperties.getRefreshToken().getExpiration(), ChronoUnit.MINUTES)))
|
||||
.signWith(jwtTokenUtil.getSigningKey(jwtProperties.getRefreshToken().getSecret()))
|
||||
.compact();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,12 +28,12 @@ public enum JwtTokenRule {
|
||||
/**
|
||||
* 액세스 토큰의 접두사입니다.
|
||||
*/
|
||||
ACCESS_PREFIX("access"),
|
||||
ACCESS_PREFIX("accessToken"),
|
||||
|
||||
/**
|
||||
* 리프레시 토큰의 접두사입니다.
|
||||
*/
|
||||
REFRESH_PREFIX("refresh"),
|
||||
REFRESH_PREFIX("refreshToken"),
|
||||
|
||||
/**
|
||||
* Bearer 인증 스키마의 접두사입니다.
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package com.spring.infra.security.jwt;
|
||||
|
||||
import java.security.Key;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.servlet.http.Cookie;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
@@ -9,11 +11,14 @@ import javax.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.ResponseCookie;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.infra.security.service.UserPrincipalService;
|
||||
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
|
||||
/**
|
||||
@@ -59,7 +64,7 @@ public class JwtTokenService {
|
||||
*/
|
||||
public String generateAccessToken(HttpServletResponse response, Authentication authentication) {
|
||||
String accessToken = jwtTokenGenerator.generateAccessToken(authentication);
|
||||
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration / 1000);
|
||||
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.ACCESS_PREFIX.getValue(), accessToken, accessExpiration);
|
||||
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
|
||||
return accessToken;
|
||||
}
|
||||
@@ -73,7 +78,7 @@ public class JwtTokenService {
|
||||
*/
|
||||
public String generateRefreshToken(HttpServletResponse response, Authentication authentication) {
|
||||
String refreshToken = jwtTokenGenerator.generateRefreshToken(authentication);
|
||||
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration / 1000);
|
||||
ResponseCookie cookie = setTokenToCookie(JwtTokenRule.REFRESH_PREFIX.getValue(), refreshToken, refreshExpiration);
|
||||
response.addHeader(JwtTokenRule.JWT_ISSUE_HEADER.getValue(), cookie.toString());
|
||||
return refreshToken;
|
||||
}
|
||||
@@ -91,11 +96,20 @@ public class JwtTokenService {
|
||||
.path("/")
|
||||
.maxAge(maxAgeSeconds)
|
||||
.httpOnly(true)
|
||||
.sameSite("None")
|
||||
.secure(true)
|
||||
.sameSite("Lax")
|
||||
.secure(false)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 유효성을 검증합니다.
|
||||
*
|
||||
* @param token 검증할 토큰
|
||||
*/
|
||||
public void validateToken(String token) {
|
||||
jwtTokenUtil.tokenStatus(token, accessSecretKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 액세스 토큰의 유효성을 검증합니다.
|
||||
*
|
||||
@@ -133,13 +147,32 @@ public class JwtTokenService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰으로부터 인증 정보를 생성합니다.
|
||||
* JWT 토큰으로부터 인증 정보를 생성합니다.
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @return 생성된 Authentication 객체
|
||||
*/
|
||||
public Authentication getJwtAuthentication(String token) {
|
||||
Claims claims = getUserClaims(token);
|
||||
String sub = claims.getSubject();
|
||||
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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰으로부터 인증 정보를 가져와서 DB정보를 조회한다.
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @return 생성된 Authentication 객체
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Authentication getAuthentication(String token) {
|
||||
UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token, accessSecretKey));
|
||||
UserDetails principal = userPrincipalService.loadUserByUsername(getUserPk(token));
|
||||
return new UsernamePasswordAuthenticationToken(principal, "", principal.getAuthorities());
|
||||
}
|
||||
|
||||
@@ -147,12 +180,25 @@ public class JwtTokenService {
|
||||
* 토큰에서 사용자 식별자를 추출합니다.
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @param secretKey 비밀 키
|
||||
* @return 추출된 사용자 식별자
|
||||
*/
|
||||
private String getUserPk(String token, Key secretKey) {
|
||||
private Claims getUserClaims(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(secretKey)
|
||||
.setSigningKey(accessSecretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody();
|
||||
}
|
||||
|
||||
/**
|
||||
* 토큰에서 사용자 식별자를 추출합니다.
|
||||
*
|
||||
* @param token JWT 토큰
|
||||
* @return 추출된 사용자 식별자
|
||||
*/
|
||||
private String getUserPk(String token) {
|
||||
return Jwts.parserBuilder()
|
||||
.setSigningKey(accessSecretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token)
|
||||
.getBody()
|
||||
|
||||
@@ -9,10 +9,14 @@ import javax.servlet.http.Cookie;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
import io.jsonwebtoken.ExpiredJwtException;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.MalformedJwtException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import io.jsonwebtoken.security.SignatureException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
@@ -42,14 +46,35 @@ public class JwtTokenUtil {
|
||||
.parseClaimsJws(token);
|
||||
return JwtTokenStatus.AUTHENTICATED;
|
||||
} catch (ExpiredJwtException | IllegalArgumentException e) {
|
||||
log.error("만료된 JWT 토큰입니다.");
|
||||
return JwtTokenStatus.EXPIRED;
|
||||
} catch (JwtException e) {
|
||||
log.error("JWT 토큰이 잘못되었습니다.");
|
||||
return JwtTokenStatus.INVALID;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* JWT 토큰의 상태를 확인하고 예외를 던집니다.
|
||||
*
|
||||
* @param token 검증할 JWT 토큰
|
||||
* @param secretKey 토큰 검증에 사용할 비밀 키
|
||||
*/
|
||||
public void tokenStatus(String token, Key secretKey) {
|
||||
try {
|
||||
Jwts.parserBuilder()
|
||||
.setSigningKey(secretKey)
|
||||
.build()
|
||||
.parseClaimsJws(token);
|
||||
} catch (ExpiredJwtException | IllegalArgumentException e) {
|
||||
throw new ExpiredJwtException(null, null, SecurityExceptionRule.EXPIRED_JWT_ERROR.getMessage());
|
||||
} catch (SignatureException e) {
|
||||
throw new SignatureException(SecurityExceptionRule.SIGNATURE_ERROR.getMessage());
|
||||
} catch (MalformedJwtException e) {
|
||||
throw new MalformedJwtException(SecurityExceptionRule.MALFORMED_JWT_ERROR.getMessage());
|
||||
} catch (JwtException e) {
|
||||
throw new JwtException(SecurityExceptionRule.JWT_TOKEN_ERROR.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 쿠키에서 특정 접두사를 가진 토큰을 추출합니다.
|
||||
*
|
||||
|
||||
@@ -8,6 +8,7 @@ import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import com.spring.infra.security.service.UserPrincipalService;
|
||||
|
||||
@@ -20,6 +21,7 @@ public class UserAuthenticationProvider implements AuthenticationProvider {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final UserPrincipalService userPrincipalService;
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String loginId = authentication.getName();
|
||||
|
||||
@@ -9,6 +9,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import com.spring.domain.user.entity.AppUser;
|
||||
import com.spring.domain.user.repository.AppUserRepository;
|
||||
import com.spring.infra.security.domain.UserPrincipal;
|
||||
import com.spring.infra.security.error.SecurityExceptionRule;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
@@ -22,7 +23,7 @@ public class UserPrincipalService implements UserDetailsService {
|
||||
@Override
|
||||
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||
AppUser user = appUserRepository.findByLoginId(username)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("NOT FOUND USER"));
|
||||
.orElseThrow(() -> new UsernameNotFoundException(SecurityExceptionRule.USER_UNAUTHORIZED.getMessage()));
|
||||
return UserPrincipal.valueOf(user);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.spring.web.controller;
|
||||
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/main")
|
||||
public class MainController {
|
||||
|
||||
@GetMapping
|
||||
@PreAuthorize("hasRole('ROLE_ADMIN')")
|
||||
public String main() {
|
||||
return "pages/main/main";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.spring.web.controller;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
|
||||
@Controller
|
||||
@RequestMapping("/")
|
||||
public class SignController {
|
||||
|
||||
@GetMapping
|
||||
public String signIn() {
|
||||
return "pages/sign/sign-in";
|
||||
}
|
||||
|
||||
@GetMapping("/sign-up")
|
||||
public String signUp() {
|
||||
return "pages/sign/sign-up";
|
||||
}
|
||||
|
||||
}
|
||||
@@ -33,6 +33,7 @@ spring:
|
||||
# - classpath:quartz-schema.sql
|
||||
|
||||
jpa:
|
||||
open-in-view: false
|
||||
database-platform: org.hibernate.dialect.H2Dialect
|
||||
hibernate:
|
||||
ddl-auto: create
|
||||
@@ -77,9 +78,9 @@ spring:
|
||||
cache: false
|
||||
check-template-location: false
|
||||
enabled: true
|
||||
prefix: classpath:/templates
|
||||
prefix: classpath:/templates/
|
||||
suffix: .html
|
||||
view-names: /views/*
|
||||
view-names: pages/*
|
||||
|
||||
h2:
|
||||
console: # H2 DB를 웹에서 관리할 수 있는 기능
|
||||
@@ -90,8 +91,8 @@ spring:
|
||||
|
||||
jwt:
|
||||
access-token:
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290
|
||||
expiration: 900 # 15분
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||
expiration: 1
|
||||
refresh-token:
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290
|
||||
expiration: 604800 # 7일
|
||||
secret: bnhjdXMyLjAtcGxhdGZvcm0tcHJvamVjdC13aXRoLXNwcmluZy1ib290bnhjdF9zdHJvbmdfY29tcGxleF9zdHJvbmc=
|
||||
expiration: 10080
|
||||
@@ -1,11 +1,12 @@
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
background: linear-gradient(to right, #f0f0f5, #e9ecef);
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100vh;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
flex-direction: column; /* 세로 방향으로 정렬 */
|
||||
justify-content: center; /* 수직 중앙 정렬 */
|
||||
align-items: center; /* 수평 중앙 정렬 */
|
||||
height: 100vh; /* 전체 화면 높이 사용 */
|
||||
}
|
||||
|
||||
.container {
|
||||
@@ -15,6 +16,7 @@ body {
|
||||
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2);
|
||||
width: 350px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px; /* footer와의 간격 조정 */
|
||||
}
|
||||
|
||||
.icon {
|
||||
@@ -59,17 +61,6 @@ input::placeholder {
|
||||
color: #aaa; /* 플레이스홀더 색상 */
|
||||
}
|
||||
|
||||
.remember-me {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.remember-me label {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
@@ -84,4 +75,20 @@ button {
|
||||
|
||||
button:hover {
|
||||
background-color: #005bb5;
|
||||
}
|
||||
|
||||
.small-button {
|
||||
padding: 5px 10px; /* 버튼 패딩 조정 */
|
||||
font-size: 14px; /* 글자 크기 조정 */
|
||||
background-color: #007aff; /* 배경색 */
|
||||
color: white; /* 글자색 */
|
||||
border: none; /* 테두리 없음 */
|
||||
border-radius: 5px; /* 모서리 둥글게 */
|
||||
cursor: pointer; /* 커서 포인터 */
|
||||
transition: background-color 0.3s; /* 배경색 전환 효과 */
|
||||
margin-top: 10px; /* 로그인 버튼과의 간격 추가 */
|
||||
}
|
||||
|
||||
.small-button:hover {
|
||||
background-color: #005bb5; /* 호버 시 배경색 변경 */
|
||||
}
|
||||
13
batch-quartz/src/main/resources/static/js/apis/user-api.js
Normal file
13
batch-quartz/src/main/resources/static/js/apis/user-api.js
Normal file
@@ -0,0 +1,13 @@
|
||||
import apiClient from '../common/axios-instance.js';
|
||||
|
||||
export const signIn = async (username, password) => {
|
||||
try {
|
||||
await apiClient.post('/sign-in', {username, password});
|
||||
} catch (error) {
|
||||
console.error('Sign-in error:', error);
|
||||
}
|
||||
};
|
||||
|
||||
export const signUp = async (loginId, password, userName) => {
|
||||
await apiClient.post('/api/user/sign-up', {loginId, password, userName});
|
||||
};
|
||||
@@ -0,0 +1,87 @@
|
||||
// Axios apiClient 생성
|
||||
const apiClient = axios.create({
|
||||
baseURL: 'http://localhost:8081', // 기본 URL 설정
|
||||
timeout: 100000000, // 요청 타임아웃 설정 (10초)
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
});
|
||||
|
||||
// 요청 인터셉터 추가
|
||||
apiClient.interceptors.request.use(
|
||||
(config) => {
|
||||
const token = getAccessToken();
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
(error) => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 응답 인터셉터 추가
|
||||
apiClient.interceptors.response.use(
|
||||
(response) => {
|
||||
return response;
|
||||
},
|
||||
async (error) => {
|
||||
const originalRequest = error.config;
|
||||
|
||||
// 401 Unauthorized 에러 처리
|
||||
if (error.response && error.response.status === 401 && !originalRequest._retry) {
|
||||
console.log("333333333333");
|
||||
originalRequest._retry = true;
|
||||
|
||||
const refreshToken = getRefreshToken();
|
||||
console.log("refreshToken===="+refreshToken);
|
||||
if (refreshToken) {
|
||||
try {
|
||||
const response = await axios.post(baseURL+'/auth/refresh', {
|
||||
refreshToken: refreshToken,
|
||||
});
|
||||
|
||||
const { accessToken, newRefreshToken } = response.data;
|
||||
|
||||
// 새로 받은 토큰을 쿠키에 저장
|
||||
setAccessToken(accessToken);
|
||||
if (newRefreshToken) {
|
||||
setRefreshToken(newRefreshToken);
|
||||
}
|
||||
|
||||
// 갱신된 토큰으로 원래 요청 재시도
|
||||
originalRequest.headers['Authorization'] = `Bearer ${accessToken}`;
|
||||
return apiClient(originalRequest);
|
||||
} catch (refreshError) {
|
||||
// refresh token이 만료된 경우 처리 (로그아웃 등)
|
||||
console.error('Token refresh failed:', refreshError);
|
||||
// 로그아웃 처리 또는 로그인 페이지로 리다이렉트
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 쿠키에서 accessToken 가져오기
|
||||
function getAccessToken() {
|
||||
return Cookies.get('accessToken');
|
||||
}
|
||||
|
||||
// 쿠키에서 refreshToken 가져오기
|
||||
function getRefreshToken() {
|
||||
return Cookies.get('refreshToken');
|
||||
}
|
||||
|
||||
// 쿠키에 accessToken 저장
|
||||
function setAccessToken(token) {
|
||||
Cookies.set('accessToken', token, { secure: true, sameSite: 'strict' });
|
||||
}
|
||||
|
||||
// 쿠키에 refreshToken 저장
|
||||
function setRefreshToken(token) {
|
||||
Cookies.set('refreshToken', token, { secure: true, sameSite: 'strict' });
|
||||
}
|
||||
|
||||
export default apiClient;
|
||||
@@ -1,43 +0,0 @@
|
||||
// Axios 인스턴스 생성
|
||||
const axiosInstance = axios.create({
|
||||
baseURL: 'http://localhost:8081', // 기본 URL 설정
|
||||
timeout: 10000, // 요청 타임아웃 설정 (10초)
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
// 필요한 경우 추가 헤더 설정
|
||||
}
|
||||
});
|
||||
|
||||
// 요청 인터셉터 (필요한 경우)
|
||||
axiosInstance.interceptors.request.use(
|
||||
config => {
|
||||
// 요청 전에 수행할 작업 (예: 토큰 추가)
|
||||
const token = localStorage.getItem('token'); // 예시: 로컬 스토리지에서 토큰 가져오기
|
||||
if (token) {
|
||||
config.headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
return config;
|
||||
},
|
||||
error => {
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
// 응답 인터셉터 (필요한 경우)
|
||||
axiosInstance.interceptors.response.use(
|
||||
response => {
|
||||
return response;
|
||||
},
|
||||
error => {
|
||||
console.log(error.response);
|
||||
// 오류 처리 (예: 401 Unauthorized 처리)
|
||||
if (error.response && error.response.status === 401) {
|
||||
// 로그아웃 처리 또는 리다이렉트
|
||||
console.log("111111111111111");
|
||||
console.log(error.response);
|
||||
}
|
||||
return Promise.reject(error);
|
||||
}
|
||||
);
|
||||
|
||||
export default axiosInstance;
|
||||
2
batch-quartz/src/main/resources/static/js/lib/cookie/js.cookie.min.js
vendored
Normal file
2
batch-quartz/src/main/resources/static/js/lib/cookie/js.cookie.min.js
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
/*! js-cookie v3.0.5 | MIT */
|
||||
!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t):(e="undefined"!=typeof globalThis?globalThis:e||self,function(){var n=e.Cookies,o=e.Cookies=t();o.noConflict=function(){return e.Cookies=n,o}}())}(this,(function(){"use strict";function e(e){for(var t=1;t<arguments.length;t++){var n=arguments[t];for(var o in n)e[o]=n[o]}return e}var t=function t(n,o){function r(t,r,i){if("undefined"!=typeof document){"number"==typeof(i=e({},o,i)).expires&&(i.expires=new Date(Date.now()+864e5*i.expires)),i.expires&&(i.expires=i.expires.toUTCString()),t=encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g,decodeURIComponent).replace(/[()]/g,escape);var c="";for(var u in i)i[u]&&(c+="; "+u,!0!==i[u]&&(c+="="+i[u].split(";")[0]));return document.cookie=t+"="+n.write(r,t)+c}}return Object.create({set:r,get:function(e){if("undefined"!=typeof document&&(!arguments.length||e)){for(var t=document.cookie?document.cookie.split("; "):[],o={},r=0;r<t.length;r++){var i=t[r].split("="),c=i.slice(1).join("=");try{var u=decodeURIComponent(i[0]);if(o[u]=n.read(c,u),e===u)break}catch(e){}}return e?o[e]:o}},remove:function(t,n){r(t,"",e({},n,{expires:-1}))},withAttributes:function(n){return t(this.converter,e({},this.attributes,n))},withConverter:function(n){return t(e({},this.converter,n),this.attributes)}},{attributes:{value:Object.freeze(o)},converter:{value:Object.freeze(n)}})}({read:function(e){return'"'===e[0]&&(e=e.slice(1,-1)),e.replace(/(%[\dA-F]{2})+/gi,decodeURIComponent)},write:function(e){return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g,decodeURIComponent)}},{path:"/"});return t}));
|
||||
@@ -0,0 +1,8 @@
|
||||
import {signIn} from '../../apis/user-api.js';
|
||||
|
||||
document.getElementById('signinForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
signIn(username, password);
|
||||
});
|
||||
@@ -0,0 +1,9 @@
|
||||
import {signUp} from '../../apis/user-api.js';
|
||||
|
||||
document.getElementById('signupForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault();
|
||||
const loginId = document.getElementById('loginId').value;
|
||||
const password = document.getElementById('password').value;
|
||||
const userName = document.getElementById('userName').value;
|
||||
signUp(loginId, password, userName);
|
||||
});
|
||||
@@ -1,20 +0,0 @@
|
||||
import axiosInstance from '../common/axiosInstance.js';
|
||||
|
||||
const login = async (username, password) => {
|
||||
try {
|
||||
const response = await axiosInstance.post('/sign-in', {
|
||||
username,
|
||||
password
|
||||
});
|
||||
console.log('로그인 성공:', response.data);
|
||||
} catch (error) {
|
||||
console.error('로그인 실패:', error);
|
||||
}
|
||||
};
|
||||
|
||||
document.getElementById('signinForm').addEventListener('submit', function(event) {
|
||||
event.preventDefault(); // 기본 폼 제출 방지
|
||||
const username = document.getElementById('username').value;
|
||||
const password = document.getElementById('password').value;
|
||||
login(username, password);
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:fragment="config" lang="ko" xml:lang="ko">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}">
|
||||
<script th:src="@{/js/lib/axios/axios.min.js}"></script>
|
||||
<script th:src="@{/js/lib/cookie/js.cookie.min.js}"></script>
|
||||
</html>
|
||||
@@ -0,0 +1,6 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
|
||||
<footer th:fragment="footer">
|
||||
<p>Footer</p>
|
||||
</footer>
|
||||
</html>
|
||||
@@ -0,0 +1,4 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:fragment="header" lang="ko" xml:lang="ko">
|
||||
<h1>사이트 로고</h1>
|
||||
</html>
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" th:fragment="sidebar" lang="ko" xml:lang="ko">
|
||||
<div class="sidebar">
|
||||
<nav>
|
||||
<ul>
|
||||
<li><a href="/home">홈</a></li>
|
||||
<li><a href="/about">소개</a></li>
|
||||
<li><a href="/services">서비스</a></li>
|
||||
<li><a href="/contact">연락처</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</html>
|
||||
@@ -0,0 +1,16 @@
|
||||
<!DOCTYPE html>
|
||||
<html lagn="ko" xml:lang="ko"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head th:replace="fragments/config :: config"/>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div th:replace="fragments/header::header"></div>
|
||||
<div class="content">
|
||||
<div th:replace="fragments/left::sidebar"></div>
|
||||
<div class="main" layout:fragment="content"></div>
|
||||
</div>
|
||||
<footer th:replace="fragments/footer::footer"></footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
<!DOCTYPE html>
|
||||
<html lagn="ko" xml:lang="ko"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head th:replace="fragments/config :: config"/>
|
||||
<body>
|
||||
<section layout:fragment="content"></section>
|
||||
<footer th:replace="fragments/footer::footer"></footer>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Error page</title>
|
||||
<link rel="stylesheet" href="/css/error.css">
|
||||
</head>
|
||||
<body>
|
||||
<h1>Error Page</h1>
|
||||
<span th:text="${message}"></span>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,11 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/layout}"
|
||||
layout:fragment="content" lang="ko" xml:lang="ko">
|
||||
|
||||
<div>
|
||||
본문 영역입니다.
|
||||
</div>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,35 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
|
||||
layout:decorate="~{layouts/signin-layout}" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<title>로그인 페이지</title>
|
||||
</head>
|
||||
<body>
|
||||
<section layout:fragment="content">
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<img src="/images/user.png" alt="User Icon">
|
||||
</div>
|
||||
<h1>로그인</h1>
|
||||
<form id="signinForm" method="post">
|
||||
<div class="input-group">
|
||||
<div class="input-icon">
|
||||
<img src="/images/user-id.png" alt="User Icon">
|
||||
<input type="text" id="username" name="username" placeholder="아이디">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-icon">
|
||||
<img src="/images/user-lock.png" alt="Password Icon">
|
||||
<input type="password" id="password" name="password" placeholder="비밀번호">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
<button id="signup" th:onclick="|location.href='@{/sign-up}'|" class="small-button">회원가입</button>
|
||||
</div>
|
||||
<script type="module" th:src="@{/js/pages/sign/sign-in.js}" defer></script>
|
||||
</section>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,28 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>회원가입 페이지</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<script src="/js/lib/axios/axios.min.js"></script>
|
||||
<script src="/js/lib/cookie/js.cookie.min.js"></script>
|
||||
<script type="module" src="/js/pages/sign/sign-up.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>회원가입</h1>
|
||||
<form id="signupForm" method="post">
|
||||
<div class="input-group">
|
||||
<input type="text" id="loginId" name="loginId" placeholder="아이디">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="password" id="password" name="password" placeholder="비밀번호">
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<input type="text" id="userName" name="userName" placeholder="이름">
|
||||
</div>
|
||||
<button type="submit">회원가입</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -1,33 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org" lang="ko" xml:lang="ko">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>로그인 페이지</title>
|
||||
<link rel="stylesheet" href="/css/style.css">
|
||||
<script src="/js/lib/axios/axios.min.js"></script>
|
||||
<script type="module" src="/js/user/signIn.js" defer></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="icon">
|
||||
<img src="/images/user.png" alt="User Icon">
|
||||
</div>
|
||||
<h1>로그인</h1>
|
||||
<form id="signinForm" method="post">
|
||||
<div class="input-group">
|
||||
<div class="input-icon">
|
||||
<img src="/images/user-id.png" alt="User Icon">
|
||||
<input type="text" id="username" name="username" placeholder="아이디">
|
||||
</div>
|
||||
</div>
|
||||
<div class="input-group">
|
||||
<div class="input-icon">
|
||||
<img src="/images/user-lock.png" alt="Password Icon">
|
||||
<input type="password" id="password" name="password" placeholder="비밀번호">
|
||||
</div>
|
||||
</div>
|
||||
<button type="submit">로그인</button>
|
||||
</form>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user