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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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