287 lines
8.0 KiB
Plaintext
287 lines
8.0 KiB
Plaintext
unit UserAuthentification;
|
|
|
|
interface
|
|
|
|
uses
|
|
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
|
|
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Winapi.ShellAPI,
|
|
superobject, IdHTTP, IdSSLOpenSSL, System.Math, System.IOUtils, System.RegularExpressions,
|
|
Tocsg.Safe, Tocsg.COLib, Tocsg.COLib.Encrypt;
|
|
|
|
const
|
|
CODE_AUTH_OK = 100;
|
|
|
|
type
|
|
// 로그인 성공 시 호출될 이벤트 타입 정의
|
|
TNotifyLoginSuccess = procedure(Sender: TObject; ResultCode: Integer) of object;
|
|
|
|
TAuthForm = class(TForm)
|
|
Label1, Label2: TLabel;
|
|
EditID, EditPW: TEdit;
|
|
ButtonLogin: TButton;
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure FromClose(Sender: TObject; var Action: TCloseAction);
|
|
procedure ButtonLoginClick(Sender: TObject);
|
|
|
|
private
|
|
{ Private declarations }
|
|
iFailCount: Integer; // 로그인 실패 횟수
|
|
BlockStartTime: TDateTime; // 차단 시작 시간
|
|
FOnSuccess: TNotifyLoginSuccess; // 이벤트를 담을 변수
|
|
public
|
|
{ Public declarations }
|
|
sAgentId, sDestIPort: string;
|
|
property OnSuccess: TNotifyLoginSuccess read FOnSuccess write FOnSuccess;
|
|
end;
|
|
|
|
var
|
|
AuthForm: TAuthForm;
|
|
|
|
implementation
|
|
|
|
{$R *.dfm}
|
|
|
|
procedure TAuthForm.FormCreate(Sender: TObject);
|
|
begin
|
|
Self.Position := poScreenCenter;
|
|
Self.BorderStyle := bsSingle;
|
|
Self.FormStyle := fsStayOnTop;
|
|
end;
|
|
|
|
procedure TAuthForm.FromClose(Sender: TObject; var Action: TCloseAction);
|
|
begin
|
|
// 폼이 닫힐 때 메모리에서 자동으로 제거되도록 설정
|
|
Action := caFree;
|
|
end;
|
|
|
|
function IsValidInput(const AText: string): Boolean;
|
|
const
|
|
// 영문, 숫자, 특수문자만 허용
|
|
ALLOWED_PATTERN = '^[a-zA-Z0-9!@#$%^&*()_+\-=\[\]{};'':"\\|,.<>\/?]+$';
|
|
begin
|
|
// 빈 값 체크 후 패턴 검사
|
|
Result := (AText <> '') and TRegEx.IsMatch(AText, ALLOWED_PATTERN);
|
|
end;
|
|
|
|
function GetPostData(sDestIPort, sUrl, sData: String; sReqType: String = ''): String;
|
|
var
|
|
HTTP: TIdHTTP;
|
|
SSL: TIdSSLIOHandlerSocketOpenSSL;
|
|
ss: TStringStream;
|
|
begin
|
|
Result := '';
|
|
|
|
try
|
|
if sUrl = '' then
|
|
exit;
|
|
|
|
if sUrl[1] = '/' then
|
|
Delete(sUrl, 1, 1);
|
|
|
|
Guard(SSL, TIdSSLIOHandlerSocketOpenSSL.Create(nil));
|
|
SSL.SSLOptions.Method := sslvSSLv23;
|
|
SSL.SSLOptions.SSLVersions := [sslvTLSv1_2, sslvTLSv1_1, sslvTLSv1];
|
|
|
|
Guard(HTTP, TIdHTTP.Create(nil));
|
|
HTTP.IOHandler := SSL;
|
|
with HTTP do
|
|
begin
|
|
HandleRedirects := true;
|
|
Request.Clear;
|
|
Request.UserAgent := 'Mozilla/5.0';
|
|
Request.ContentType := 'application/xml';
|
|
Request.AcceptCharSet := 'UTF-8';
|
|
// Request.Connection := 'Keep-Alive';
|
|
// Request.CustomHeaders.Values['Keep-Alive'] := 'timeout=300, max=100';
|
|
Request.Connection := 'close';
|
|
HTTPOptions := HTTPOptions - [hoKeepOrigProtocol];
|
|
if sReqType <> '' then
|
|
Request.CustomHeaders.Values['requestType'] := sReqType;
|
|
HTTPOptions := HTTP.HTTPOptions + [hoForceEncodeParams];
|
|
ConnectTimeout := 5000;
|
|
ReadTimeout := 5000;
|
|
end;
|
|
|
|
Guard(ss, TStringStream.Create(sData, TEncoding.UTF8));
|
|
Result := HTTP.Post(sDestIPort + sUrl, ss);
|
|
except
|
|
on E: EIdHTTPProtocolException do
|
|
begin
|
|
Result := E.ErrorMessage;
|
|
end;
|
|
on E: Exception do
|
|
begin
|
|
Result := 'Error: ' + E.Message;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TAuthForm.ButtonLoginClick(Sender: TObject);
|
|
var
|
|
O: ISuperObject;
|
|
pSalt, pKek, pDek : TBytes;
|
|
sResult, sSalt: String;
|
|
nResult: integer;
|
|
CurrentTime: TDateTime;
|
|
begin
|
|
|
|
if iFailCount >= 5 then
|
|
begin
|
|
CurrentTime := Now;
|
|
// 차단 시작 시간으로부터 5분이 지났는지 확인
|
|
if (CurrentTime - BlockStartTime) < (5 / (24 * 60)) then
|
|
begin
|
|
MessageBox(0,'로그인 5회 실패로 인해 5분간 로그인이 차단됩니다', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
Exit;
|
|
end
|
|
else
|
|
begin
|
|
// 5분이 지났으면 카운트 초기화
|
|
iFailCount := 0;
|
|
end;
|
|
end;
|
|
|
|
if EditID.Text = '' then
|
|
begin
|
|
MessageBox(0,'ID를 입력하세요', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
EditID.SetFocus;
|
|
Exit;
|
|
end;
|
|
|
|
if not IsValidInput(EditID.Text) then
|
|
begin
|
|
MessageBox(0,'ID에는 영문, 숫자, 특수기호를 제외하고 들어갈 수 없습니다', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
Exit;
|
|
end;
|
|
|
|
if EditPW.Text = '' then
|
|
begin
|
|
MessageBox(0,'패스워드를 입력하세요', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
EditPW.SetFocus;
|
|
Exit;
|
|
end;
|
|
|
|
if not IsValidInput(EditPW.Text) then
|
|
begin
|
|
MessageBox(0,'패스워드에는 영문, 숫자, 특수기호를 제외하고 들어갈 수 없습니다', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
Exit;
|
|
end;
|
|
|
|
try
|
|
O := SO;
|
|
// O.S['empId'] := '0505';
|
|
// O.S['pw'] := 'Q!w2e3r4t5';
|
|
// O.S['agentId'] := '25111809085600FF96644E47';
|
|
O.S['empId'] := EditID.Text;
|
|
O.S['pw'] := EditPW.Text;
|
|
O.S['agentId'] := sAgentId;
|
|
|
|
sResult := GetPostData(sDestIPort, 'aapi/auth/refresh-token/issue', O.AsJSon);
|
|
//showmessage(sResult);
|
|
if sResult <> '' then
|
|
begin
|
|
// 일반 예외(Exception)
|
|
if sResult.Contains('Error') then
|
|
begin
|
|
//_Trace('Error .. GetPostData() .. Msg="%s"', [Result]);
|
|
MessageBox(0, LPCWSTR(sResult), 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
exit;
|
|
end
|
|
// 프로토콜 예외(EIdHTTPProtocolException)
|
|
else if sResult.Contains('err') then
|
|
begin
|
|
O := SO(sResult);
|
|
sResult := O.S['err'];
|
|
MessageBox(0, LPCWSTR('Error: ' + sResult), 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
|
|
Inc(iFailCount);
|
|
|
|
if iFailCount >= 5 then
|
|
begin
|
|
BlockStartTime := Now; // 현재 시간 기록
|
|
MessageBox(0, '인증 실패 5회 연속으로 5분간 로그인이 차단됩니다.', 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
exit;
|
|
end
|
|
else
|
|
begin
|
|
MessageBox(0, PChar(format('아이디 또는 비밀번호가 일치하지 않습니다' + sLineBreak +
|
|
'(현재 %d회 실패 / 5회 연속실패 시 차단)', [iFailCount])), 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
end;
|
|
exit;
|
|
end
|
|
else if sResult.Contains('passwordChange') then
|
|
begin
|
|
O := SO(sResult);
|
|
sResult := O.S['passwordChangeRequired'];
|
|
if sResult = 'true' then
|
|
begin
|
|
var sUrl := O.S['passwordChangeUrl'];
|
|
if sUrl.StartsWith('/') then
|
|
sUrl := sUrl.Substring(1);
|
|
|
|
var sFullUrl := sDestIPort + sUrl;
|
|
ShellExecute(0, 'open', PChar(sFullUrl), nil, nil, SW_SHOWNORMAL);
|
|
//sResult := GetPostData(sDestIPort, sUrl, O.AsJSon);
|
|
end;
|
|
|
|
//MessageBox(0, LPCWSTR(sResult), 'BSOne Login', MB_ICONINFORMATION or MB_TOPMOST or MB_SETFOREGROUND);
|
|
exit;
|
|
end
|
|
else
|
|
begin
|
|
O := SO(sResult);
|
|
//O.S['refreshToken']
|
|
iFailCount := 0;
|
|
//ShowMessage('인증 완료');
|
|
end;
|
|
end
|
|
else
|
|
begin
|
|
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
ShowMessage('오류 발생: Fail .. GetPostData()' + sLineBreak + E.Message);
|
|
exit;
|
|
end;
|
|
end;
|
|
|
|
// salt 생성
|
|
pSalt := TTgColibDrm.CreateSalt(COLIB_128BIT_KEY_LEN);
|
|
if pSalt = nil then
|
|
begin
|
|
ShowMessage('CreateSalt is failed');
|
|
exit;
|
|
end;
|
|
|
|
for var i := 0 to COLIB_128BIT_KEY_LEN - 1 do
|
|
sSalt := sSalt + Format('%.2x', [pSalt[i]]);
|
|
//ShowMessage('Salt : ' + sSalt);
|
|
|
|
// KEK 생성 및 암호화(사용자 로그인 계정 암호)
|
|
pKek := TTgColibDrm.CreateKEK(EditPW.Text, sSalt, 1000, SHA256_DIGEST_SIZE);
|
|
if pKek = nil then
|
|
begin
|
|
ShowMessage('CreateKEK is failed');
|
|
exit;
|
|
end;
|
|
|
|
// KEK 복호화 디버그
|
|
// var pTemp := TTgColibDrm.GetKEK('kek.bin');
|
|
// if (Length(pKek) = Length(pTemp)) and
|
|
// CompareMem(@pKek[0], @pTemp[0], Length(pKek)) then
|
|
// begin
|
|
// ShowMessage('KEK 복호화 성공');
|
|
// end;
|
|
|
|
// DEK 생성 및 암호화(KEK)
|
|
TTgColibDrm.CreateDEK(SHA256_DIGEST_SIZE, pKek);
|
|
|
|
if Assigned(FOnSuccess) then
|
|
FOnSuccess(Self, CODE_AUTH_OK); // 메인 서비스에 알림 발송
|
|
Close;
|
|
end;
|
|
|
|
end.
|