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)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "login_id", length = 100, nullable = false)
|
||||
@Column(name = "login_id", length = 100, nullable = false, unique = true)
|
||||
private String loginId;
|
||||
|
||||
@Column(name = "password", length = 255, nullable = false)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
package com.bpgroup.poc.admin.domain.admin.entity;
|
||||
|
||||
import com.bpgroup.poc.admin.common.SubStringHelper;
|
||||
import com.bpgroup.poc.admin.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AccessLevel;
|
||||
|
|
@ -23,7 +24,7 @@ public class AdminActionLog extends BaseEntity {
|
|||
@Column(name = "request_value")
|
||||
private String requestValue;
|
||||
|
||||
@Column(name = "response_value")
|
||||
@Column(name = "response_value", length = 1000)
|
||||
private String responseValue;
|
||||
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
|
|
@ -39,7 +40,7 @@ public class AdminActionLog extends BaseEntity {
|
|||
|
||||
private AdminActionLog(String sessionId, String responseValue) {
|
||||
this.sessionId = sessionId;
|
||||
this.responseValue = responseValue;
|
||||
this.responseValue = SubStringHelper.substringInBytes(responseValue, 1000);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
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.validation.annotation.Validated;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
@Validated
|
||||
|
|
@ -26,8 +28,11 @@ public class AdminActionLogService {
|
|||
public void update(
|
||||
@NotNull @Valid AdminActionLogUpdateCommand command
|
||||
) {
|
||||
AdminActionLog findAdminActionLog = adminActionLogRepository.findBySessionId(command.getSessionId()).orElseThrow(() -> new IllegalArgumentException("Not found admin action log"));
|
||||
findAdminActionLog.update(command.toEntity());
|
||||
Optional<AdminActionLog> findAdminActionLog = adminActionLogRepository.findBySessionId(command.getSessionId());
|
||||
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) {
|
||||
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"));
|
||||
|
||||
if (findAdminToken.isNotExpired(LocalDateTime.now())) {
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@ public class LoggingFilter extends OncePerRequestFilter {
|
|||
private final AdminActionLogAppService adminActionLogAppService;
|
||||
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"};
|
||||
|
||||
@Override
|
||||
|
|
|
|||
|
|
@ -1,11 +1,10 @@
|
|||
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.authorization.AuthorizationAppService;
|
||||
import com.bpgroup.poc.admin.security.authentication.*;
|
||||
import com.bpgroup.poc.admin.security.authorization.CustomAccessDeniedHandler;
|
||||
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.JwtTokenProvider;
|
||||
import com.bpgroup.poc.admin.security.jwt.JwtTokenValidateFilter;
|
||||
|
|
@ -38,7 +37,6 @@ public class SecurityConfig {
|
|||
|
||||
private final AuthenticationAppService authenticationAppService;
|
||||
private final AuthorizationAppService authorizationAppService;
|
||||
private final JwtTokenAppService jwtTokenAppService;
|
||||
|
||||
private final JwtTokenProvider jwtTokenProvider;
|
||||
|
||||
|
|
@ -70,7 +68,7 @@ public class SecurityConfig {
|
|||
private void configureAuthorization(HttpSecurity http) throws Exception {
|
||||
http.authorizeHttpRequests(c -> c
|
||||
.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()
|
||||
.anyRequest()
|
||||
.access(new CustomAuthorizationManager(authorizationAppService))
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ import java.util.stream.Stream;
|
|||
public class JwtTokenValidateFilter extends OncePerRequestFilter {
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
title: '오류',
|
||||
message: message,
|
||||
}, {
|
||||
cancel: cancelFunction
|
||||
});
|
||||
alert(message);
|
||||
|
||||
// TODO: 퍼블리싱 추가 후 주석 해제 및 작업 필요
|
||||
// this.showAlertModal({
|
||||
// title: '오류',
|
||||
// message: message,
|
||||
// }, {
|
||||
// cancel: cancelFunction
|
||||
// });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
@ -98,17 +101,20 @@ const PageHelper = {
|
|||
callbacks = {};
|
||||
}
|
||||
|
||||
this.loadAndApplyPage({
|
||||
url: `/common/modal/alert`,
|
||||
method: 'POST',
|
||||
data: `title=${params.title}&message=${params.message}`,
|
||||
contentSelector: 'div[data-alert-modal="body"]',
|
||||
appendToSelector: 'body'
|
||||
}, {
|
||||
success: function () {
|
||||
EventRouter.register('clickAlertModalCancelButton', callbacks.cancel);
|
||||
},
|
||||
});
|
||||
alert(params.message);
|
||||
|
||||
// TODO: 퍼블리싱 추가 후 주석 해제 및 작업 필요
|
||||
// this.loadAndApplyPage({
|
||||
// url: `/common/modal/alert`,
|
||||
// method: 'POST',
|
||||
// data: `title=${params.title}&message=${params.message}`,
|
||||
// contentSelector: 'div[data-alert-modal="body"]',
|
||||
// appendToSelector: 'body'
|
||||
// }, {
|
||||
// success: function () {
|
||||
// EventRouter.register('clickAlertModalCancelButton', callbacks.cancel);
|
||||
// },
|
||||
// });
|
||||
},
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ const Reqhelper = {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
refreshCsrf();
|
||||
if (eFunc) {
|
||||
eFunc(error);
|
||||
}
|
||||
|
|
@ -65,6 +66,7 @@ const Reqhelper = {
|
|||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
refreshCsrf();
|
||||
if (eFunc) {
|
||||
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('iptUpdateId').value = this.value;
|
||||
document.getElementById('iptUpdateLoginId').value = this.options[this.selectedIndex].text;
|
||||
|
|
@ -153,8 +153,7 @@
|
|||
Reqhelper.reqPostJson(requestUri, data, () => {
|
||||
PageHelper.showAlertModal({title: '수정 완료', message: '수정이 완료되었습니다.'});
|
||||
}, () => {
|
||||
// PageHelper.showErrorModal('수정에 실패했습니다.');
|
||||
alert('수정에 실패했습니다.');
|
||||
PageHelper.showErrorModal('수정에 실패했습니다.');
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue