admin: server exception 발생 시 stateless 로 인한 csrf 재발급 처리

This commit is contained in:
geonhos 2024-05-24 15:58:15 +09:00
parent 9370aaa1df
commit c140f8fffb
14 changed files with 129 additions and 52 deletions

View File

@ -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;
}
}

View File

@ -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)

View File

@ -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) {

View File

@ -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);
} }

View File

@ -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());
}
} }
} }

View File

@ -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())) {

View File

@ -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

View File

@ -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))

View File

@ -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

View File

@ -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()));
}
}

View File

@ -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);
}
}

View File

@ -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);
// },
// });
}, },
/** /**

View File

@ -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);
});
});
} }

View File

@ -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('수정에 실패했습니다.');
}); });
}); });