258 lines
9.0 KiB
Markdown
258 lines
9.0 KiB
Markdown
# 설계서: VPD 백오피스 read-only 전환 문의 진단 및 세션 격리 개선 (#683)
|
|
|
|
> **상태**: Investigation documented · implementation pending
|
|
> **작성**: [AI] Architect · **최종수정**: 2026-07-19
|
|
> **추적성** — Redmine: #683 · 관련 ADR: 없음
|
|
> · 진단 파일: `SecurityModelAdvice.java`, `SecurityConfig.java`, `application.yml`, `static/js/app.js`, `/home/opc/apps/vpd-backoffice/app.log`
|
|
> · 확인 명령: `status.sh`, `curl /permissions`, 앱 로그 grep
|
|
|
|
## 1. 목적
|
|
|
|
VPD 백오피스에서 “권한 화면을 열어 둔 상태에서 갑자기 read-only로 다시 변경된다”는 문의를 조사하고, 현재 확인된 증거와 후속 개선 항목을 분리한다.
|
|
|
|
이번 문서는 장애 확정 보고서가 아니라, 2026-07-16 운영 로그와 2026-07-19 소스 확인을 기준으로 한 현행 진단 기록이다.
|
|
|
|
## 2. 현재까지 확인한 사실
|
|
|
|
### 2.1 운영 프로세스
|
|
|
|
운영 앱 상태:
|
|
|
|
```text
|
|
/home/opc/apps/vpd-backoffice/status.sh
|
|
running pid=2679131
|
|
```
|
|
|
|
앱 로그 기준 기동 시각:
|
|
|
|
```text
|
|
2026-07-13T16:30:11.805+09:00 Tomcat started on port 8082
|
|
2026-07-13T16:30:11.818+09:00 Started VpdBackofficeApplication
|
|
```
|
|
|
|
2026-07-16 문의 시점 전후에 앱 재기동 흔적은 확인되지 않았다. 따라서 “서버 재기동으로 admin session이 날아갔다”는 설명은 현재 증거만으로는 낮다.
|
|
|
|
### 2.2 read-only 판정 로직
|
|
|
|
서버는 모든 화면 모델에 `canMutate`, `readOnlyMode`를 넣는다.
|
|
|
|
```java
|
|
return authentication.getAuthorities().stream()
|
|
.map(GrantedAuthority::getAuthority)
|
|
.anyMatch("ROLE_ADMIN"::equals);
|
|
```
|
|
|
|
```java
|
|
return authentication != null && authentication.isAuthenticated() && !canMutate();
|
|
```
|
|
|
|
즉 현재 구현에서 read-only는 별도 DB 권한 상태가 아니라 Spring Security 인증 주체가 `ROLE_ADMIN`인지 여부로 결정된다.
|
|
|
|
| 인증 상태 | 서버 모델 |
|
|
|---|---|
|
|
| `ROLE_ADMIN` | `canMutate=true`, `readOnlyMode=false` |
|
|
| 인증됨, `ROLE_ADMIN` 없음 | `canMutate=false`, `readOnlyMode=true` |
|
|
| 미인증 | `/permissions`는 401 또는 로그인 유도 |
|
|
|
|
브라우저에서 실제 폼을 비활성화하는 코드는 `static/js/app.js`가 `<meta name="backoffice-read-only">` 값을 읽어 수행한다.
|
|
|
|
```javascript
|
|
function isReadOnlyMode() {
|
|
return document.querySelector('meta[name="backoffice-read-only"]')?.content === 'true';
|
|
}
|
|
```
|
|
|
|
### 2.3 admin 직접 조회 결과
|
|
|
|
운영 앱에 admin 인증으로 `/permissions`를 직접 조회했을 때 다음 meta가 확인됐다.
|
|
|
|
```html
|
|
<meta name="backoffice-can-mutate" content="true">
|
|
<meta name="backoffice-read-only" content="false">
|
|
```
|
|
|
|
따라서 확인 시점 기준 서버가 admin을 read-only로 잘못 판단하는 상태는 아니었다.
|
|
|
|
### 2.4 앱 로그의 권한 거부 증거
|
|
|
|
운영 앱 로그에서 2026-07-16 16:09:35에 `AccessDeniedException`이 확인됐다.
|
|
|
|
```text
|
|
2026-07-16T16:09:35.826+09:00 ... AccessDeniedException: Access Denied
|
|
```
|
|
|
|
같은 시각 다음 예외도 함께 기록됐다.
|
|
|
|
```text
|
|
2026-07-16T16:09:35.825+09:00 ... IllegalStateException:
|
|
getOutputStream() has already been called for this response
|
|
```
|
|
|
|
해석:
|
|
|
|
- 변경성 요청이 `ROLE_ADMIN` 없이 들어온 흔적은 있다.
|
|
- 현재 로그에는 요청 path, principal, authorities가 남지 않는다.
|
|
- 따라서 어떤 URL에서 어떤 사용자로 거부됐는지는 사후 식별할 수 없다.
|
|
|
|
## 3. 원인 후보
|
|
|
|
현재 증거 기준으로 우선순위는 다음과 같다.
|
|
|
|
| 후보 | 가능성 | 근거 |
|
|
|---|---:|---|
|
|
| 브라우저 세션의 인증 주체가 admin이 아닌 사용자로 바뀜 | 높음 | read-only는 `ROLE_ADMIN` 부재 시 켜지고, `AccessDeniedException`이 실제 발생 |
|
|
| 같은 브라우저의 다른 탭/도구가 다른 인증 정보를 보냄 | 중간 | VPD 앱은 form login과 HTTP Basic이 모두 활성 |
|
|
| 세션 쿠키 충돌 | 중간 | VPD 앱은 기본 `JSESSIONID`를 사용. DDS는 `DDS_BACKOFFICE_SESSION`으로 분리되어 있음 |
|
|
| 서버 재기동으로 세션 만료 | 낮음 | 앱 로그상 문의 전후 재기동 흔적 없음 |
|
|
| DB 권한/VPD 설정이 화면 권한을 read-only로 바꿈 | 낮음 | 화면 read-only는 DB VPD 권한이 아니라 Spring Security role 기준 |
|
|
|
|
## 4. 현재 구조의 취약점
|
|
|
|
### 4.1 VPD 세션 쿠키명이 기본값이다
|
|
|
|
VPD 백오피스의 `application.yml`은 세션 쿠키 이름을 지정하지 않는다.
|
|
|
|
```yaml
|
|
server:
|
|
servlet:
|
|
session:
|
|
cookie:
|
|
secure: ${BACKOFFICE_SESSION_COOKIE_SECURE:false}
|
|
http-only: true
|
|
same-site: lax
|
|
```
|
|
|
|
Spring Boot 기본값인 `JSESSIONID`를 사용하면 같은 host/path에서 다른 Java 웹앱과 섞일 여지가 있다. DDS 백오피스는 이미 아래처럼 별도 쿠키명을 사용한다.
|
|
|
|
```yaml
|
|
server:
|
|
servlet:
|
|
session:
|
|
cookie:
|
|
name: DDS_BACKOFFICE_SESSION
|
|
```
|
|
|
|
### 4.2 HTTP Basic과 form login이 동시에 켜져 있다
|
|
|
|
VPD 백오피스는 브라우저 화면에 form login을 쓰면서 HTTP Basic도 허용한다.
|
|
|
|
```java
|
|
.httpBasic(basic -> {
|
|
})
|
|
.formLogin(login -> login
|
|
.loginPage("/login")
|
|
.permitAll())
|
|
```
|
|
|
|
브라우저나 테스트 도구가 Basic 인증 캐시를 유지하면, 사용자가 form login으로 보는 세션과 요청에 붙는 인증 header가 달라질 수 있다.
|
|
|
|
### 4.3 권한 거부 로그가 운영 진단에 부족하다
|
|
|
|
현재 `AccessDeniedException` stack trace는 남지만 다음 정보가 없다.
|
|
|
|
- 요청 method/path
|
|
- authenticated principal
|
|
- authorities
|
|
- 세션 ID fingerprint
|
|
- referer/user-agent 요약
|
|
|
|
이 정보가 없으면 “갑자기 read-only로 보였다”는 사용자 체감과 실제 요청을 연결하기 어렵다.
|
|
|
|
## 5. 권장 개선안
|
|
|
|
### 5.1 즉시 개선
|
|
|
|
1. VPD 전용 세션 쿠키명을 지정한다.
|
|
|
|
```yaml
|
|
server:
|
|
servlet:
|
|
session:
|
|
cookie:
|
|
name: VPD_BACKOFFICE_SESSION
|
|
```
|
|
|
|
2. `AccessDeniedHandler`를 추가해 권한 거부 시 최소 진단 로그를 남긴다.
|
|
|
|
기록할 값:
|
|
|
|
- `method`
|
|
- `requestURI`
|
|
- `principal`
|
|
- `authorities`
|
|
- `remoteAddr`
|
|
- `sessionIdHash`
|
|
|
|
원문 쿠키, 비밀번호, Bearer token은 기록하지 않는다.
|
|
|
|
3. 화면 상단에 현재 로그인 주체와 권한 모드를 표시한다.
|
|
|
|
예:
|
|
|
|
```text
|
|
로그인: admin · 수정 가능
|
|
로그인: guest · 읽기 전용
|
|
```
|
|
|
|
### 5.2 후속 개선
|
|
|
|
1. 브라우저 화면용 security chain에서는 HTTP Basic을 끄고 form login만 사용한다.
|
|
2. MCP/내부 점검용 Basic 인증이 필요하면 `/mcp-client-demo`의 server-side call이나 전용 actuator성 경로로 격리한다.
|
|
3. read-only로 렌더링될 때 “왜 read-only인가”를 표시한다.
|
|
|
|
예:
|
|
|
|
```text
|
|
현재 계정에 ROLE_ADMIN이 없어 변경 버튼이 비활성화되었습니다.
|
|
다른 탭에서 guest로 로그인했거나 세션이 바뀐 경우 다시 admin으로 로그인하세요.
|
|
```
|
|
|
|
## 6. 인수조건
|
|
|
|
- [ ] admin으로 `/permissions`를 조회하면 `backoffice-read-only=false`이고 저장/삭제 폼이 활성이다.
|
|
- [ ] viewer/guest로 `/permissions`를 조회하면 `backoffice-read-only=true`이고 변경 폼은 비활성이다.
|
|
- [ ] VPD와 DDS 백오피스를 같은 브라우저에서 번갈아 열어도 세션 쿠키가 충돌하지 않는다.
|
|
- [ ] 권한 없는 POST/PUT/PATCH/DELETE 요청은 403으로 끝나며, 로그에 method/path/principal/authorities가 남는다.
|
|
- [ ] 로그에 raw cookie, password, Bearer token은 남지 않는다.
|
|
- [ ] read-only 배너가 현재 사용자와 권한 모드를 명확히 설명한다.
|
|
|
|
## 7. 검증 계획
|
|
|
|
1. 단위/통합 테스트
|
|
- `SecurityConfig`에서 VPD 세션 쿠키명이 `VPD_BACKOFFICE_SESSION`인지 확인
|
|
- admin/viewer 각각에 대해 `readOnlyMode` model 값 검증
|
|
- 권한 없는 POST가 403이며 진단 로그 handler를 타는지 검증
|
|
|
|
2. 로컬 HTTP 확인
|
|
|
|
```bash
|
|
curl -i -u "$BACKOFFICE_ADMIN_USER:$BACKOFFICE_ADMIN_PASSWORD" \
|
|
http://127.0.0.1:8082/permissions
|
|
```
|
|
|
|
기대:
|
|
|
|
```text
|
|
backoffice-can-mutate=true
|
|
backoffice-read-only=false
|
|
```
|
|
|
|
3. 운영 확인
|
|
- admin 로그인 후 `/permissions`에서 저장 버튼 활성 확인
|
|
- guest/viewer 로그인 후 read-only 배너와 비활성 폼 확인
|
|
- 같은 브라우저에서 DDS 화면과 VPD 화면을 번갈아 열어 session 유지 확인
|
|
- 권한 없는 POST를 1회 발생시키고 로그에 path/principal이 남는지 확인
|
|
|
|
## 8. 현재 결론
|
|
|
|
현재까지 확인한 증거만으로는 “admin 권한이 서버에서 갑자기 read-only로 바뀌는 기능 오류”라고 단정할 수 없다.
|
|
|
|
확인된 것은 다음이다.
|
|
|
|
- admin 직접 조회는 수정 가능 상태로 정상이다.
|
|
- 문의 시점 근처에 `AccessDeniedException`이 있다.
|
|
- 현재 로그는 어떤 사용자/URL에서 발생했는지 식별할 수 없다.
|
|
- VPD 세션 쿠키가 기본 `JSESSIONID`라 운영 혼선을 줄이기 위해 고유 쿠키명으로 분리하는 것이 맞다.
|
|
|
|
따라서 다음 작업은 기능 수정이 아니라 **세션 격리와 진단 로그 보강**을 먼저 적용해, 동일 현상이 재발할 때 원인을 확정할 수 있게 만드는 것이다.
|