В своей книге “Визуальное программирование” я описывал простую систему лицензирования, в основе которой лежат алгоритмы кодирования аппаратных атрибутов компьютера, на который устанавливается программное обеспечение. Алгоритм простой и надёжный, но, как показала практика, современные тенденции маркетинга требуют более гибкого подхода в лицензировании программного обеспечения: кроме пожизненной лицензии требуются ещё две категории лицензий:
- Лицензии на определенную версию программы
- Лицензии на отдельные модули программы
Для этого мной был разработан набор скриптов для 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).
![](https://k245.ru/wp-content/uploads/2022/09/izobrazhenie_2022-09-18_130637429.png)
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).
![](https://k245.ru/wp-content/uploads/2022/09/izobrazhenie_2022-09-18_133147936.png)
На второй странице расположено поле ввода лицензиара (1) – владельца лицензии, поле отображения кода регистрации (2) – аппаратного ключа, возвращаемого функцией App_GetHWKey(). Рядом с этим полем есть кнопка для копирования кода регистрации в буфер обмена, что упрощает сам процесс, который подразумевает отправку данного кода вендору, дистрибьютору или реселлеру для получения лицензионного ключа (4) на данную установку программы.
![](https://k245.ru/wp-content/uploads/2022/09/izobrazhenie_2022-09-18_133809731.png)
Завершение регистрации происходит при нажатии кнопки “Активировать” (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. По константам подскажите подробней, чего не хватает, добавлю описание или ссылку.