В своей книге “Визуальное программирование” я описывал простую систему лицензирования, в основе которой лежат алгоритмы кодирования аппаратных атрибутов компьютера, на который устанавливается программное обеспечение. Алгоритм простой и надёжный, но, как показала практика, современные тенденции маркетинга требуют более гибкого подхода в лицензировании программного обеспечения: кроме пожизненной лицензии требуются ещё две категории лицензий:
- Лицензии на определенную версию программы
- Лицензии на отдельные модули программы
Для этого мной был разработан набор скриптов для My Visual Database, с помощью которого реализуется лицензирование, в том числе версионное и модульное. Благодаря продуманному алгоритму, скрипты могут быть включены в любой ваш проект: при инициализации подсистемы она автоматически создает все необходимые элементы пользовательского интерфейса, включая форму регистрации.
Алгоритм
Алгоритм проверки правильности лицензионного ключа основан на сравнении его в вычисленным значением, которое получается при вычислении хэша MD5 для строкового выражения.
Тип лицензии | Состав выражения | Примечание |
---|---|---|
Пожизненная | Аппаратный ключ + секретное слово | |
Версионная | Аппаратный ключ + секретное слово + номер версии | Используется номер основной версии, лицензия действует на все субверсии ПО |
Модульная | Аппаратный ключ + секретное слово + префикс модулей + список модулей | Список модулей – это список номеров модулей через запятую |
Версионно-модульная | Аппаратный ключ + секретное слово + номер версии + префикс модулей + список модулей |
Для определения аппаратного ключа используется функция App_GetHWKey(), которая объединяет серийный номер раздела C: и мак-адрес.
function App_GetHWKey: string;
// возвращает уникальный ключ оборудования
begin
try
Result := StrToMD5 ( GetFirstMacAddress + GetHardDiskSerial('c:') );
except // в редких случаях диска с: может не быть
RaiseException('App_GetHWKey() - не найден диск C:');
end;
end;
Code language: Delphi (delphi)
Одного серийного номера раздела С: может быть недостаточно, так как он легко редактируется некоторыми инструментами по обслуживанию дисков. В то же время были случаи, когда пользователь менял сетевые настройки (включал/отключал wifi-адаптер) и при этом менялся мак-адрес, возвращаемый функцией GetFirstMacAddress(), что приводило к сбою системы лицензирования. Такой же сбой могут вызывать анонимайзеры, меняющие мак-адрес. Поэтому при реализации данной функции нужно будет выбрать наименьшее зло. Или придумать более надёжный алгоритм.
Эту функцию я разместил в модуле App, так как планирую использовать её для сетевой идентификации и она может пригодиться в других модулях программы. Там же я добавил функции, которые отвечают за отображение номера версии. Версия из строковой величины была преобразована в два числа: мажорную и минорную часть номера, отделяемые при отображении точкой. За внешний вид версии теперь отвечает функция App_GetVersion().
function App_GetVersion:string;
// название версии - строка
begin
Result := IntToStr(APP_VERSION) + '.' + IntToStr(APP_SUBVERSION);
// если используется модуль лицензирования //
if LICENSE_TURN_ON and (not Licensed) then
Result := Result + ' (DEMO)';
end;
Code language: Delphi (delphi)
Тут возникло неудобство в виде обратной зависимости модуля App.pas от License.pas. Зато механизм лицензирования можно легко включать и отключать с помощью константы LICENSE_TURN_ON. Увы, за простоту придется заплатить надёжностью – так как скомпилированные скрипты MVDB слабо защищены, то злоумышленник, прочитав данную статью, может изменить значение данной константы в готовом проекте, поэтому в вашем проекте дайте этой константе другое название.
Основная процедура проверки лицензии License_Check(); теперь выглядит так:
procedure License_Check;
// проверить лицензию и при необходимости вывести различные надписи
var
tmpKey: string; // кешируем данные
tmpHWKey: string;
begin
tmpKey := IniFile_Read(LICENSE_SECT,LICENSE_KEY,''); // кешируем данные
if tmpKey = '' then
Licensed := False
else
begin
tmpHWKey := App_GetHWKey;
// установить глобальную переменную, которая может использоваться для ограничения функциональности
//
// полная пожизненная лицензия
Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD );
// лицензия на версию
if not Licensed then
Licensed := tmpKey = StrToMD5 ( tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
// пожизненная модульная лицензия
if not Licensed then
Licensed := License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD );
// модульная лицензия на версию
if not Licensed then
Licensed := License_CheckModules( tmpKey, tmpHWKey + LICENSE_SECRET_WORD + IntToStr(APP_VERSION) );
end;
// при необходимости менять надписи
License_UpdateAboutForm; // на форме "О программе"
App_UpdateMainForm; // на главной форме
end;
Code language: PHP (php)
Для проверки модульной лицензии потребовалась дополнительная функция License_CheckModules(), которая перебирает все возможные варианты сочетаний модулей, чтобы определить, какому из них соответствует данная лицензия.
function License_CheckModules(AKey:string; AData:string):boolean;
// подбор комбинаций (сочетаний) из номеров модулей.
var
i: integer;
k: integer; // сколько разрядов
p: integer; // разряд, который будем увеличивать
A: array of integer; // хранение сочетания модулей в виде чисел
s: string; // сборка номеров в виде строки
begin
License_Modules := '';
for k := 1 to LICENSE_MODULE_COUNT do
begin
SetLength(A, k+1 ); // нулевой элемент не используем
for i := 1 to k do
A[i] := i; // первое подмножество
p := k; // будем увеличивать самый правый разряд
while p >= 1 do // закончим, если разряды закончились
begin
// сформировать сборку в виде строки
s:='';
for i:=1 to k do
s := s + IntToStr(A[i])+',';
delete(s,length(s),1);
// проверяем
Result := AKey = StrToMD5 ( AData + LICENSE_MOD + s );
if Result then
begin
License_Modules := s;
exit;
end;
// выход из цикла, если мы проверили сборку, состоящую из всех элементов
if k = LICENSE_MODULE_COUNT then
break;
// сдвиг указателя, если последний элемент достиг максимума
if A[k] = LICENSE_MODULE_COUNT then
begin
p := p - 1;
end
else
p := k;
// если указатель не вышел за границы, то генерируем следующее сочетание
if p >= 1 then
for i := k downto p do
begin
A[i] := A[p] + i - p + 1;
end;
end;
end;
end;
Code language: Delphi (delphi)
Таким образом, система получилась универсальной: одним ключом можно разрешить работу программы с любым сочетанием модулей, с учетом версии или без учета версии. Результат проверки сохраняется в двух переменных: Licensed и License_Modules. Массив License_ModuleNames предназначен для хранения удобочитаемых названий для модулей, список которых формируется с помощью функции License_GetModulesNames().
var
Licensed: boolean; // проверка, есть ли у программы лицензия
License_Modules: string; // лицензии модулей: номера через запятую
License_ModuleNames: array of string; // лицензии модулей: имена
function License_GetModulesNames():string;
// Получить список лицензированных модулей
var
tmpList: array of string;
i:integer;
begin
Result := '';
tmpList := SplitString(License_Modules,',');
for i:=0 to Length(tmpList) - 1 do
begin
Result := Result + License_ModuleNames[ StrToInt( tmpList[i] ) ] + ', ';
end;
if Result <> '' then
Delete(Result,Length(Result)-1,2);
end;
Code language: Delphi (delphi)
А чтобы все заработало, достаточно вызвать процедуру инициализации. Ниже приводится пример инициализации для пяти модулей. Номера модулей начинаются с единицы.
procedure License_Init;
// инициализация подсиcтсемы лицензирования
begin
License_CreateRegForm;
// модули начинаются с единицы.
SetLength(License_ModuleNames,LICENSE_MODULE_COUNT+1);
// Названия модулей, отображаются в окне "О программе"
License_ModuleNames[1] := 'Первый модуль';
License_ModuleNames[2] := 'Второй модуль';
License_ModuleNames[3] := 'Третий модуль';
License_ModuleNames[4] := 'Четвертый модуль';
License_ModuleNames[5] := 'Пятый модуль';
// проверка лицензии
License_Check;
end;
Code language: Delphi (delphi)
Интеграция с приложением
Кроме функции App_GetVersion, имеются ещё несколько процедур, с помощью которых реализовано взаимодействие с подсистемой лицензирования. В частности, на стандартной форме “О программе” с помощью процедуры License_UpdateAboutForm() добавляются сведения о лицензиаре (1), лицензионном ключе (2) и активных модулях (3). А также кнопка для отображения формы регистрации (4).

