admin: server exception 발생 시 stateless 로 인한 csrf 재발급 처리
This commit is contained in:
parent
9370aaa1df
commit
c140f8fffb
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.bpgroup.poc.admin.common;
|
||||||
|
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
|
|
||||||
|
public class SubStringHelper {
|
||||||
|
|
||||||
|
public static String substringInBytes(String original, int byteLength) {
|
||||||
|
byte[] bytes = original.getBytes(StandardCharsets.UTF_8);
|
||||||
|
if (bytes.length > byteLength) {
|
||||||
|
bytes = Arrays.copyOf(bytes, byteLength);
|
||||||
|
return new String(bytes, StandardCharsets.UTF_8);
|
||||||
|
}
|
||||||
|
return original;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -16,7 +16,7 @@ public class Admin extends BaseEntity {
|
||||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Column(name = "login_id", length = 100, nullable = false)
|
@Column(name = "login_id", length = 100, nullable = false, unique = true)
|
||||||
private String loginId;
|
private String loginId;
|
||||||
|
|
||||||
@Column(name = "password", length = 255, nullable = false)
|
@Column(name = "password", length = 255, nullable = false)
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
package com.bpgroup.poc.admin.domain.admin.entity;
|
package com.bpgroup.poc.admin.domain.admin.entity;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.common.SubStringHelper;
|
||||||
import com.bpgroup.poc.admin.domain.BaseEntity;
|
import com.bpgroup.poc.admin.domain.BaseEntity;
|
||||||
import jakarta.persistence.*;
|
import jakarta.persistence.*;
|
||||||
import lombok.AccessLevel;
|
import lombok.AccessLevel;
|
||||||
|
|
@ -23,7 +24,7 @@ public class AdminActionLog extends BaseEntity {
|
||||||
@Column(name = "request_value")
|
@Column(name = "request_value")
|
||||||
private String requestValue;
|
private String requestValue;
|
||||||
|
|
||||||
@Column(name = "response_value")
|
@Column(name = "response_value", length = 1000)
|
||||||
private String responseValue;
|
private String responseValue;
|
||||||
|
|
||||||
@ManyToOne(fetch = FetchType.LAZY)
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
|
@ -39,7 +40,7 @@ public class AdminActionLog extends BaseEntity {
|
||||||
|
|
||||||
private AdminActionLog(String sessionId, String responseValue) {
|
private AdminActionLog(String sessionId, String responseValue) {
|
||||||
this.sessionId = sessionId;
|
this.sessionId = sessionId;
|
||||||
this.responseValue = responseValue;
|
this.responseValue = SubStringHelper.substringInBytes(responseValue, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
public static AdminActionLog createOf(String sessionId, String requestUri, String requestValue, Admin admin) {
|
public static AdminActionLog createOf(String sessionId, String requestUri, String requestValue, Admin admin) {
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,5 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface AdminTokenRepository extends JpaRepository<AdminToken, Long> {
|
public interface AdminTokenRepository extends JpaRepository<AdminToken, Long> {
|
||||||
Optional<AdminToken> findLastByAdminIdAndIpAndState(Long adminId, String ip, TokenState state);
|
Optional<AdminToken> findFirstByAdminIdAndIpAndStateOrderByIdDesc(Long adminId, String ip, TokenState state);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Validated
|
@Validated
|
||||||
|
|
@ -26,8 +28,11 @@ public class AdminActionLogService {
|
||||||
public void update(
|
public void update(
|
||||||
@NotNull @Valid AdminActionLogUpdateCommand command
|
@NotNull @Valid AdminActionLogUpdateCommand command
|
||||||
) {
|
) {
|
||||||
AdminActionLog findAdminActionLog = adminActionLogRepository.findBySessionId(command.getSessionId()).orElseThrow(() -> new IllegalArgumentException("Not found admin action log"));
|
Optional<AdminActionLog> findAdminActionLog = adminActionLogRepository.findBySessionId(command.getSessionId());
|
||||||
findAdminActionLog.update(command.toEntity());
|
if (findAdminActionLog.isPresent()) {
|
||||||
|
AdminActionLog adminActionLog = findAdminActionLog.get();
|
||||||
|
adminActionLog.update(command.toEntity());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,7 @@ public class AdminTokenService {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void expire(@NotNull @Valid AdminTokenExpireCommand command) {
|
public void expire(@NotNull @Valid AdminTokenExpireCommand command) {
|
||||||
AdminToken findAdminToken = adminTokenRepository.findLastByAdminIdAndIpAndState(command.getAdminId(), command.getIp(), TokenState.NORMAL)
|
AdminToken findAdminToken = adminTokenRepository.findFirstByAdminIdAndIpAndStateOrderByIdDesc(command.getAdminId(), command.getIp(), TokenState.NORMAL)
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Not found token"));
|
.orElseThrow(() -> new IllegalArgumentException("Not found token"));
|
||||||
|
|
||||||
if (findAdminToken.isNotExpired(LocalDateTime.now())) {
|
if (findAdminToken.isNotExpired(LocalDateTime.now())) {
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ public class LoggingFilter extends OncePerRequestFilter {
|
||||||
private final AdminActionLogAppService adminActionLogAppService;
|
private final AdminActionLogAppService adminActionLogAppService;
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"};
|
private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font", "/csrf"};
|
||||||
private static final String[] EXCLUDED_CONTENT_TYPES = {"text/html"};
|
private static final String[] EXCLUDED_CONTENT_TYPES = {"text/html"};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,10 @@
|
||||||
package com.bpgroup.poc.admin.security;
|
package com.bpgroup.poc.admin.security;
|
||||||
|
|
||||||
import com.bpgroup.poc.admin.app.jwt.JwtTokenAppService;
|
|
||||||
import com.bpgroup.poc.admin.security.authentication.*;
|
|
||||||
import com.bpgroup.poc.admin.app.authentication.AuthenticationAppService;
|
import com.bpgroup.poc.admin.app.authentication.AuthenticationAppService;
|
||||||
|
import com.bpgroup.poc.admin.app.authorization.AuthorizationAppService;
|
||||||
|
import com.bpgroup.poc.admin.security.authentication.*;
|
||||||
import com.bpgroup.poc.admin.security.authorization.CustomAccessDeniedHandler;
|
import com.bpgroup.poc.admin.security.authorization.CustomAccessDeniedHandler;
|
||||||
import com.bpgroup.poc.admin.security.authorization.CustomAuthorizationManager;
|
import com.bpgroup.poc.admin.security.authorization.CustomAuthorizationManager;
|
||||||
import com.bpgroup.poc.admin.app.authorization.AuthorizationAppService;
|
|
||||||
import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
|
import com.bpgroup.poc.admin.security.jwt.JwtTokenConstants;
|
||||||
import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider;
|
import com.bpgroup.poc.admin.security.jwt.JwtTokenProvider;
|
||||||
import com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter;
|
import com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter;
|
||||||
|
|
@ -38,7 +37,6 @@ public class SecurityConfig {
|
||||||
|
|
||||||
private final AuthenticationAppService authenticationAppService;
|
private final AuthenticationAppService authenticationAppService;
|
||||||
private final AuthorizationAppService authorizationAppService;
|
private final AuthorizationAppService authorizationAppService;
|
||||||
private final JwtTokenAppService jwtTokenAppService;
|
|
||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
|
|
||||||
|
|
@ -70,7 +68,7 @@ public class SecurityConfig {
|
||||||
private void configureAuthorization(HttpSecurity http) throws Exception {
|
private void configureAuthorization(HttpSecurity http) throws Exception {
|
||||||
http.authorizeHttpRequests(c -> c
|
http.authorizeHttpRequests(c -> c
|
||||||
.requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll()
|
.requestMatchers("/css/**", "/images/**", "/js/**", "/font/**", "/favicon.ico").permitAll()
|
||||||
.requestMatchers("/common/modal/**").permitAll()
|
.requestMatchers("/common/modal/**", "/csrf").permitAll()
|
||||||
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
||||||
.anyRequest()
|
.anyRequest()
|
||||||
.access(new CustomAuthorizationManager(authorizationAppService))
|
.access(new CustomAuthorizationManager(authorizationAppService))
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ import java.util.stream.Stream;
|
||||||
public class JwtTokenValidateFilter extends OncePerRequestFilter {
|
public class JwtTokenValidateFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final JwtTokenProvider jwtTokenProvider;
|
private final JwtTokenProvider jwtTokenProvider;
|
||||||
private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font"};
|
private static final String[] EXCLUDED_URL_PATTERNS = {"/login", "/logout", "/error", "/css", "/js", "/images", "/favicon.ico", "/common/modal", "/font", "/csrf"};
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
package com.bpgroup.poc.admin.web.common;
|
||||||
|
|
||||||
|
import com.bpgroup.poc.admin.web.common.reqres.CsrfTokenResponse;
|
||||||
|
import jakarta.servlet.http.HttpServletRequest;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.web.csrf.CsrfToken;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class CommonRestController {
|
||||||
|
@GetMapping("/csrf")
|
||||||
|
public ResponseEntity<CsrfTokenResponse> token(HttpServletRequest request) {
|
||||||
|
CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||||
|
return ResponseEntity.ok(CsrfTokenResponse.of(token.getToken(), token.getHeaderName()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
package com.bpgroup.poc.admin.web.common.reqres;
|
||||||
|
|
||||||
|
import lombok.AccessLevel;
|
||||||
|
import lombok.Getter;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.ToString;
|
||||||
|
|
||||||
|
@Getter
|
||||||
|
@ToString
|
||||||
|
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
|
||||||
|
public class CsrfTokenResponse {
|
||||||
|
private final String token;
|
||||||
|
private final String header;
|
||||||
|
|
||||||
|
public static CsrfTokenResponse of(String token, String header) {
|
||||||
|
return new CsrfTokenResponse(token, header);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -56,12 +56,15 @@ const PageHelper = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.showAlertModal({
|
alert(message);
|
||||||
title: '오류',
|
|
||||||
message: message,
|
// TODO: 퍼블리싱 추가 후 주석 해제 및 작업 필요
|
||||||
}, {
|
// this.showAlertModal({
|
||||||
cancel: cancelFunction
|
// title: '오류',
|
||||||
});
|
// message: message,
|
||||||
|
// }, {
|
||||||
|
// cancel: cancelFunction
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -98,17 +101,20 @@ const PageHelper = {
|
||||||
callbacks = {};
|
callbacks = {};
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loadAndApplyPage({
|
alert(params.message);
|
||||||
url: `/common/modal/alert`,
|
|
||||||
method: 'POST',
|
// TODO: 퍼블리싱 추가 후 주석 해제 및 작업 필요
|
||||||
data: `title=${params.title}&message=${params.message}`,
|
// this.loadAndApplyPage({
|
||||||
contentSelector: 'div[data-alert-modal="body"]',
|
// url: `/common/modal/alert`,
|
||||||
appendToSelector: 'body'
|
// method: 'POST',
|
||||||
}, {
|
// data: `title=${params.title}&message=${params.message}`,
|
||||||
success: function () {
|
// contentSelector: 'div[data-alert-modal="body"]',
|
||||||
EventRouter.register('clickAlertModalCancelButton', callbacks.cancel);
|
// appendToSelector: 'body'
|
||||||
},
|
// }, {
|
||||||
});
|
// success: function () {
|
||||||
|
// EventRouter.register('clickAlertModalCancelButton', callbacks.cancel);
|
||||||
|
// },
|
||||||
|
// });
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@ const Reqhelper = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
refreshCsrf();
|
||||||
if (eFunc) {
|
if (eFunc) {
|
||||||
eFunc(error);
|
eFunc(error);
|
||||||
}
|
}
|
||||||
|
|
@ -65,6 +66,7 @@ const Reqhelper = {
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
refreshCsrf();
|
||||||
if (eFunc) {
|
if (eFunc) {
|
||||||
eFunc(error);
|
eFunc(error);
|
||||||
}
|
}
|
||||||
|
|
@ -76,4 +78,18 @@ const Reqhelper = {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshCsrf() {
|
||||||
|
fetch('/csrf', {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}).then(response => {
|
||||||
|
response.json().then(data => {
|
||||||
|
const csrfToken = data.token;
|
||||||
|
document.querySelector('meta[name="_csrf"]').setAttribute('content', csrfToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
@ -88,6 +88,25 @@
|
||||||
})
|
})
|
||||||
;
|
;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 조회
|
||||||
|
*/
|
||||||
|
document.getElementById('btnFindAll').addEventListener(('click'), function () {
|
||||||
|
const requestUri = /*[[@{/admin/management/list}]]*/ '';
|
||||||
|
Reqhelper.reqGetJson(requestUri, (res) => {
|
||||||
|
const selAdmin = document.getElementById('selAdmin');
|
||||||
|
selAdmin.innerHTML = '<option value="">선택</option>';
|
||||||
|
res.forEach(item => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = item.id;
|
||||||
|
option.text = item.loginId;
|
||||||
|
selAdmin.appendChild(option);
|
||||||
|
});
|
||||||
|
}, () => {
|
||||||
|
PageHelper.showErrorModal('데이터 조회에 실패했습니다.');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 관리자 등록
|
* 관리자 등록
|
||||||
*/
|
*/
|
||||||
|
|
@ -111,25 +130,6 @@
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* 전체 조회
|
|
||||||
*/
|
|
||||||
document.getElementById('btnFindAll').addEventListener(('click'), function () {
|
|
||||||
const requestUri = /*[[@{/admin/management/list}]]*/ '';
|
|
||||||
Reqhelper.reqGetJson(requestUri, (res) => {
|
|
||||||
const selAdmin = document.getElementById('selAdmin');
|
|
||||||
selAdmin.innerHTML = '<option value="">선택</option>';
|
|
||||||
res.forEach(item => {
|
|
||||||
const option = document.createElement('option');
|
|
||||||
option.value = item.id;
|
|
||||||
option.text = item.loginId;
|
|
||||||
selAdmin.appendChild(option);
|
|
||||||
});
|
|
||||||
}, () => {
|
|
||||||
PageHelper.showErrorModal('데이터 조회에 실패했습니다.');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('selAdmin').addEventListener(('change'), function () {
|
document.getElementById('selAdmin').addEventListener(('change'), function () {
|
||||||
document.getElementById('iptUpdateId').value = this.value;
|
document.getElementById('iptUpdateId').value = this.value;
|
||||||
document.getElementById('iptUpdateLoginId').value = this.options[this.selectedIndex].text;
|
document.getElementById('iptUpdateLoginId').value = this.options[this.selectedIndex].text;
|
||||||
|
|
@ -153,8 +153,7 @@
|
||||||
Reqhelper.reqPostJson(requestUri, data, () => {
|
Reqhelper.reqPostJson(requestUri, data, () => {
|
||||||
PageHelper.showAlertModal({title: '수정 완료', message: '수정이 완료되었습니다.'});
|
PageHelper.showAlertModal({title: '수정 완료', message: '수정이 완료되었습니다.'});
|
||||||
}, () => {
|
}, () => {
|
||||||
// PageHelper.showErrorModal('수정에 실패했습니다.');
|
PageHelper.showErrorModal('수정에 실패했습니다.');
|
||||||
alert('수정에 실패했습니다.');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue