From 79ed5a158ee77e939e8cdd1cb66a127455dbe660 Mon Sep 17 00:00:00 2001 From: devmrko Date: Wed, 22 Jul 2026 13:11:42 +0900 Subject: [PATCH] feat(backoffice): authenticate Smilegate tool users from ADB --- docs/design/smilegate-tool-users/README.md | 30 +++++++++ sql/adb/70_sg_tool_user.sql | 48 +++++++++++++++ .../vpdbackoffice/config/SecurityConfig.java | 61 +++++++++++++------ 3 files changed, 119 insertions(+), 20 deletions(-) create mode 100644 docs/design/smilegate-tool-users/README.md create mode 100644 sql/adb/70_sg_tool_user.sql diff --git a/docs/design/smilegate-tool-users/README.md b/docs/design/smilegate-tool-users/README.md new file mode 100644 index 0000000..01b1181 --- /dev/null +++ b/docs/design/smilegate-tool-users/README.md @@ -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 요청은 거부된다. diff --git a/sql/adb/70_sg_tool_user.sql b/sql/adb/70_sg_tool_user.sql new file mode 100644 index 0000000..22b4a4e --- /dev/null +++ b/sql/adb/70_sg_tool_user.sql @@ -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; + diff --git a/src/main/java/com/cloudhandson/vpdbackoffice/config/SecurityConfig.java b/src/main/java/com/cloudhandson/vpdbackoffice/config/SecurityConfig.java index a731c4b..a131aa8 100644 --- a/src/main/java/com/cloudhandson/vpdbackoffice/config/SecurityConfig.java +++ b/src/main/java/com/cloudhandson/vpdbackoffice/config/SecurityConfig.java @@ -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 jdbcTemplateProvider, BackofficeProperties properties, PasswordEncoder passwordEncoder ) { - var security = properties.security(); - String encodedPassword = security.adminPasswordHash() == null || security.adminPasswordHash().isBlank() - ? passwordEncoder.encode(security.adminPassword()) - : security.adminPasswordHash(); - List 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 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