크로스플랫폼 이미지 인증코드 생성: SkiaSharp 기반의 고도화된 보안 기법

기초부터 시작하는 C# 이미지 인증코드 구현

웹 애플리케이션에서 인증코드(CAPTCHA)는 자동 공격과 봇 행위를 방지하는 핵심 보안 수단입니다. 특히 로그인, 회원가입, 댓글 작성 등 민감한 작업 시점에 필수적으로 적용됩니다. .NET 생태계에서 주력 언어인 C#은 이미 강력한 이미지 처리 기능을 갖추고 있으며, 오픈소스 라이브러리와 크로스플랫폼 프레임워크를 활용해 윈도우, 리눅스, 맥에서 동일한 품질의 인증코드를 생성할 수 있습니다.

본 문서에서는 SkiaSharp만을 사용하여 그래픽 요소, 노이즈, 왜곡 효과 등을 포함한 고급 이미지 인증코드를 구현하는 전 과정을 단계별로 설명합니다.

1. 인증 코드의 본질: 단순히 ‘보이는 것’을 넘어서

인증코드의 작동 원리는 다음과 같습니다:

  1. 생성: 사용자 요청 시 서버가 인증 정보를 포함한 이미지를 생성하고 전송.
  2. 해석: 사용자는 해당 이미지를 인식하고 정답을 추출. 인간은 빠르게 이해하지만, 자동화 도구는 어려움.
  3. 검증: 사용자가 제출한 정보를 서버가 검증하며, 기본 정보와 사용자 행동 패턴(클릭, 드래그 등)을 함께 분석.

이 과정에서 중요한 것은 문자, 이미지, 음성 등의 형식으로 숨겨진 정보를 제공하면서도, 머신러닝이나 OCR 도구가 쉽게 해독하지 못하도록 하는 것입니다. 특히 이미지 인증의 경우, 다음 두 가지 접근 방식을 통해 정확도를 높입니다:

  • OCR 저항성 강화: 글자나 형태의 구분을 어렵게 만듦.
  • 행동 분석: 마우스 움직임, 클릭 패턴, 스크롤 속도 등을 수집해 비정상적인 동작을 탐지.

이번 실습에서는 OCR 대응 능력 향상에 초점을 맞춰, 다음 기능들을 구현합니다: 도형 그리기, 간섭 요소 추가, 왜곡 필터 적용, 구멍 만들기.

2. 도형 및 텍스트 렌더링

SkiaSharp를 사용하면 이미지 조작, 글꼴 렌더링, 쉐이더 컴파일 등 다양한 기능을 제공합니다. 이 문서에서는 3.119.1 버전을 기준으로 하되, 새 버전에서도 호환되는 방식을 사용합니다.

2.1 초기 설정

비트맵과 캔버스를 생성하고 배경을 흰색으로 설정합니다.

using var bitmap = new SKBitmap(width, height);
using var canvas = new SKCanvas(bitmap);
canvas.Clear(SKColors.White);

2.2 도형 그리기 예시 (오리)

간단한 도형을 생성하여 인증 코드에 활용할 수 있는 예제입니다.

private static void DrawDuck(SKCanvas canvas)
{
    using var stroke = new SKPaint
    {
        Color = SKColors.DarkRed,
        StrokeWidth = 3,
        IsAntialias = true,
        Style = SKPaintStyle.Stroke
    };

    float cx = canvas.LocalClipBounds.MidX;
    float cy = canvas.LocalClipBounds.MidY;

    // 몸통
    canvas.DrawCircle(cx, cy, 35, stroke);

    // 머리
    canvas.DrawCircle(cx + 25, cy - 20, 20, stroke);

    // 부리
    canvas.DrawLine(cx + 45, cy - 20, cx + 60, cy - 18, stroke);
    canvas.DrawLine(cx + 45, cy - 20, cx + 60, cy - 22, stroke);

    // 눈
    using var dot = new SKPaint { Color = SKColors.Black, IsAntialias = true };
    canvas.DrawCircle(cx + 30, cy - 25, 2.5f, dot);

    // 꼬리
    canvas.DrawLine(cx - 35, cy - 5, cx - 45, cy + 5, stroke);
}

