페이지 개선2

This commit is contained in:
pandoli365 2026-06-13 15:05:18 +09:00
parent d88cfbf5fb
commit 40bde12c6d
2 changed files with 111 additions and 0 deletions

View File

@ -0,0 +1,54 @@
-- Security hardening DDL for environments that already have the base tables.
-- Review duplicate rows before adding the unique constraint.
-- Find duplicate login identities before enforcing uniqueness.
SELECT
"provider",
"provider_user_id",
COUNT(*) AS duplicate_count
FROM "user_auth_identities"
WHERE "is_delete" IS NOT TRUE
GROUP BY "provider", "provider_user_id"
HAVING COUNT(*) > 1;
-- Enforce one active identity per provider account.
CREATE UNIQUE INDEX IF NOT EXISTS "ux_user_auth_identities_provider_user_id_active"
ON "user_auth_identities" ("provider", "provider_user_id")
WHERE "is_delete" IS NOT TRUE;
-- Add missing recruit_posts constraints safely if the base DDL was applied before it became idempotent.
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'recruit_posts_user_id_fkey'
) THEN
ALTER TABLE "recruit_posts"
ADD CONSTRAINT "recruit_posts_user_id_fkey"
FOREIGN KEY ("user_id") REFERENCES "users" ("id");
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'recruit_posts_role_check'
) THEN
ALTER TABLE "recruit_posts"
ADD CONSTRAINT "recruit_posts_role_check"
CHECK ("role" IN ('기획', '아트', '프로그래머'));
END IF;
END
$$;
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'recruit_posts_participation_type_check'
) THEN
ALTER TABLE "recruit_posts"
ADD CONSTRAINT "recruit_posts_participation_type_check"
CHECK ("participation_type" IN ('취미', '수익쉐어', '유급', '게임잼'));
END IF;
END
$$;

View File

@ -0,0 +1,57 @@
package com.pandoli365.bibimbap.security;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Map;
public final class CsrfTokens {
public static final String SESSION_ATTRIBUTE = "csrfToken";
public static final String HEADER_NAME = "X-CSRF-Token";
private static final int TOKEN_BYTES = 32;
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
private CsrfTokens() {
}
public static String getOrCreate(HttpSession session) {
if (session == null) {
return "";
}
Object existing = session.getAttribute(SESSION_ATTRIBUTE);
if (existing instanceof String token && !token.isBlank()) {
return token;
}
byte[] bytes = new byte[TOKEN_BYTES];
SECURE_RANDOM.nextBytes(bytes);
String token = Base64.getUrlEncoder().withoutPadding().encodeToString(bytes);
session.setAttribute(SESSION_ATTRIBUTE, token);
return token;
}
public static boolean isValid(HttpServletRequest request) {
HttpSession session = request == null ? null : request.getSession(false);
if (session == null) {
return false;
}
Object expected = session.getAttribute(SESSION_ATTRIBUTE);
if (!(expected instanceof String expectedToken) || expectedToken.isBlank()) {
return false;
}
String provided = request.getHeader(HEADER_NAME);
if (provided == null || provided.isBlank()) {
provided = request.getParameter("_csrf");
}
return expectedToken.equals(provided);
}
public static Map<String, Object> errorBody() {
return Map.of(
"status", 403,
"message", "요청 보안 토큰이 유효하지 않습니다."
);
}
}