procedure License_UpdateAboutForm;
// обновить информацию о лицензии на форме "О программе"
var
tmpLabel: TLabel;
tmpLabLink: TLabel;
tmpButton: TButton;
tmpButOK: TButton;
begin
// номер версии
FindC(frmdbCoreAbout,'labVersion',tmpLabel,False);
if tmpLabel<>nil then
tmpLabel.Caption := 'Версия '+App_GetVersion;
// лицензиара и лицензионный ключ
FindC(frmdbCoreAbout,'LinkLabel1',tmpLabLink); // для привязки
FindC(frmdbCoreAbout,'Button1',tmpButOK); // для привязки
//
FindC(frmdbCoreAbout,'labLicensee',tmpLabel,False);
if tmpLabel=nil then
begin
tmpLabel := TLabel.Create(frmdbCoreAbout);
with tmpLabel do
begin
Name := 'labLicensee';
Parent := tmpLabLink.Parent;
Left := tmpLabLink.Left;
Top := tmpLabLink.Top + 24 + 80 + 40;
Font.Size := 11;
end;
end;
tmpLabel.Caption := 'Лицензиар: '+IniFile_Read(LICENSE_SECT,LICENSE_LICENSEE,'');
//
FindC(frmdbCoreAbout,'labLicenseKey',tmpLabel,False);
if tmpLabel=nil then
begin
tmpLabel := TLabel.Create(frmdbCoreAbout);
with tmpLabel do
begin
Name := 'labLicenseKey';
Parent := tmpLabLink.Parent;
Left := tmpLabLink.Left;
Top := tmpLabLink.Top + 24*2 + 80+40;
Font.Size := 11;
end;
end;
tmpLabel.Caption := 'Ключ: '+IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
//
FindC(frmdbCoreAbout,'labModuleList',tmpLabel,False);
if tmpLabel=nil then
begin
tmpLabel := TLabel.Create(frmdbCoreAbout);
with tmpLabel do
begin
Name := 'labModuleList';
Parent := tmpLabLink.Parent;
Left := tmpLabLink.Left;
Top := tmpLabLink.Top + 24*3 + 80+40;
AutoSize := False;
Width := 400;
Height := 72;
WordWrap := True;
Font.Size := 11;
end;
end;
tmpLabel.Caption := License_GetModulesNames();
//
FindC(frmdbCoreAbout,'btnRegister',tmpButton,False);
if tmpButton = nil then
begin
tmpButton := TButton.Create(frmdbCoreAbout);
with tmpButton do
begin
Name := 'btnRegister';
Parent := tmpButOK.Parent;
Left := tmpButOK.Left - 134;
Width := 130;
Top := tmpButOK.Top;
Font.Size := 11;
Default := False;
Caption := 'Регистраци'+YA;
OnClick := @License_About_btnRegister_OnClick;
end;
end;
// tmpButton.Visible := not Licensed;
end;
Code language: Delphi (delphi)
Так как форма регистрации создается программно, то её вызов возможен только с помощью функции App_GetFormByName(), которая возвращает форму по имени:
procedure License_About_btnRegister_OnClick( Sender: TObject; var Cancel:boolean );
// обработка нажатия кнопки регистрации на форме "О программе"
begin
App_GetFormByName('frmAppReg').ShowModal;
end;
Code language: JavaScript (javascript)
Форма регистрации
Программное создание формы – это кропотливый и длительный процесс по сравнению с использованием визуального конструктора форм. Оправдано ли оно? Давайте для начала сравним оба подхода.
Процессы | Визуальный конструктор | Программный код |
---|---|---|
Создание | Быстрое, сразу виден результат | Медленное, для просмотра результата требуется запуск приложения |
Редактирование | Быстрое | Медленное |
Перенос в другие проекты | Требуется ручное копирование элементов формы. | Быстрое, достаточно скопировать файл со скриптами и подключить его в проект |
Как видите, если не требуется перенос функциональности в другие проекты, то проще использовать визуальный конструктор. Но в случае с подсистемой лицензирования я решил сначала с помощью визуального конструктора сделать прототип, а затем реализовать его создание программно. И вот что у меня вышло.
procedure License_CreateRegForm;
// создание формы регистрации
var
tmpForm:TForm;
tmpPC:TdbPageControl;
tmpTab: TdbTabSheet;
tmpLabel: TLabel; // TdbLabel не использовать - проблемы со стилем
tmpMemo: TdbMemo;
tmpCheckBox: TdbCheckBox;
tmpButton: TdbButton;
tmpEdit: TdbEdit;
tmpFileName: string;
begin
tmpForm := TForm.Create(Application);
with tmpForm do
begin
Name := 'frmAppReg';
Caption := 'Регистрация ';
Width := 387 + 16;
Height := 308;
BorderStyle := bsDialog;
Font.Size := 11;
Font.Name := 'Segoe UI';
OnShow := @License_RegForm_OnShow;
Position := poScreenCenter;
end;
//
tmpPC := TdbPageControl.Create(tmpForm);
with tmpPC do
begin
Name := 'pgcMain';
Parent := tmpForm;
Align := alClient;
end;
//////////////////////////////////////////////////////////////////////////////////////////////
//
tmpTab := TdbTabSheet.Create(tmpForm);
with tmpTab do
begin
Name := 'tshLicenseAgr';
TabVisible := False;
PageControl := tmpPC;
end;
//
tmpLabel := TLabel.Create(tmpForm);
with tmpLabel do
begin
Name := 'labLicenseAgr';
Parent := tmpTab;
Caption := 'Лицензионное соглашение:';
Left := 8;
Top := 8;
end;
//
tmpMemo := TdbMemo.Create(tmpForm);
with tmpMemo do
begin
Name := 'memLicenseAgr';
Parent := tmpTab;
Left := 8;
Top := 32;
Width := 364+4;
Height := 161;
ScrollBars := ssVertical;
Anchors := akTop+akLeft+akRight+akBottom;
//
tmpFileName := ExtractFilePath(Application.ExeName) + 'license.txt';
if FileExists(tmpFileName) then
Lines.LoadFromFile(tmpFileName)
else
RaiseException('Не найден файл license.txt');
end;
//
tmpCheckBox := TdbCheckBox.Create(tmpForm);
with tmpCheckBox do
begin
Name := 'chbAccept';
Parent := tmpTab;
Left := 8;
Top := 200;
Width := 290;
Caption := 'Принять лицензионное соглашение';
OnClick := @License_RegForm_chbAccept_OnClick;
Anchors := akLeft+akBottom;
end;
//
tmpButton := TdbButton.Create(tmpForm);
with tmpButton do
begin
Name := 'btnNext';
Parent := tmpTab;
Left := 238;
Top := 230;
Width := 137;
Height := 32;
Caption := 'Далее';
OnClick := @License_RegForm_btnNext_OnClick;
Anchors := akRight+akBottom;
end;
//////////////////////////////////////////////////////////////////////////////////////////////
//
tmpTab := TdbTabSheet.Create(tmpForm);
with tmpTab do
begin
Name := 'tshLicenseData';
TabVisible := False;
PageControl := tmpPC;
end;
//
tmpLabel := TLabel.Create(tmpForm);
with tmpLabel do
begin
Name := 'labLicensee';
Parent := tmpTab;
Caption := 'Лицензиар:';
Left := 8;
Top := 8;
end;
//
tmpEdit := TdbEdit.Create(tmpForm);
with tmpEdit do
begin
Name := 'edtLicensee';
Parent := tmpTab;
Left := 8;
Top := 32;
Width := 364;
end;
//
tmpLabel := TLabel.Create(tmpForm);
with tmpLabel do
begin
Name := 'labRegistrationCode';
Parent := tmpTab;
Caption := 'Код регистрации:';
Left := 8;
Top := 64;
end;
//
tmpEdit := TdbEdit.Create(tmpForm);
with tmpEdit do
begin
Name := 'edtRegistrationCode';
Parent := tmpTab;
Left := 8;
Top := 88;
Width := 337;
Height := 28;
end;
//
tmpButton := TdbButton.Create(tmpForm);
with tmpButton do
begin
Name := 'btnCopyToClipboard';
Caption := '';
Parent := tmpTab;
Left := 346;
Top := 88;
Width := 28;
Height := 28;
Hint := 'Скопировать в буфер обмена';
ShowHint := True;
ImageAlignment := 2; // iaCenter
OnClick := @License_RegForm_btnCopyToClipboard_OnClick;
end;
//
tmpLabel := TLabel.Create(tmpForm);
with tmpLabel do
begin
Name := 'labLicenseKey';
Parent := tmpTab;
Caption := 'Лицензионный ключ:';
Left := 8;
Top := 120;
end;
//
tmpEdit := TdbEdit.Create(tmpForm);
with tmpEdit do
begin
Name := 'edtLicenseKey';
Parent := tmpTab;
Left := 8;
Top := 144;
Width := 364;
end;
//
tmpButton := TdbButton.Create(tmpForm);
with tmpButton do
begin
Name := 'btnBuy';
Parent := tmpTab;
Left := 8;
Top := 230-6;
Width := 130;
Height := 32;
Caption := 'Купить';
OnClick := @License_RegForm_btnBuy_OnClick;
Anchors := akLeft+akBottom;
end;
//
tmpButton := TdbButton.Create(tmpForm);
with tmpButton do
begin
Name := 'btnActivate';
Parent := tmpTab;
Left := 215;
Top := 230-6;
Width := 160;
Height := 32;
Caption := 'Активировать';
OnClick := @License_RegForm_btnActivate_OnClick;
Anchors := akRight+akBottom;
end;
tmpPC.ActivePageIndex := 0;
end;
procedure License_RegForm_btnActivate_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Активировать"
var
tmpForm: TForm;
tmpLicenseKey: TdbEdit;
tmpLicensee: TdbEdit;
begin
CForm(Sender,tmpForm);
FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
FindC(tmpForm,'edtLicensee',tmpLicensee);
// записываем
IniFile_Write(LICENSE_SECT,LICENSE_LICENSEE,tmpLicensee.Text);
IniFile_Write(LICENSE_SECT,LICENSE_KEY,tmpLicenseKey.Text);
// проверяем
License_Check;
// смотрим
if Licensed then
begin
ShowMessage('Право владения лицензией подтверждено');
tmpForm.Close; // закрыть форму
end
else
begin
// сбрасываем
IniFile_Write(LICENSE_SECT,LICENSE_LICENSEE,'');
IniFile_Write(LICENSE_SECT,LICENSE_KEY,'');
ShowMessage('Требуется актуальный лицензионный ключ');
end;
end;
procedure License_RegForm_btnBuy_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Купить"
begin
OpenURL(LICENSE_BUY_LINK);
end;
// procedure License_RegForm_OnShow (Sender: TObject; Action: string);
procedure License_RegForm_OnShow (Sender: TObject;);
//
var
tmpForm: TForm;
tmpPC: TdbPageControl;
tmpCheck: TdbCheckBox;
tmpButton: TdbButton;
tmpLicensee: TdbEdit;
tmpRegistrationCode: TdbEdit;
tmpLicenseKey: TdbEdit;
begin
tmpForm := TForm(Sender);
FindC(tmpForm,'pgcMain',tmpPC);
FindC(tmpForm,'chbAccept',tmpCheck);
FindC(tmpForm,'btnNext',tmpButton);
FindC(tmpForm,'edtLicensee',tmpLicensee);
FindC(tmpForm,'edtRegistrationCode',tmpRegistrationCode);
FindC(tmpForm,'edtLicenseKey',tmpLicenseKey);
// открыть первую вкладку
tmpPC.ActivePageIndex := 0;
tmpCheck.Checked := False;
tmpButton.Enabled := False;
// заполнить поля данными
tmpLicensee.Text := IniFile_Read(LICENSE_SECT,LICENSE_LICENSEE,'');
tmpRegistrationCode.Text := App_GetHWKey;
tmpLicenseKey.Text := IniFile_Read(LICENSE_SECT,LICENSE_KEY,'');
end;
procedure License_RegForm_btnNext_OnClick (Sender: TObject; var Cancel: boolean);
// Обработка нажатия кнопки "Далее"
var
tmpForm: TForm;
tmpPC: TdbPageControl;
begin
CForm(Sender,tmpForm);
FindC(tmpForm,'pgcMain',tmpPC);
tmpPC.ActivePageIndex := 1;
end;
procedure License_RegForm_chbAccept_OnClick (Sender: TObject);
// разблокировка кнопки чекером
var
tmpForm:TForm;
tmpButton:TdbButton;
tmpChecker: TdbCheckbox;
begin
tmpChecker := TdbCheckbox(Sender);
CForm(Sender,tmpForm);
FindC(tmpForm,'btnNext',tmpButton);
tmpButton.Enabled := tmpChecker.Checked;
end;
procedure License_RegForm_btnCopyToClipboard_OnClick (Sender: TObject; var Cancel: boolean);
// копирование кода регистрации в буфер обмена
var
tmpForm:TForm;
tmpEdit:TdbEdit;
begin
CForm(Sender,tmpForm);
FindC(tmpForm,'edtRegistrationCode',tmpEdit);
tmpEdit.SelectAll;
tmpEdit.CopyToClipboard;
tmpEdit.SelLength := 0;
end;
Code language: Delphi (delphi)
Если вы дошли до этого места, значит вас не напугал этот огромный листинг, и вы можете полюбоваться результатом. Форма реализована в виде двухстраничного мастера. На первой странице пользователю предлагается ознакомиться с лицензионным соглашением (1), которое загружается из файла license.txt. Он должен располагаться рядом с исполняемым файлом, а его отсутствие означает нарушение целостности программного комплекса и условий лицензионного соглашения. Разблокировка кнопки “Далее” (3) происходит только после того, как пользователь принял лицензионное соглашение (2).