2.3 텍스트 그리기

문자열을 중심에 배치하고, 폰트 크기와 위치를 계산하여 출력합니다.

private static void DrawText(SKCanvas canvas, string text)
{
    using var paint = new SKPaint
    {
        Color = SKColors.DarkRed,
        IsAntialias = true
    };

    var fontTypeface = SKFontManager.Default.MatchFamily("Microsoft YaHei", SKFontStyle.Normal);
    using var font = new SKFont(fontTypeface, canvas.LocalClipBounds.Height * 0.4f);

    var clip = canvas.LocalClipBounds;
    float totalWidth = font.MeasureText(text, paint);
    float x = clip.MidX - totalWidth / 2;
    float y = clip.MidY + font.Metrics.CapHeight / 2;

    canvas.DrawText(text, x, y, font, paint);
}

3. 간섭 요소 추가

인증코드의 정교함을 높이기 위해 세 가지 간섭 요소를 추가합니다.

3.1 잡음 텍스처 생성

배경에 백색 잡음을 추가하고, 스캔 라인 효과를 더해 오래된 CRT 모니터처럼 보이도록 합니다.

private static void AddNoiseTexture(SKCanvas canvas)
{
    var clip = canvas.LocalClipBounds;
    int w = (int)clip.Width;
    int h = (int)clip.Height;

    using var noiseBmp = new SKBitmap(w, h, SKColorType.Rgba8888, SKAlphaType.Opaque);
    var rand = new Random();

    for (int y = 0; y < h; y++)
        for (int x = 0; x < w; x++)
            noiseBmp.SetPixel(x, y, new SKColor((byte)rand.Next(230, 255)));

    using var scanPaint = new SKPaint
    {
        Color = SKColors.White.WithAlpha(30),
        Style = SKPaintStyle.Fill,
        IsAntialias = true
    };

    for (int i = 0; i < 3; i++)
    {
        var path = new SKPath();
        float y0 = i * h / 3f;
        path.MoveTo(0, y0);
        path.LineTo(w, y0 + 80);
        path.LineTo(w, y0 + 100);
        path.LineTo(0, y0 + 20);
        path.Close();
        canvas.DrawPath(path, scanPaint);
    }

    using var texturePaint = new SKPaint { FilterQuality = SKFilterQuality.None };
    using var texture = SKImage.FromBitmap(noiseBmp);
    canvas.DrawImage(texture, 0, 0, texturePaint);
}

3.2 잡선 및 점 추가

무작위로 선과 점을 그려 인식을 어렵게 만듭니다.

// 잡선
using var linePaint = new SKPaint
{
    Color = new SKColor(0, 0, 0, 90),
    StrokeWidth = 1,
    IsAntialias = true
};
for (int i = 0; i < 6; i++)
{
    var p1 = new SKPoint(rand.Next(width), rand.Next(height));
    var p2 = new SKPoint(rand.Next(width), rand.Next(height));
    canvas.DrawLine(p1, p2, linePaint);
}

// 잡점
using var pointPaint = new SKPaint { Color = new SKColor(0, 0, 0, 120) };
for (int i = 0; i < width * height / 150; i++)
    canvas.DrawPoint(rand.Next(width), rand.Next(height), pointPaint);

4. 왜곡 필터 적용

기존 이미지가 쉽게 인식될 수 있다면, 왜곡을 통해 난이도를 높입니다.

4.1 문자 무작위 회전

각 문자를 개별적으로 회전하여 인식을 어렵게 합니다.

foreach (var c in text)
{
    float charWidth = font.MeasureText(c.ToString(), textPaint);
    canvas.Save();

    canvas.Translate(x + charWidth / 2, y);
    canvas.RotateDegrees(rand.NextSingle() * 30 - 15);
    canvas.Translate(-charWidth / 2, 0);

    canvas.DrawText(c.ToString(), 0, 0, font, textPaint);
    canvas.Restore();

    x += charWidth;
}

