В своей книге “Визуальное программирование” я описывал простую систему лицензирования, в основе которой лежат алгоритмы кодирования аппаратных атрибутов компьютера, на который устанавливается программное обеспечение. Алгоритм простой и надёжный, но, как показала практика, современные тенденции маркетинга требуют более гибкого подхода в лицензировании программного обеспечения: кроме пожизненной лицензии требуются ещё две категории лицензий:

  • Лицензии на определенную версию программы
  • Лицензии на отдельные модули программы

Для этого мной был разработан набор скриптов для 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.

4 комментария к «Модульное лицензирование»
  1. Добрый день проверьте пожалуйста блок лицензирования, т.к. при запуске приложения лезут ошибки: По отсутствию констант. (кое что нашел-подобрал),
    и в строке “tmpHWKey := Agg_GetHWKey; // установить глобальную переменную, которая может.. ” AGG или APP?

  2. Добрый день.
    При запуске, приложение начинпет “ругаться” на отсутствие некоторых констант. И на данную строчку:.. tmpHWKey := Agg_GetHWKey

  3. Добрый день. При запуске приложения, выскакивают ошибки: по отсутствию констант (какую то часть я дописал), и по строке “… tmpHWKey := Agg_GetHWKey;
    // установить глобальную переменную… “

    1. Вячеслав, я исправил опечатку в статье: вместо Agg_GetHWKey нужно использовать App_GetHWKey. По константам подскажите подробней, чего не хватает, добавлю описание или ссылку.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *