지대한
8 months ago
58 changed files with 1004 additions and 58 deletions
@ -0,0 +1,53 @@
|
||||
package kr.co.palnet.kac.app.core.security; |
||||
|
||||
import kr.co.palnet.kac.config.security.SecurityConfig; |
||||
import kr.co.palnet.kac.config.security.exception.BaseAccessDeniedHandler; |
||||
import kr.co.palnet.kac.config.security.exception.BaseAuthenticationEntryPoint; |
||||
import kr.co.palnet.kac.config.security.service.BaseUserDetailsService; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
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.web.SecurityFilterChain; |
||||
|
||||
|
||||
@Slf4j |
||||
@EnableWebSecurity |
||||
@Configuration |
||||
public class AppSecurityConfig extends SecurityConfig { |
||||
|
||||
// 시큐리티 적용 안하는 URL 목록
|
||||
private final String[] IGNORE_URL = {}; |
||||
|
||||
private final String[] USER_URL = {}; |
||||
|
||||
|
||||
|
||||
public AppSecurityConfig(BaseUserDetailsService baseUserDetailsService, BaseAuthenticationEntryPoint baseAuthenticationEntryPoint, BaseAccessDeniedHandler baseAccessDeniedHandler) { |
||||
super(baseUserDetailsService, baseAuthenticationEntryPoint, baseAccessDeniedHandler); |
||||
} |
||||
|
||||
@Override |
||||
@Bean |
||||
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
||||
this.setDefaultHttpSecurity(http); |
||||
http |
||||
.authorizeHttpRequests(authz -> |
||||
authz |
||||
.requestMatchers(USER_URL).hasRole("USER") |
||||
.anyRequest().authenticated() |
||||
) |
||||
; |
||||
|
||||
return http.build(); |
||||
} |
||||
|
||||
@Bean |
||||
public WebSecurityCustomizer appWebSecurityCustomizer() { |
||||
return web -> web.ignoring() |
||||
.requestMatchers(IGNORE_URL); |
||||
} |
||||
|
||||
} |
@ -1,7 +0,0 @@
|
||||
package kr.co.palnet.kac.data.pty.repository; |
||||
|
||||
import kr.co.palnet.kac.data.pty.model.PtyCstmrBas; |
||||
import org.springframework.data.jpa.repository.JpaRepository; |
||||
|
||||
public interface PtyCstmrBasRepository extends JpaRepository<PtyCstmrBas, Integer> { |
||||
} |
@ -0,0 +1,11 @@
|
||||
|
||||
|
||||
dependencies { |
||||
implementation "$boot:spring-boot-starter-web" |
||||
implementation "$boot:spring-boot-starter-security" |
||||
implementation "com.auth0:java-jwt:4.4.0" |
||||
|
||||
implementation project(":common-util") |
||||
implementation project(":data-pty") |
||||
} |
||||
|
@ -0,0 +1,90 @@
|
||||
package kr.co.palnet.kac.config.security; |
||||
|
||||
import kr.co.palnet.kac.config.security.exception.BaseAccessDeniedHandler; |
||||
import kr.co.palnet.kac.config.security.exception.BaseAuthenticationEntryPoint; |
||||
import kr.co.palnet.kac.config.security.filter.JwtCheckFilter; |
||||
import kr.co.palnet.kac.config.security.filter.JwtLoginFilter; |
||||
import kr.co.palnet.kac.config.security.service.BaseUserDetailsService; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.ProviderManager; |
||||
import org.springframework.security.authentication.dao.DaoAuthenticationProvider; |
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity; |
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; |
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; |
||||
import org.springframework.security.config.http.SessionCreationPolicy; |
||||
import org.springframework.security.crypto.password.NoOpPasswordEncoder; |
||||
import org.springframework.security.crypto.password.PasswordEncoder; |
||||
import org.springframework.security.web.SecurityFilterChain; |
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; |
||||
|
||||
|
||||
@RequiredArgsConstructor |
||||
public abstract class SecurityConfig { |
||||
|
||||
private final BaseUserDetailsService baseUserDetailsService; |
||||
private final BaseAuthenticationEntryPoint baseAuthenticationEntryPoint; |
||||
private final BaseAccessDeniedHandler baseAccessDeniedHandler; |
||||
|
||||
private final String[] SWAGGER_URI = { |
||||
"/swagger-ui/index.html", |
||||
"/v3/api-docs", |
||||
"/swagger-resources/**", |
||||
"/webjars/**", |
||||
"/swagger-ui/**" |
||||
}; |
||||
|
||||
|
||||
@Bean |
||||
PasswordEncoder passwordEncoder() { |
||||
// return new BCryptPasswordEncoder();
|
||||
// TODO 테스트 후 BCryptPasswordEncoder 로 변경
|
||||
return NoOpPasswordEncoder.getInstance(); |
||||
} |
||||
|
||||
@Bean |
||||
public AuthenticationManager authenticationManager() { |
||||
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider(); |
||||
authProvider.setUserDetailsService(baseUserDetailsService); |
||||
authProvider.setPasswordEncoder(passwordEncoder()); |
||||
return new ProviderManager(authProvider); |
||||
} |
||||
|
||||
@Bean |
||||
protected SecurityFilterChain filterChain(HttpSecurity http) throws Exception { |
||||
setDefaultHttpSecurity(http); |
||||
http |
||||
.authorizeHttpRequests(authz -> |
||||
authz |
||||
.anyRequest().authenticated() |
||||
); |
||||
return http.build(); |
||||
} |
||||
|
||||
public void setDefaultHttpSecurity(HttpSecurity http) throws Exception { |
||||
http |
||||
.csrf(AbstractHttpConfigurer::disable) |
||||
.sessionManagement(session -> |
||||
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS) |
||||
) |
||||
.httpBasic(httpSecurityHttpBasicConfigurer -> httpSecurityHttpBasicConfigurer.authenticationEntryPoint(baseAuthenticationEntryPoint)) |
||||
.addFilterAt(new JwtLoginFilter(authenticationManager(), baseUserDetailsService), UsernamePasswordAuthenticationFilter.class) |
||||
.addFilterAt(new JwtCheckFilter(authenticationManager(), baseUserDetailsService, baseAuthenticationEntryPoint), BasicAuthenticationFilter.class) |
||||
.exceptionHandling(exceptionHandlingconfig -> |
||||
exceptionHandlingconfig |
||||
.authenticationEntryPoint(baseAuthenticationEntryPoint) |
||||
.accessDeniedHandler(baseAccessDeniedHandler) |
||||
); |
||||
} |
||||
|
||||
@Bean |
||||
public WebSecurityCustomizer webSecurityCustomizer() { |
||||
return web -> web.ignoring() |
||||
.requestMatchers(SWAGGER_URI); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,23 @@
|
||||
package kr.co.palnet.kac.config.security.exception; |
||||
|
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import lombok.RequiredArgsConstructor; |
||||
import org.springframework.security.access.AccessDeniedException; |
||||
import org.springframework.security.web.access.AccessDeniedHandler; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
@RequiredArgsConstructor |
||||
@Component |
||||
public class BaseAccessDeniedHandler implements AccessDeniedHandler { |
||||
|
||||
private final BaseAuthenticationExceptionHandler baseAuthenticationExceptionHandler; |
||||
|
||||
@Override |
||||
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { |
||||
baseAuthenticationExceptionHandler.handle(request, response, accessDeniedException); |
||||
} |
||||
} |
@ -0,0 +1,25 @@
|
||||
package kr.co.palnet.kac.config.security.exception; |
||||
|
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.web.AuthenticationEntryPoint; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
@Slf4j |
||||
@RequiredArgsConstructor |
||||
@Component |
||||
public class BaseAuthenticationEntryPoint implements AuthenticationEntryPoint { |
||||
|
||||
private final BaseAuthenticationExceptionHandler baseAuthenticationExceptionHandler; |
||||
|
||||
@Override |
||||
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { |
||||
baseAuthenticationExceptionHandler.handle(request, response, authException); |
||||
} |
||||
} |
@ -0,0 +1,84 @@
|
||||
package kr.co.palnet.kac.config.security.exception; |
||||
|
||||
import kr.co.palnet.kac.config.security.model.BaseAuthErrorCode; |
||||
import lombok.Getter; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
|
||||
@Getter |
||||
public class BaseAuthenticationException extends AuthenticationException { |
||||
private final BaseAuthErrorCode errorCode; |
||||
private final Object[] paramArray; |
||||
|
||||
public BaseAuthenticationException() { |
||||
this(null, BaseAuthErrorCode.UNAUTHORIZED, (Object[]) null); |
||||
} |
||||
|
||||
public BaseAuthenticationException(BaseAuthErrorCode errorCode) { |
||||
this(null, errorCode, (Object[]) null); |
||||
} |
||||
|
||||
public BaseAuthenticationException(Object[] paramArray) { |
||||
this(null, BaseAuthErrorCode.UNAUTHORIZED, paramArray); |
||||
} |
||||
|
||||
public BaseAuthenticationException(BaseAuthErrorCode errorCode, Object[] paramArray) { |
||||
this(null, errorCode, paramArray); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg) { |
||||
this(msg, BaseAuthErrorCode.UNAUTHORIZED, (Object[]) null); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, Object[] paramArray) { |
||||
this(msg, BaseAuthErrorCode.UNAUTHORIZED, paramArray); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, BaseAuthErrorCode errorCode) { |
||||
this(msg, errorCode, (Object[]) null); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, BaseAuthErrorCode errorCode, Object[] paramArray) { |
||||
super(msg); |
||||
this.errorCode = errorCode; |
||||
this.paramArray = paramArray; |
||||
} |
||||
|
||||
public BaseAuthenticationException(Throwable cause) { |
||||
this(null, BaseAuthErrorCode.UNAUTHORIZED, null, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, BaseAuthErrorCode errorCode, Throwable cause) { |
||||
this(msg, errorCode, null, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, Throwable cause) { |
||||
this(msg, BaseAuthErrorCode.UNAUTHORIZED, null, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(Object[] paramArray, Throwable cause) { |
||||
this(null, BaseAuthErrorCode.UNAUTHORIZED, paramArray, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, Object[] paramArray, Throwable cause) { |
||||
this(msg, BaseAuthErrorCode.UNAUTHORIZED, paramArray, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(BaseAuthErrorCode errorCode, Throwable cause) { |
||||
this(null, errorCode, null, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(BaseAuthErrorCode errorCode, Object[] paramArray, Throwable cause) { |
||||
this(null, errorCode, paramArray, cause); |
||||
} |
||||
|
||||
public BaseAuthenticationException(String msg, BaseAuthErrorCode errorCode, Object[] paramArray, Throwable cause) { |
||||
super(msg, cause); |
||||
this.errorCode = errorCode; |
||||
this.paramArray = paramArray; |
||||
} |
||||
|
||||
|
||||
public String getCode() { |
||||
return errorCode.code(); |
||||
} |
||||
} |
@ -0,0 +1,83 @@
|
||||
package kr.co.palnet.kac.config.security.exception; |
||||
|
||||
import jakarta.servlet.RequestDispatcher; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import kr.co.palnet.kac.config.security.model.BaseAuthErrorCode; |
||||
import kr.co.palnet.kac.config.security.model.AuthErrorRS; |
||||
import kr.co.palnet.kac.util.ObjectMapperUtils; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.beans.factory.annotation.Qualifier; |
||||
import org.springframework.context.support.MessageSourceAccessor; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.HttpStatus; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.security.access.AccessDeniedException; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.stereotype.Component; |
||||
|
||||
import java.io.IOException; |
||||
import java.time.Instant; |
||||
|
||||
@Slf4j |
||||
@RequiredArgsConstructor |
||||
@Component |
||||
public class BaseAuthenticationExceptionHandler { |
||||
|
||||
@Qualifier("authErrorMessageSourceAccessor") |
||||
private final MessageSourceAccessor authErrorMessageSourceAccessor; |
||||
|
||||
public void handle(HttpServletRequest request, HttpServletResponse response, Exception authException) throws IOException, ServletException { |
||||
|
||||
Object excpetion = request.getAttribute(RequestDispatcher.ERROR_EXCEPTION); |
||||
if (excpetion == null) { |
||||
excpetion = authException; |
||||
} |
||||
|
||||
String reqURI = (String) request.getAttribute(RequestDispatcher.ERROR_REQUEST_URI); |
||||
if (reqURI == null) { |
||||
reqURI = request.getRequestURI(); |
||||
} |
||||
|
||||
|
||||
BaseAuthErrorCode errorCode = BaseAuthErrorCode.UNAUTHORIZED; |
||||
if (excpetion instanceof BaseAuthenticationException) { |
||||
log.debug("BaseAuthenticationException : {}", excpetion.getClass()); |
||||
BaseAuthenticationException bae = (BaseAuthenticationException) excpetion; |
||||
BaseAuthErrorCode baseAuthErrorCode = bae.getErrorCode(); |
||||
if (baseAuthErrorCode != null) { |
||||
errorCode = baseAuthErrorCode; |
||||
} |
||||
} |
||||
if (excpetion instanceof AuthenticationException) { |
||||
log.debug("AuthenticationException : {}", excpetion.getClass()); |
||||
errorCode = BaseAuthErrorCode.INVALID_TOKEN; |
||||
AuthenticationException ae = (AuthenticationException) excpetion; |
||||
// TODO 종류 별로 ErrrorCode 분기 처리
|
||||
|
||||
|
||||
} else if (excpetion instanceof AccessDeniedException) { |
||||
log.debug("AccessDeniedException : {}", excpetion.getClass()); |
||||
errorCode = BaseAuthErrorCode.UNAUTHORIZED; |
||||
AccessDeniedException ad = (AccessDeniedException) excpetion; |
||||
// TODO 종류 별로 ErrrorCode 분기 처리
|
||||
|
||||
|
||||
} |
||||
|
||||
AuthErrorRS rs = AuthErrorRS.builder() |
||||
.timestamp(Instant.now().toString()) |
||||
.path(reqURI) |
||||
.status(errorCode.status().value()) |
||||
.error(errorCode.status().getReasonPhrase()) |
||||
.code(errorCode.code()) |
||||
.message(authErrorMessageSourceAccessor.getMessage(errorCode.code())) |
||||
.build(); |
||||
|
||||
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |
||||
response.setStatus(HttpStatus.UNAUTHORIZED.value()); |
||||
response.getOutputStream().write(ObjectMapperUtils.getObjectMapper().writeValueAsBytes(rs)); |
||||
} |
||||
} |
@ -0,0 +1,75 @@
|
||||
package kr.co.palnet.kac.config.security.filter; |
||||
|
||||
import com.auth0.jwt.exceptions.TokenExpiredException; |
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import kr.co.palnet.kac.config.security.util.JwtUtil; |
||||
import kr.co.palnet.kac.config.security.exception.BaseAuthenticationException; |
||||
import kr.co.palnet.kac.config.security.model.BaseAuthErrorCode; |
||||
import kr.co.palnet.kac.config.security.model.BaseUserDetails; |
||||
import kr.co.palnet.kac.config.security.service.BaseUserDetailsService; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.context.SecurityContextHolder; |
||||
import org.springframework.security.web.AuthenticationEntryPoint; |
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
@Slf4j |
||||
public class JwtCheckFilter extends BasicAuthenticationFilter { |
||||
|
||||
private BaseUserDetailsService baseUserDetailsService; |
||||
|
||||
public JwtCheckFilter(AuthenticationManager authenticationManager, BaseUserDetailsService baseUserDetailsService) { |
||||
super(authenticationManager); |
||||
this.baseUserDetailsService = baseUserDetailsService; |
||||
} |
||||
|
||||
public JwtCheckFilter(AuthenticationManager authenticationManager, BaseUserDetailsService baseUserDetailsService, AuthenticationEntryPoint authenticationEntryPoint) { |
||||
super(authenticationManager, authenticationEntryPoint); |
||||
this.baseUserDetailsService = baseUserDetailsService; |
||||
} |
||||
|
||||
@Override |
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { |
||||
// super.doFilterInternal(request, response, chain);
|
||||
|
||||
String bearer = request.getHeader(HttpHeaders.AUTHORIZATION); |
||||
if (bearer == null || !bearer.startsWith("Bearer ")) { |
||||
chain.doFilter(request, response); |
||||
return; |
||||
} |
||||
|
||||
String token = bearer.substring("Bearer ".length()); |
||||
|
||||
BaseUserDetails userDetails; |
||||
try { |
||||
userDetails = JwtUtil.verify(token, "AUTH"); |
||||
} catch (TokenExpiredException e) { |
||||
throw new BaseAuthenticationException(BaseAuthErrorCode.EXPIRED_TOKEN); |
||||
} catch (Exception e) { |
||||
log.debug(">>> e : {}", e.getClass().getName()); |
||||
throw new BaseAuthenticationException(BaseAuthErrorCode.INVALID_TOKEN); |
||||
} |
||||
|
||||
/* |
||||
실시간 처리가 필요할 경우 사용 |
||||
access token(auth token)을 짧게 잡고 refresh token으로 재발급시 DB조회 하도록 구성 |
||||
UserDetails newUserDetails = baseUserDetailsService.loadUserByUsername(userDetails.getUserId()); |
||||
*/ |
||||
|
||||
// 토큰 자체를 신뢰함.
|
||||
UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken( |
||||
userDetails, null, userDetails.getAuthorities() |
||||
); |
||||
|
||||
SecurityContextHolder.getContext().setAuthentication(userToken); |
||||
|
||||
chain.doFilter(request, response); |
||||
} |
||||
} |
@ -0,0 +1,66 @@
|
||||
package kr.co.palnet.kac.config.security.filter; |
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import jakarta.servlet.FilterChain; |
||||
import jakarta.servlet.ServletException; |
||||
import jakarta.servlet.http.HttpServletRequest; |
||||
import jakarta.servlet.http.HttpServletResponse; |
||||
import kr.co.palnet.kac.config.security.util.JwtUtil; |
||||
import kr.co.palnet.kac.config.security.model.UserLoginForm; |
||||
import kr.co.palnet.kac.config.security.exception.BaseAuthenticationException; |
||||
import kr.co.palnet.kac.config.security.model.BaseAuthErrorCode; |
||||
import kr.co.palnet.kac.config.security.model.BaseUserDetails; |
||||
import kr.co.palnet.kac.config.security.service.BaseUserDetailsService; |
||||
import kr.co.palnet.kac.util.ObjectMapperUtils; |
||||
import org.springframework.http.HttpHeaders; |
||||
import org.springframework.http.MediaType; |
||||
import org.springframework.security.authentication.AuthenticationManager; |
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; |
||||
import org.springframework.security.core.Authentication; |
||||
import org.springframework.security.core.AuthenticationException; |
||||
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; |
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; |
||||
|
||||
import java.io.IOException; |
||||
|
||||
public class JwtLoginFilter extends UsernamePasswordAuthenticationFilter { |
||||
private BaseUserDetailsService baseUserDetailsService; |
||||
private ObjectMapper objectMapper = ObjectMapperUtils.getObjectMapper(); |
||||
|
||||
public JwtLoginFilter(BaseUserDetailsService baseUserDetailsService) { |
||||
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/v1/login", "POST")); |
||||
} |
||||
|
||||
public JwtLoginFilter(AuthenticationManager authenticationManager, BaseUserDetailsService baseUserDetailsService) { |
||||
super(authenticationManager); |
||||
this.baseUserDetailsService = baseUserDetailsService; |
||||
setRequiresAuthenticationRequestMatcher(new AntPathRequestMatcher("/v1/login", "POST")); |
||||
|
||||
} |
||||
|
||||
|
||||
@Override |
||||
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { |
||||
try { |
||||
UserLoginForm userLogin = objectMapper.readValue(request.getInputStream(), UserLoginForm.class); |
||||
|
||||
// TODO 로그인 처리
|
||||
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken( |
||||
userLogin.getUserId(), userLogin.getPassword(), null |
||||
); |
||||
return getAuthenticationManager().authenticate(token); |
||||
|
||||
} catch (IOException e) { |
||||
throw new BaseAuthenticationException(BaseAuthErrorCode.INVALID_LOGIN); |
||||
} |
||||
} |
||||
|
||||
@Override |
||||
protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException { |
||||
BaseUserDetails user = (BaseUserDetails) authResult.getPrincipal(); |
||||
response.setHeader("Auth-Token", JwtUtil.makeAuthToken(user)); |
||||
response.setHeader("Refresh-Token", JwtUtil.makeRefreshToken(user)); |
||||
response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE); |
||||
response.getOutputStream().write(ObjectMapperUtils.getObjectMapper().writeValueAsBytes(user.toMap())); |
||||
} |
||||
} |
@ -0,0 +1,26 @@
|
||||
package kr.co.palnet.kac.config.security.message; |
||||
|
||||
import org.springframework.beans.factory.annotation.Qualifier; |
||||
import org.springframework.context.MessageSource; |
||||
import org.springframework.context.annotation.Bean; |
||||
import org.springframework.context.annotation.Configuration; |
||||
import org.springframework.context.support.MessageSourceAccessor; |
||||
import org.springframework.context.support.ReloadableResourceBundleMessageSource; |
||||
|
||||
@Configuration |
||||
public class AuthErrorMessageSourceConfig { |
||||
|
||||
@Bean(name = "authErrorMessageSource") |
||||
public MessageSource getAuthErrorMessageSource() { |
||||
ReloadableResourceBundleMessageSource authErrorMessageSource = new ReloadableResourceBundleMessageSource(); |
||||
authErrorMessageSource.setBasenames("classpath:messages/errors/auth_error"); |
||||
authErrorMessageSource.setDefaultEncoding("UTF-8"); |
||||
authErrorMessageSource.setCacheSeconds(300); |
||||
return authErrorMessageSource; |
||||
} |
||||
|
||||
@Bean(name = "authErrorMessageSourceAccessor") |
||||
public MessageSourceAccessor authErrorMessageSourceAccessor(@Qualifier("authErrorMessageSource") MessageSource authErrorMessageSource) { |
||||
return new MessageSourceAccessor(authErrorMessageSource); |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
package kr.co.palnet.kac.config.security.model; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import lombok.Builder; |
||||
import lombok.Data; |
||||
import lombok.NoArgsConstructor; |
||||
|
||||
@Data |
||||
@NoArgsConstructor |
||||
@AllArgsConstructor |
||||
@Builder |
||||
public class AuthErrorRS { |
||||
private String timestamp; |
||||
private int status; |
||||
private String error; |
||||
private String path; |
||||
private String message; |
||||
private String code; |
||||
} |
@ -0,0 +1,49 @@
|
||||
package kr.co.palnet.kac.config.security.model; |
||||
|
||||
|
||||
import org.springframework.http.HttpStatus; |
||||
|
||||
public enum BaseAuthErrorCode { |
||||
|
||||
// 비밀번호를 10회 이상 틀렸습니다. 관리자에게 문의하길 바랍니다.
|
||||
PASSWORD_OVER_INPUT("AT001", HttpStatus.BAD_REQUEST, "패스워드 입력 제한횟수 초과"), |
||||
// 로그인 정보가 올바르지 않습니다.
|
||||
INVALID_LOGIN("AT002", HttpStatus.BAD_REQUEST, "잘못된 로그인 정보"), |
||||
|
||||
// TODO 로그인 상태에 따른 에러코드 정의 필요
|
||||
|
||||
|
||||
// 로그인이 필요한 서비스 입니다.
|
||||
NON_TOKEN("AT100", HttpStatus.UNAUTHORIZED, "토큰 없음"), |
||||
// 토큰 정보가 올바르지 않습니다.
|
||||
INVALID_TOKEN("AT101", HttpStatus.UNAUTHORIZED, "잘못된 토큰"), |
||||
// 토큰이 만료되었습니다.
|
||||
EXPIRED_TOKEN("AT102", HttpStatus.UNAUTHORIZED, "만료된 토큰"), |
||||
// 권한이 없습니다.
|
||||
UNAUTHORIZED("AT103", HttpStatus.UNAUTHORIZED, "권한 없음"); |
||||
|
||||
|
||||
private final String code; |
||||
|
||||
private final HttpStatus status; |
||||
|
||||
private final String message; |
||||
|
||||
private BaseAuthErrorCode(String code, HttpStatus status, String message) { |
||||
this.code = code; |
||||
this.status = status; |
||||
this.message = message; |
||||
} |
||||
|
||||
public String code() { |
||||
return this.code; |
||||
} |
||||
|
||||
public String message() { |
||||
return this.message; |
||||
} |
||||
|
||||
public HttpStatus status() { |
||||
return this.status; |
||||
} |
||||
} |
@ -0,0 +1,89 @@
|
||||
package kr.co.palnet.kac.config.security.model; |
||||
|
||||
|
||||
import kr.co.palnet.kac.data.pty.model.PtyCstmrBas; |
||||
import lombok.AllArgsConstructor; |
||||
import lombok.Builder; |
||||
import lombok.Data; |
||||
import lombok.NoArgsConstructor; |
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
|
||||
import java.util.List; |
||||
import java.util.Map; |
||||
|
||||
@Data |
||||
@NoArgsConstructor |
||||
@AllArgsConstructor |
||||
@Builder |
||||
public class BaseUserDetails implements UserDetails { |
||||
|
||||
// 유저 기본 정보
|
||||
// sno
|
||||
private Long cstmrSno; |
||||
// id
|
||||
private String userId; |
||||
// password
|
||||
private String userPswd; |
||||
// authorities(role) - 추후 여러권한을 가질 수 있도록 설계 필요
|
||||
private String cstmrDivCd; |
||||
// 계정상태
|
||||
private String cstmrStatusCd; |
||||
|
||||
@Override |
||||
public List<SimpleGrantedAuthority> getAuthorities() { |
||||
String role = String.format("ROLE_%s", this.cstmrDivCd); |
||||
SimpleGrantedAuthority authority = new SimpleGrantedAuthority(role); |
||||
return List.of(authority); |
||||
} |
||||
|
||||
@Override |
||||
public String getPassword() { |
||||
return this.userPswd; |
||||
} |
||||
|
||||
@Override |
||||
public String getUsername() { |
||||
return this.userId; |
||||
} |
||||
|
||||
@Override |
||||
public boolean isAccountNonExpired() { |
||||
return !"DORMANT".equals(this.cstmrStatusCd); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isAccountNonLocked() { |
||||
return !"LOCK".equals(this.cstmrStatusCd); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isCredentialsNonExpired() { |
||||
return !"PWD_EXPIRED".equals(this.cstmrStatusCd); |
||||
} |
||||
|
||||
@Override |
||||
public boolean isEnabled() { |
||||
return "ACTIVE".equals(this.cstmrStatusCd); |
||||
} |
||||
|
||||
public static BaseUserDetails toUserDetails(PtyCstmrBas ptyCstmrBas) { |
||||
return BaseUserDetails.builder() |
||||
.cstmrSno(ptyCstmrBas.getCstmrSno()) |
||||
.userId(ptyCstmrBas.getUserId()) |
||||
.userPswd(ptyCstmrBas.getUserPswd()) |
||||
.cstmrDivCd(ptyCstmrBas.getCstmrDivCd()) |
||||
.cstmrStatusCd(ptyCstmrBas.getCstmrStatusCd()) |
||||
.build(); |
||||
} |
||||
|
||||
public Map<String, Object> toMap() { |
||||
return Map.of( |
||||
"cstmrSno", this.cstmrSno, |
||||
// "cstmrDivCd", this.cstmrDivCd,
|
||||
// "cstmrStatusCd", this.cstmrStatusCd
|
||||
"userId", this.userId |
||||
); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,15 @@
|
||||
package kr.co.palnet.kac.config.security.model; |
||||
|
||||
import lombok.AllArgsConstructor; |
||||
import lombok.Builder; |
||||
import lombok.Data; |
||||
import lombok.NoArgsConstructor; |
||||
|
||||
@Data |
||||
@NoArgsConstructor |
||||
@AllArgsConstructor |
||||
@Builder |
||||
public class UserLoginForm { |
||||
private String userId; |
||||
private String password; |
||||
} |
@ -0,0 +1,28 @@
|
||||
package kr.co.palnet.kac.config.security.service; |
||||
|
||||
import kr.co.palnet.kac.config.security.model.BaseUserDetails; |
||||
import kr.co.palnet.kac.data.pty.model.PtyCstmrBas; |
||||
import kr.co.palnet.kac.data.pty.service.PtyCstmrDomainService; |
||||
import lombok.RequiredArgsConstructor; |
||||
import org.springframework.security.core.userdetails.UserDetails; |
||||
import org.springframework.security.core.userdetails.UserDetailsService; |
||||
import org.springframework.security.core.userdetails.UsernameNotFoundException; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
@RequiredArgsConstructor |
||||
@Service |
||||
public class BaseUserDetailsService implements UserDetailsService { |
||||
|
||||
private final PtyCstmrDomainService ptyCstmrDomainService; |
||||
|
||||
@Override |
||||
public UserDetails loadUserByUsername(String userId) throws UsernameNotFoundException { |
||||
PtyCstmrBas cstmrInfoByUserId = ptyCstmrDomainService.getCstmrInfoByUserId(userId); |
||||
if(cstmrInfoByUserId == null){ |
||||
throw new UsernameNotFoundException("사용자 정보가 없습니다."); |
||||
} |
||||
BaseUserDetails userDetails = BaseUserDetails.toUserDetails(cstmrInfoByUserId); |
||||
return userDetails; |
||||
} |
||||
|
||||
} |
@ -0,0 +1,75 @@
|
||||
package kr.co.palnet.kac.config.security.util; |
||||
|
||||
import com.auth0.jwt.JWT; |
||||
import com.auth0.jwt.algorithms.Algorithm; |
||||
import com.auth0.jwt.exceptions.JWTVerificationException; |
||||
import com.auth0.jwt.interfaces.DecodedJWT; |
||||
import kr.co.palnet.kac.config.security.model.BaseUserDetails; |
||||
import kr.co.palnet.kac.util.KisaEncryptUtil; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
|
||||
import java.time.Instant; |
||||
|
||||
@Slf4j |
||||
public class JwtUtil { |
||||
// TODO key는 properties에서 가져올수 있도록 처리
|
||||
private static final Algorithm ALGORITHM = Algorithm.HMAC512("pal-networks"); |
||||
// 1시간
|
||||
// private static final long AUTH_TIME = 60 * 60;
|
||||
private static final long AUTH_TIME = 10; |
||||
// 7일
|
||||
private static final long REFRESH_TIME = 60 * 60 * 24 * 7; |
||||
|
||||
public static String makeAuthToken(BaseUserDetails user) { |
||||
String encCstmrSno = KisaEncryptUtil.CbcEncrypt.encrypt(String.valueOf(user.getCstmrSno())); |
||||
// List<SimpleGrantedAuthority> authorities = user.getAuthorities();
|
||||
// String authStr = String.join(",", authorities.stream().map(GrantedAuthority::getAuthority).map(str -> str.replaceFirst("ROLE_", "")).toArray(String[]::new));
|
||||
String authStr = user.getCstmrDivCd(); |
||||
return JWT.create() |
||||
.withSubject("AUTH") |
||||
.withAudience("kac") |
||||
.withIssuer("palnet") |
||||
.withIssuedAt(Instant.now()) |
||||
.withExpiresAt(Instant.ofEpochMilli(Instant.now().plusSeconds(AUTH_TIME).toEpochMilli())) |
||||
.withClaim("userId", user.getUserId()) |
||||
.withClaim("sno", encCstmrSno) |
||||
.withClaim("role", authStr) |
||||
.sign(ALGORITHM); |
||||
} |
||||
|
||||
public static String makeRefreshToken(BaseUserDetails user) { |
||||
String encCstmrSno = KisaEncryptUtil.CbcEncrypt.encrypt(String.valueOf(user.getCstmrSno())); |
||||
// List<SimpleGrantedAuthority> authorities = user.getAuthorities();
|
||||
// String authStr = String.join(",", authorities.stream().map(GrantedAuthority::getAuthority).map(str -> str.replaceFirst("ROLE_", "")).toArray(String[]::new));
|
||||
String authStr = user.getCstmrDivCd(); |
||||
return JWT.create() |
||||
.withSubject("REFRESH") |
||||
.withAudience("kac") |
||||
.withIssuer("palnet") |
||||
.withIssuedAt(Instant.now()) |
||||
.withExpiresAt(Instant.ofEpochMilli(Instant.now().plusSeconds(REFRESH_TIME).toEpochMilli())) |
||||
.withClaim("userId", user.getUserId()) |
||||
.withClaim("sno", encCstmrSno) |
||||
.withClaim("role", authStr) |
||||
.sign(ALGORITHM); |
||||
} |
||||
|
||||
|
||||
public static BaseUserDetails verify(String token, String type) { |
||||
DecodedJWT decodeJwt = JWT.require(ALGORITHM).build().verify(token); |
||||
String sub = decodeJwt.getSubject(); |
||||
if (!type.equals(sub)) { |
||||
throw new JWTVerificationException("Token is not valid - sub(type)"); |
||||
} |
||||
String decCstmrSno = KisaEncryptUtil.CbcEncrypt.decrypt(decodeJwt.getClaim("sno").asString()); |
||||
String decRole = decodeJwt.getClaim("role").asString(); |
||||
BaseUserDetails userDetails = BaseUserDetails.builder() |
||||
.userId(decodeJwt.getClaim("userId").asString()) |
||||
.cstmrSno(Long.parseLong(decCstmrSno)) |
||||
.cstmrDivCd(decRole) |
||||
.build(); |
||||
return userDetails; |
||||
} |
||||
|
||||
|
||||
} |
@ -0,0 +1 @@
|
||||
lombok.copyableAnnotations += org.springframework.beans.factory.annotation.Qualifier |
@ -0,0 +1,6 @@
|
||||
AT001=\uBE44\uBC00\uBC88\uD638\uB97C 10\uD68C \uC774\uC0C1 \uD2C0\uB838\uC2B5\uB2C8\uB2E4. \uAD00\uB9AC\uC790\uC5D0\uAC8C \uBB38\uC758\uD558\uAE38 \uBC14\uB78D\uB2C8\uB2E4. |
||||
AT002=\uB85C\uADF8\uC778 \uC815\uBCF4\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. |
||||
AT100=\uB85C\uADF8\uC778\uC774 \uD544\uC694\uD55C \uC11C\uBE44\uC2A4 \uC785\uB2C8\uB2E4. |
||||
AT101=\uD1A0\uD070 \uC815\uBCF4\uAC00 \uC62C\uBC14\uB974\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4. |
||||
AT102=\uD1A0\uD070\uC774 \uB9CC\uB8CC\uB418\uC5C8\uC2B5\uB2C8\uB2E4. |
||||
AT103=\uAD8C\uD55C\uC774 \uC5C6\uC2B5\uB2C8\uB2E4. |
@ -0,0 +1,6 @@
|
||||
AT001=Password incorrect more than 10 times, please contact your administrator. |
||||
AT002=The login information is not valid. |
||||
AT100=This service requires login. |
||||
AT101=Token information is incorrect. |
||||
AT102=Token has expired. |
||||
AT103=You do not have permission. |
@ -1,6 +1,7 @@
|
||||
|
||||
|
||||
dependencies { |
||||
implementation 'org.springframework.boot:spring-boot-starter' |
||||
// implementation "$boot:spring-boot-starter" |
||||
implementation "$boot:spring-boot-starter-json" |
||||
} |
||||
|
||||
|
@ -0,0 +1,52 @@
|
||||
package kr.co.palnet.kac.util; |
||||
|
||||
import com.fasterxml.jackson.annotation.JsonInclude; |
||||
import com.fasterxml.jackson.databind.DeserializationFeature; |
||||
import com.fasterxml.jackson.databind.MapperFeature; |
||||
import com.fasterxml.jackson.databind.ObjectMapper; |
||||
import com.fasterxml.jackson.databind.SerializationFeature; |
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer; |
||||
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; |
||||
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder; |
||||
|
||||
import java.time.format.DateTimeFormatter; |
||||
import java.util.TimeZone; |
||||
|
||||
/** |
||||
* packageName : com.sumair.common.core.utils |
||||
* fileName : ObjectMapperUtils |
||||
* author : dhji |
||||
* date : 2023-05-17(017) |
||||
* description : |
||||
* =========================================================== |
||||
* DATE AUTHOR NOTE |
||||
* ----------------------------------------------------------- |
||||
* 2023-05-17(017) dhji 최초 생성 |
||||
*/ |
||||
public class ObjectMapperUtils { |
||||
public static ObjectMapper getObjectMapper() { |
||||
return getObjectMapperBuilder().build(); |
||||
} |
||||
|
||||
public static Jackson2ObjectMapperBuilder getObjectMapperBuilder() { |
||||
Jackson2ObjectMapperBuilder jacksonBuilder = new Jackson2ObjectMapperBuilder(); |
||||
jacksonBuilder.serializationInclusion(JsonInclude.Include.NON_NULL); |
||||
jacksonBuilder.serializationInclusion(JsonInclude.Include.NON_EMPTY); |
||||
// jacksonBuilder.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
|
||||
// jacksonBuilder.featuresToDisable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
// jacksonBuilder.featuresToDisable(DeserializationFeature.ADJUST_DATES_TO_CONTEXT_TIME_ZONE);
|
||||
// jacksonBuilder.featuresToDisable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES);
|
||||
// jacksonBuilder.modulesToInstall(new JavaTimeModule());
|
||||
// jacksonBuilder.timeZone(TimeZone.getTimeZone("UTC"));
|
||||
// jacksonBuilder.simpleDateFormat("yyyy-MM-dd HH:mm:ss");
|
||||
// jacksonBuilder.serializers(new LocalDateSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
// jacksonBuilder.serializers(new LocalDateTimeSerializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
// jacksonBuilder.deserializers(new LocalDateDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
|
||||
// jacksonBuilder.deserializers(new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
|
||||
|
||||
return jacksonBuilder; |
||||
} |
||||
} |
@ -0,0 +1,27 @@
|
||||
|
||||
dependencies { |
||||
implementation "$boot:spring-boot-starter-data-jpa" |
||||
|
||||
implementation "com.querydsl:querydsl-jpa:5.0.0:jakarta" |
||||
annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" |
||||
annotationProcessor "jakarta.annotation:jakarta.annotation-api" |
||||
annotationProcessor "jakarta.persistence:jakarta.persistence-api" |
||||
|
||||
|
||||
|
||||
testRuntimeOnly("com.h2database:h2") |
||||
} |
||||
|
||||
def querydslDir = layout.buildDirectory.dir("generated/querydsl").get().asFile |
||||
|
||||
sourceSets { |
||||
main.java.srcDir(querydslDir) |
||||
} |
||||
|
||||
tasks.withType(JavaCompile) { |
||||
options.getGeneratedSourceOutputDirectory().set(file(querydslDir)) |
||||
} |
||||
|
||||
clean { |
||||
delete file(querydslDir) |
||||
} |
@ -0,0 +1,11 @@
|
||||
package kr.co.palnet.kac.data.pty.repository; |
||||
|
||||
import kr.co.palnet.kac.data.pty.model.PtyCstmrBas; |
||||
import org.springframework.data.jpa.repository.JpaRepository; |
||||
import org.springframework.data.jpa.repository.Query; |
||||
import org.springframework.data.repository.query.Param; |
||||
|
||||
public interface PtyCstmrBasRepository extends JpaRepository<PtyCstmrBas, Integer> { |
||||
@Query("select b from PtyCstmrBas b inner join b.ptyCstmrDtl where b.userId = :userId") |
||||
PtyCstmrBas findByUserId(@Param("userId") String userId); |
||||
} |
@ -0,0 +1,19 @@
|
||||
package kr.co.palnet.kac.data.pty.service; |
||||
|
||||
import kr.co.palnet.kac.data.pty.model.PtyCstmrBas; |
||||
import kr.co.palnet.kac.data.pty.repository.PtyCstmrBasRepository; |
||||
import lombok.RequiredArgsConstructor; |
||||
import lombok.extern.slf4j.Slf4j; |
||||
import org.springframework.stereotype.Service; |
||||
|
||||
@Slf4j |
||||
@RequiredArgsConstructor |
||||
@Service |
||||
public class PtyCstmrDomainService { |
||||
private final PtyCstmrBasRepository ptyCstmrBasRepository; |
||||
|
||||
public PtyCstmrBas getCstmrInfoByUserId(String userId) { |
||||
return ptyCstmrBasRepository.findByUserId(userId); |
||||
} |
||||
|
||||
} |
@ -0,0 +1,5 @@
|
||||
{ |
||||
"local": { |
||||
"host": "http://localhost:8080" |
||||
} |
||||
} |
@ -0,0 +1,19 @@
|
||||
### 로그인 |
||||
@userId = user |
||||
@password = 1234 |
||||
|
||||
POST {{host}}/v1/login |
||||
Authorization: Bearer {{authToken}} // 토큰이 있을 경우 필터에 걸리는지 확인하기 위한 조치 |
||||
Content-Type: application/json |
||||
|
||||
{ |
||||
"userId": "{{userId}}", |
||||
"password": "{{password}}" |
||||
} |
||||
|
||||
> {% |
||||
client.global.set("authToken", response.headers.valueOf("Auth-Token")); |
||||
%} |
||||
|
||||
|
||||
|
Loading…
Reference in new issue