На второй странице расположено поле ввода лицензиара (1) – владельца лицензии, поле отображения кода регистрации (2) – аппаратного ключа, возвращаемого функцией App_GetHWKey(). Рядом с этим полем есть кнопка для копирования кода регистрации в буфер обмена, что упрощает сам процесс, который подразумевает отправку данного кода вендору, дистрибьютору или реселлеру для получения лицензионного ключа (4) на данную установку программы.

Завершение регистрации происходит при нажатии кнопки “Активировать” (5), а для получения информации о том, где именно можно купить лицензию на программу, служит кнопка “Купить” (6), открывающая в браузере страничку вендора.
Итоги
Модульное лицензирование было добавлено в проект “Производство”. При желании оно может быть использовано и в других проектах, созданных на фреймворке My Visual Database. А небольшие доработки позволят использовать модуль license.pas в проектах Delphi и других проектах, использующих язык Object Pascal.
Добрый день проверьте пожалуйста блок лицензирования, т.к. при запуске приложения лезут ошибки: По отсутствию констант. (кое что нашел-подобрал),
и в строке “tmpHWKey := Agg_GetHWKey; // установить глобальную переменную, которая может.. ” AGG или APP?
Добрый день.
При запуске, приложение начинпет “ругаться” на отсутствие некоторых констант. И на данную строчку:.. tmpHWKey := Agg_GetHWKey
Добрый день. При запуске приложения, выскакивают ошибки: по отсутствию констант (какую то часть я дописал), и по строке “… tmpHWKey := Agg_GetHWKey;
// установить глобальную переменную… “
Вячеслав, я исправил опечатку в статье: вместо Agg_GetHWKey нужно использовать App_GetHWKey. По константам подскажите подробней, чего не хватает, добавлю описание или ссылку.