페이지 개선2
This commit is contained in:
parent
d88cfbf5fb
commit
40bde12c6d
|
|
@ -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
|
||||||
|
$$;
|
||||||
|
|
@ -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", "요청 보안 토큰이 유효하지 않습니다."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue