기초부터 시작하는 C# 이미지 인증코드 구현
웹 애플리케이션에서 인증코드(CAPTCHA)는 자동 공격과 봇 행위를 방지하는 핵심 보안 수단입니다. 특히 로그인, 회원가입, 댓글 작성 등 민감한 작업 시점에 필수적으로 적용됩니다. .NET 생태계에서 주력 언어인 C#은 이미 강력한 이미지 처리 기능을 갖추고 있으며, 오픈소스 라이브러리와 크로스플랫폼 프레임워크를 활용해 윈도우, 리눅스, 맥에서 동일한 품질의 인증코드를 생성할 수 있습니다.
본 문서에서는 SkiaSharp만을 사용하여 그래픽 요소, 노이즈, 왜곡 효과 등을 포함한 고급 이미지 인증코드를 구현하는 전 과정을 단계별로 설명합니다.
1. 인증 코드의 본질: 단순히 ‘보이는 것’을 넘어서
인증코드의 작동 원리는 다음과 같습니다:
- 생성: 사용자 요청 시 서버가 인증 정보를 포함한 이미지를 생성하고 전송.
- 해석: 사용자는 해당 이미지를 인식하고 정답을 추출. 인간은 빠르게 이해하지만, 자동화 도구는 어려움.
- 검증: 사용자가 제출한 정보를 서버가 검증하며, 기본 정보와 사용자 행동 패턴(클릭, 드래그 등)을 함께 분석.
이 과정에서 중요한 것은 문자, 이미지, 음성 등의 형식으로 숨겨진 정보를 제공하면서도, 머신러닝이나 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 기반으로 크로스플랫폼 이미지 인증코드를 구현하는 전과정을 다룹니다. 도형, 텍스트, 잡음, 왜곡, 구멍 등 다양한 기법을 통합하여, 머신러닝 기반 인식 공격에도 강건한 인증 시스템을 설계할 수 있습니다.
실제 적용 시에는 이러한 기법들을 조합하거나, 사용자 행동 데이터와 연계하여 더욱 정교한 인증 메커니즘을 구성할 수 있습니다.