4.2 파동 왜곡 (GLSL 쉐이더 기반)

정사각형에서 반복되는 파동 효과를 생성하기 위해 쉐이더를 사용합니다.

public static SKBitmap ApplyWaveDistortion(SKBitmap src, SKPoint center, float waveLength = 30, float amplitude = 12)
{
    using var texture = SKShader.CreateBitmap(src, SKShaderTileMode.Clamp, SKShaderTileMode.Clamp);

    const string glsl = @"
uniform shader texture;
uniform vec2   center;
uniform float  waveLength;
uniform float  amplitude;

half4 main(vec2 coord)
{
    vec2 dt = coord - center;
    float r = length(dt);
    float offset = sin(r / waveLength * 6.2831853) * amplitude;
    vec2 dir = (r > 0.0) ? dt / r : vec2(0);
    vec2 uv = coord + dir * offset;
    return texture.eval(uv);
}";

    using var effect = SKRuntimeEffect.CreateShader(glsl, out var err);
    if (effect == null) throw new Exception($"Shading error: {err}");

    var uniforms = new SKRuntimeEffectUniforms(effect)
    {
        ["center"] = new[] { center.X, center.Y },
        ["waveLength"] = waveLength,
        ["amplitude"] = amplitude
    };

    var children = new SKRuntimeEffectChildren(effect)
    {
        ["texture"] = texture
    };

    var info = new SKImageInfo(src.Width, src.Height);
    using var surface = SKSurface.Create(info);
    using var paint = new SKPaint();
    paint.Shader = effect.ToShader(uniforms, children);
    surface.Canvas.DrawRect(info.Rect, paint);
    surface.Canvas.Flush();

    return SKBitmap.FromImage(surface.Snapshot());
}

5. 구멍 만들기 (홀딩 기법)

사용자가 특정 영역을 맞추는 상호작용을 유도하기 위해 구멍을 만들어냅니다. 이는 테두리와 내부를 별도로 그리며 혼합 모드를 사용합니다.

private static SKBitmap CreateHoleMask(SKBitmap source, int radius, bool includeInner = true)
{
    var result = new SKBitmap(includeInner ? radius * 2 : source.Width, includeInner ? radius * 2 : source.Height, SKColorType.Rgba8888, SKAlphaType.Premul);
    using var canvas = new SKCanvas(result);

    using var circlePaint = new SKPaint { IsAntialias = true, Color = SKColors.White };
    canvas.DrawCircle(includeInner ? radius : source.Width / 2, includeInner ? radius : source.Height / 2, radius, circlePaint);

    if (includeInner)
    {
        using var blendPaint = new SKPaint { BlendMode = SKBlendMode.SrcIn };
        var srcRect = new SKRect(0, 0, source.Width, source.Height);
        var dstRect = new SKRect(-(source.Width / 2 - radius), -(source.Height / 2 - radius), 
                                  -(source.Width / 2 - radius) + source.Width, 
                                  -(source.Height / 2 - radius) + source.Height);
        canvas.DrawBitmap(source, srcRect, dstRect, blendPaint);
    }
    else
    {
        using var blendPaint = new SKPaint { BlendMode = SKBlendMode.SrcOut };
        canvas.DrawBitmap(source, new SKRect(0, 0, source.Width, source.Height), blendPaint);
    }

    return result;
}

결론

이 문서에서는 SkiaSharp 기반으로 크로스플랫폼 이미지 인증코드를 구현하는 전과정을 다룹니다. 도형, 텍스트, 잡음, 왜곡, 구멍 등 다양한 기법을 통합하여, 머신러닝 기반 인식 공격에도 강건한 인증 시스템을 설계할 수 있습니다.

실제 적용 시에는 이러한 기법들을 조합하거나, 사용자 행동 데이터와 연계하여 더욱 정교한 인증 메커니즘을 구성할 수 있습니다.

태그: SkiaSharp C# Image Processing captcha cross-platform

6월 18일 23:44에 게시됨