feat(backoffice): authenticate Smilegate tool users from ADB

This commit is contained in:
devmrko
2026-07-22 13:11:42 +09:00
parent 323f746d8a
commit 79ed5a158e
3 changed files with 119 additions and 20 deletions

View File

@@ -0,0 +1,30 @@
# 설계서: 스마일게이트 PoC 도구 사용자
## 프로젝트 개요
스마일게이트 PoC는 게임 플레이어 데이터와 운영·분석 도구 사용자를 분리한다. 게임별 사용자 마스터는 분석 대상이며, 백오피스 로그인 계정으로 사용하지 않는다.
## 목표
ADB의 `ADMIN.SG_TOOL_USER`를 백오피스 로그인 원본으로 사용한다. 가상 `sg-teamlead``sg-member`를 만들고, 팀장만 변경 권한을 가진다.
## 데이터 모델
| 컬럼 | 설명 |
|---|---|
| `USER_ID` | 로그인 식별자, PK |
| `USER_NM` | 화면용 이름 |
| `ROLE_CODE` | `TEAM_LEAD` 또는 `TEAM_MEMBER` |
| `PASSWORD_HASH` | Spring Security BCrypt 해시 |
| `ENABLED_YN` | 로그인 허용 여부 |
## 권한 매핑
- `TEAM_LEAD``ROLE_ADMIN`: 설정 변경 가능
- `TEAM_MEMBER``ROLE_VIEWER`: 조회만 가능
## 검증 기준
- 가상 두 사용자가 `SG_TOOL_USER`에 존재한다.
- `sg-teamlead` 로그인은 백오피스 메인으로 이동한다.
- `sg-member`는 인증되지만 변경 HTTP 요청은 거부된다.

View File

@@ -0,0 +1,48 @@
-- Smilegate PoC backoffice users. These are tool operators, not game players.
declare
table_missing exception;
pragma exception_init(table_missing, -955);
begin
execute immediate q'[
create table sg_tool_user (
user_id varchar2(100) not null,
user_nm varchar2(200) not null,
role_code varchar2(30) not null,
password_hash varchar2(200) not null,
enabled_yn char(1) default 'Y' not null,
created_at timestamp default systimestamp not null,
updated_at timestamp default systimestamp not null,
constraint sg_tool_user_pk primary key (user_id),
constraint sg_tool_user_role_ck check (role_code in ('TEAM_LEAD', 'TEAM_MEMBER')),
constraint sg_tool_user_enabled_ck check (enabled_yn in ('Y', 'N'))
)
]';
exception
when table_missing then null;
end;
/
merge into sg_tool_user target
using (
select 'sg-teamlead' as user_id, 'SG Demo Team Lead' as user_nm,
'TEAM_LEAD' as role_code,
'$2y$12$esdNRjiRo3ZxBnUGD1xrjuIFjCeCWcxlksDfdZpeImTsVtIdh/IJe' as password_hash
from dual
union all
select 'sg-member', 'SG Demo Team Member', 'TEAM_MEMBER',
'$2y$12$esdNRjiRo3ZxBnUGD1xrjuIFjCeCWcxlksDfdZpeImTsVtIdh/IJe'
from dual
) source
on (target.user_id = source.user_id)
when matched then update set
target.user_nm = source.user_nm,
target.role_code = source.role_code,
target.password_hash = source.password_hash,
target.enabled_yn = 'Y',
target.updated_at = systimestamp
when not matched then insert (user_id, user_nm, role_code, password_hash, enabled_yn)
values (source.user_id, source.user_nm, source.role_code, source.password_hash, 'Y');
commit;

View File

@@ -1,5 +1,6 @@
package com.cloudhandson.vpdbackoffice.config;
import org.springframework.beans.factory.ObjectProvider;
import java.util.ArrayList;
import java.util.List;
import org.springframework.context.annotation.Bean;
@@ -9,10 +10,12 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.jdbc.core.JdbcTemplate;
@Configuration
public class SecurityConfig {
@@ -75,30 +78,48 @@ public class SecurityConfig {
@Bean
UserDetailsService userDetailsService(
ObjectProvider<JdbcTemplate> jdbcTemplateProvider,
BackofficeProperties properties,
PasswordEncoder passwordEncoder
) {
var security = properties.security();
String encodedPassword = security.adminPasswordHash() == null || security.adminPasswordHash().isBlank()
? passwordEncoder.encode(security.adminPassword())
: security.adminPasswordHash();
List<UserDetails> users = new ArrayList<>();
users.add(User.withUsername(security.adminUser())
// BCrypt encoding includes a random salt. Re-encoding the configured
// password on every startup would invalidate remember-me signatures.
.password(encodedPassword)
.roles("ADMIN")
.build());
if (security.guestConfigured()) {
String encodedGuestPassword = security.guestPasswordHash() == null || security.guestPasswordHash().isBlank()
? passwordEncoder.encode(security.guestPassword())
: security.guestPasswordHash();
users.add(User.withUsername(security.guestUser())
.password(encodedGuestPassword)
.roles("VIEWER")
.build());
JdbcTemplate jdbcTemplate = jdbcTemplateProvider.getIfAvailable();
if (jdbcTemplate == null) {
String encodedPassword = properties.security().adminPasswordHash() == null
|| properties.security().adminPasswordHash().isBlank()
? passwordEncoder.encode(properties.security().adminPassword())
: properties.security().adminPasswordHash();
List<UserDetails> users = new ArrayList<>();
users.add(User.withUsername(properties.security().adminUser())
.password(encodedPassword)
.roles("ADMIN")
.build());
if (properties.security().guestConfigured()) {
String encodedGuestPassword = properties.security().guestPasswordHash() == null
|| properties.security().guestPasswordHash().isBlank()
? passwordEncoder.encode(properties.security().guestPassword())
: properties.security().guestPasswordHash();
users.add(User.withUsername(properties.security().guestUser())
.password(encodedGuestPassword)
.roles("VIEWER")
.build());
}
return new InMemoryUserDetailsManager(users);
}
return new InMemoryUserDetailsManager(users);
return username -> jdbcTemplate.query(
"""
select user_id, password_hash, role_code
from sg_tool_user
where user_id = ?
and enabled_yn = 'Y'
""",
(resultSet, rowNum) -> User.withUsername(resultSet.getString("user_id"))
.password(resultSet.getString("password_hash"))
.roles("TEAM_LEAD".equals(resultSet.getString("role_code")) ? "ADMIN" : "VIEWER")
.build(),
username
).stream().findFirst().orElseThrow(
() -> new UsernameNotFoundException("Unknown or disabled tool user")
);
}
@Bean