Ne plus utiliser d’ID dans les URL. Drôle d’idée à première vue. Et pourtant, cela présente quelques avantages, en particulier en sécurité, en confidentialité et aussi… en esthétique. Voyons cela de plus près, avec des exemples d’implémentation en PostgreSQL, MySQL, SQLite et PHP.


Partager l’article Des jetons qui remplacent des ID sur les réseaux sociaux


Un jeton dans une URL ? C’est quoi ? Pourquoi ?

Dans un site d’e-commerce, dans un site de contenu, dans des forums, dans des backoffice, etc. vous avez souvent dans les URL, même si elle sont réécrites, un identifiant, ID, qui correspond tout bêtement à la clé primaire utilisée en base de donnée.

Seulement cela pose plusieurs problèmes :

  • ça expose une partie de la logique de votre base de donnée : on connaît l’ID et on peut donc deviner en incrémentant d’autres entrées. Gênant dans un backoffice. Surtout s’il y a des failles… Y compris dans une API REST.
  • ce n’est pas joli et pas très encourageant pour quelqu’un qui parcourt un site nouvellement en ligne avec peu de pages, par exemple foo.com/article/1, foo.com/article/2, foo.com/article/3.

Comment faire alors ?

UUID ? Non. Les chaînes obtenues sont trop longues.

De préférence, il faut créer une chaîne de longueur fixe, de taille acceptable, et en évitant des caractères trop compliqués. Cette chaîne doit être générée aléatoirement et être unique.

On a des exemples concrets ?

Oui. Pleins. Stackoverflow, YouTube, Amazon… et le blog que vous êtes en train de lire.

Alors, comment cela se présente à chaque fois ? Voyons ça de plus près avec des exemples d’URL.

Stackoverflow

Une URL de question sur Stackoverflow ressemble à ça : https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb. Mais savez-vous que l’URL https://stackoverflow.com/questions/5623838/ est tout aussi valable et redirigera vers l’URL précédente ? Très utile. Maintenant vous le savez. De plus, leur système de jeton permet (à priori) de ne pas connaître le nombre d’éléments en base de données.

YouTube

Le jeton utilisé par Youtube, du moins pour les vidéos, et plus compliqué. Voici un exemple d’URL : https://www.youtube.com/watch?v=WLnz4wJ4DiU. Et encore un autre : https://www.youtube.com/watch?v=c0UsOlqG_Xk

Intéressant non ? Il s’agit en fait d’une version modifiée du codage en base 64. Je vous recommande cette réponse très bien documentée sur StackExchange de Glenn Slayden, qui a fait un remarquable travail à ce sujet.

Amazon

Amazon semble utiliser un numéro de 10 chiffres. Une URL Amazon d’exemple : https://www.amazon.fr/PostgreSQL-Running-Practical-Advanced-Database/dp/1491963417/. Si vous utilisez l’URL https://www.amazon.fr/dp/1491963417/, vous serez redirigé sur la précédente, un peu comme pour Stackoverflow.

Comment réaliser ça en DB ?

Nous allons partir sur une génération aléatoire d’octets, que nous allons ensuite convertir dans une base qui est convenable. On utilisera 3 bases au choix : base64, base 16 et base 10.

L’idéal, c’est que ce jeton se génère sans action de notre part, il faut donc utiliser des déclencheurs (triggers).

Nous verrons comment faire pour PostgreSQL, MySQL et SQLite.

PostgreSQL

Pour Postgresql, j’ai cet article du blog de &yet qui explique bien le procédé, avec script clé en main.

J’ai repris le script pour avoir la possibilité de choisir le type de jeton à générer en passant par deux fonctions au lieux d’une : une fonction générant purement et simpelment le jeton, et une générant un jeton unique pour une requête, destiné à être utilisée dans un déclencheur.

Notez cependant que j’ai dû ajouter une fonction supplémentaire pour palier l’impossibilité de convertir directement une chaîne en hexadécimal vers un entier. J’ai choisi la solution proposée sur le site https://postgres.cz.

Enfin, le code que je vous fournis ici a été testé sur PostgreSQL version 10.6.

Donc, la fonction pour convertir vers des entiers décimaux :

CREATE OR REPLACE FUNCTION hex2dec (TEXT)
RETURNS TEXT as 
$$
DECLARE 
	x numeric;
	cnt int;
	digit int;
BEGIN
	x := 0;
	FOR cnt IN 1..length($1) LOOP 
		EXECUTE E'SELECT x\''||substring($1 FROM cnt FOR 1)|| E'\'::integer' INTO digit;
		x := x * 16 + digit ;
	END LOOP;
	return x::varchar;
END;
$$ language 'plpgsql' immutable;

Maintenant, définissons la fonction créant un jeton :

CREATE OR REPLACE FUNCTION short_token(base TEXT, size INT)
RETURNS TEXT AS $$

DECLARE
  token TEXT;
BEGIN
    IF base = 'base64' THEN
      token := rtrim(encode(gen_random_bytes(size), 'base64'), '=');
      token := replace(token, '/', '_');
      token := replace(token, '+', '-');
    ELSIF base = 'hex' THEN
      token := encode(gen_random_bytes(size), 'hex');
    ELSIF base = 'dec' THEN
      token := hex2dec(encode(gen_random_bytes(size), 'hex'));
    ELSE
      RAISE EXCEPTION 'Bad value for "base" argument. It must be "base64", "hex" or "dec".';
    END IF;
  RETURN token;
END;
$$ language 'plpgsql';

Testons là :

# SELECT short_token('base64', 4);

 short_token 
-------------
 GIWfjg
(1 row)

# SELECT short_token('hex', 4);

 short_token 
-------------
 4b9f380f
(1 row)

# SELECT short_token('dec', 4);

 short_token 
-------------
 1023497367
(1 row)

# SELECT short_token('foo', 4);

ERROR:  Bad value for "base" argument. It must be "base64", "hex" or "dec".
CONTEXT:  PL/pgSQL function short_token(text,integer) line 15 at RAISE

# SELECT short_token('dec', 0);

ERROR:  Length not in range
CONTEXT:  PL/pgSQL function short_token(text,integer) line 13 at assignment

Parfait, elle donne des résultats cohérents et lève bien des erreurs quand il le faut.

Ensuite, il nous faut une fonction à utiliser dans un contexte de déclencheur, cette fonction vérifie, pour la table à laquelle elle est associée via un trigger, si le jeton généré est déjà utilisé ou non en bouclant jusqu’à ce que le jeton généré n’existe pas déjà en base de données.

CREATE OR REPLACE FUNCTION unique_short_token()
RETURNS TRIGGER AS $$

DECLARE
  token TEXT;
  query TEXT;
  found TEXT;
BEGIN

  query := 'SELECT token FROM ' || quote_ident(TG_TABLE_NAME) || ' WHERE token=';

  LOOP
    token := short_token(CAST(TG_ARGV[0] AS TEXT), CAST(TG_ARGV[1] AS INT));
    
    EXECUTE query || quote_literal(token)
    INTO    found;

    IF found IS NULL THEN
      EXIT;
    END IF;
  END LOOP;

  NEW.token = token;

  RETURN NEW;
END;
$$ language 'plpgsql';

Il ne vous reste plus qu’à associer cette fonction à des déclencheurs, par exemple :

DROP TABLE IF EXISTS examples;
CREATE TABLE examples (
  id SERIAL,
  name TEXT,
  token TEXT
);

-- on associe la génération de jetons en hexa
DROP TRIGGER IF EXISTS trigger_unique_short_token ON examples;
CREATE TRIGGER trigger_unique_short_token
BEFORE INSERT ON examples
FOR EACH ROW
EXECUTE PROCEDURE unique_short_token('hex', 4);


DROP TABLE IF EXISTS examples2;
CREATE TABLE examples2 (
  id SERIAL,
  name TEXT,
  token TEXT
);

-- on associe la génération de jetons en entiers décimaux
DROP TRIGGER IF EXISTS trigger_unique_short_token ON examples2;
CREATE TRIGGER trigger_unique_short_token
BEFORE INSERT ON examples2
FOR EACH ROW
EXECUTE PROCEDURE unique_short_token('dec', 4);


DROP TABLE IF EXISTS examples3;
CREATE TABLE examples3 (
  id SERIAL,
  name TEXT,
  token TEXT
);

-- on asscoie la génération de jetons en base64
DROP TRIGGER IF EXISTS trigger_unique_short_token ON examples3;
CREATE TRIGGER trigger_unique_short_token
BEFORE INSERT ON examples3
FOR EACH ROW
EXECUTE PROCEDURE unique_short_token('base64', 4);


INSERT INTO examples(name) VALUES ('foo'), ('bar'), ('thing');
INSERT INTO examples2(name) VALUES ('foo'), ('bar'), ('thing');
INSERT INTO examples3(name) VALUES ('foo'), ('bar'), ('thing');

-- on affiche les résultats
SELECT * FROM examples;
SELECT * FROM examples2;
SELECT * FROM examples3;

Et les résultats sont :

 id | name  |  token   
----+-------+----------
  1 | foo   | 8c103451
  2 | bar   | f412fe71
  3 | thing | 6261ea56
(3 rows)

 id | name  |   token    
----+-------+------------
  1 | foo   | 2952238719
  2 | bar   | 2098431685
  3 | thing | 1039318104
(3 rows)

 id | name  | token  
----+-------+--------
  1 | foo   | 0HgMVw
  2 | bar   | I0ArEg
  3 | thing | kXuttQ
(3 rows)

Vous pouvez télécharger directement le script complet pour PostgreSQL. et le modifier selon vos besoins.

MySQL

On peut choisir la même approche pour MySQL, qui possède un générateur d’octets aléatoires ainsi que des fonctions de conversion en base 64, en binaire vers base 16 et base 10.

On va créer dans un premier temps une fonction, et ensuite créer le déclencheur. L’intéret de passer par une fonction ici est le même que dans le cas de PostgreSQL : pouvoir réutiliser le code dans d’autres cas ou d’autres déclencheurs.

Notez que le code a été testé sur MySQL version 5.7.25.

DROP FUNCTION IF EXISTS short_token;

DELIMITER $$

CREATE FUNCTION short_token(base VARCHAR(6), size INT)
RETURNS VARCHAR(255)
BEGIN
  DECLARE token VARCHAR(255);

  IF base = "base64" THEN
    SELECT
        REPLACE(
            REPLACE(
                TRIM(TRAILING '=' FROM TO_BASE64(RANDOM_BYTES(size))),
                '+',
                '-'
            ),
            '/',
            '_'
        )
    INTO token;
  ELSEIF base = "hex" THEN
    SELECT LOWER(HEX(RANDOM_BYTES(size)))
    INTO token;
  ELSEIF base = "dec" THEN
    SELECT CONV(HEX(RANDOM_BYTES(size)), 16, 10)
    INTO token;
  ELSE
    SIGNAL SQLSTATE '45000'
      SET MESSAGE_TEXT = 'Bad value for "base" argument. It must be "base64", "hex" or "dec".';
  END IF;

  RETURN token;
END$$
 
DELIMITER ;

Bien, comme pour son équivalent vu plus haut pour PostgreSQL, nous allons la tester :

mysql> SELECT short_token('dec', 4);
+-----------------------+
| short_token('dec', 4) |
+-----------------------+
| 4043672793            |
+-----------------------+
1 row in set (0.00 sec)

mysql> SELECT short_token('hex', 4);
+-----------------------+
| short_token('hex', 4) |
+-----------------------+
| c9bb80d2              |
+-----------------------+
1 row in set (0.00 sec)

mysql> SELECT short_token('base64', 4);
+--------------------------+
| short_token('base64', 4) |
+--------------------------+
| FT4GIg                   |
+--------------------------+
1 row in set (0.00 sec)

mysql> SELECT short_token('foo', 4);
ERROR 1644 (45000): Bad value for "base" argument. It must be "base64", "hex" or "dec".
mysql> SELECT short_token('base64', 0);
ERROR 1690 (22003): length value is out of range in 'random_bytes'

Parfait, c’est le comportement attendu, y compris au niveau des erreurs levées. Nous pouvons passer à la création d’une procédure stockée, qui sera appelée dans des déclencheurs pour générer un jeton unique par rapport à la table associée.

MySQL ne permettant pas de SQL dynamique dans toute portion de code destinée à des déclencheurs, j’ai dû prévoir le cas des tables concernées avec un IF THEN. Bref, assez de blabla et voyons cette procédure stockée :

DROP PROCEDURE IF EXISTS unique_short_token;

DELIMITER $$
 
CREATE PROCEDURE unique_short_token(
    IN base VARCHAR(6),
    IN size INT,
    IN table_name VARCHAR(255),
    OUT token VARCHAR(255)
)
BEGIN
    ok: LOOP
        SET token = short_token(base, size);

        -- MySQL ne permet pas le SQL dynamique dans le code utilisé dans
        -- des triggers… C’est très pénible et ça oblige à faire ce genre
        -- d’horreur
        IF table_name = 'examples' THEN
            SELECT `token`
            INTO @found
            FROM examples
            WHERE `token` = QUOTE(@token);
        ELSEIF table_name = 'examples2' THEN
            SELECT `token`
            INTO @found
            FROM examples2
            WHERE `token` = QUOTE(@token);
        ELSEIF table_name = 'examples3' THEN
            SELECT `token`
            INTO @found
            FROM examples3
            WHERE `token` = QUOTE(@token);
        END IF;

        IF @found IS NULL THEN
            LEAVE ok;
        END IF;
    END LOOP;
END$$
 
DELIMITER ;

L’idée est sensiblement la même que pour l’équivalent en PostgreSQL. Sauf qu’il faut prévoir avant le nom des tables (oui c’est nul, mais c’est MySQL, ce n’est ni Oracle, ni PostgreSQL).

Allons-y pour les déclencheurs (et les tables) :


DROP TABLE IF EXISTS examples;
CREATE TABLE examples (
    id      INT AUTO_INCREMENT PRIMARY KEY,
    name    VARCHAR(100) NOT NULL,
    token   VARCHAR(255)
);

DROP TABLE IF EXISTS examples2;
CREATE TABLE examples2 (
    id      INT AUTO_INCREMENT PRIMARY KEY,
    name    VARCHAR(100) NOT NULL,
    token   VARCHAR(255)
);

DROP TABLE IF EXISTS examples3;
CREATE TABLE examples3 (
    id      INT AUTO_INCREMENT PRIMARY KEY,
    name    VARCHAR(100) NOT NULL,
    token   VARCHAR(255)
);



DROP TRIGGER IF EXISTS trigger_unique_short_token;

DELIMITER $$

CREATE TRIGGER trigger_unique_short_token
BEFORE INSERT ON examples FOR EACH ROW
BEGIN
    DECLARE token VARCHAR(255);

    CALL unique_short_token('hex', 4, 'examples', token);

    SET NEW.token = token;
END$$
 
DELIMITER ;



DROP TRIGGER IF EXISTS trigger_unique_short_token2;

DELIMITER $$

CREATE TRIGGER trigger_unique_short_token2
BEFORE INSERT ON examples2 FOR EACH ROW
BEGIN
    DECLARE token VARCHAR(255);

    CALL unique_short_token('dec', 4, 'examples2', token);

    SET NEW.token = token;
END$$
 
DELIMITER ;



DROP TRIGGER IF EXISTS trigger_unique_short_token3;

DELIMITER $$

CREATE TRIGGER trigger_unique_short_token3
BEFORE INSERT ON examples3 FOR EACH ROW
BEGIN
    DECLARE token VARCHAR(255);

    CALL unique_short_token('base64', 4, 'examples3', token);

    SET NEW.token = token;
END$$
 
DELIMITER ;

Et testons tout ça :

INSERT INTO examples(`name`) VALUES ('foo'), ('bar'), ('thing');
INSERT INTO examples2(`name`) VALUES ('foo'), ('bar'), ('thing');
INSERT INTO examples3(`name`) VALUES ('foo'), ('bar'), ('thing');

SELECT * FROM examples;
SELECT * FROM examples2;
SELECT * FROM examples3;

On obtient alors :

+----+-------+----------+
| id | name  | token    |
+----+-------+----------+
|  1 | foo   | 7e52cf7a |
|  2 | bar   | c7b165b5 |
|  3 | thing | 76841269 |
+----+-------+----------+
3 rows in set (0.00 sec)

+----+-------+------------+
| id | name  | token      |
+----+-------+------------+
|  1 | foo   | 2758149348 |
|  2 | bar   | 2493851898 |
|  3 | thing | 1853883029 |
+----+-------+------------+
3 rows in set (0.00 sec)

+----+-------+--------+
| id | name  | token  |
+----+-------+--------+
|  1 | foo   | oQpNvA |
|  2 | bar   | DVuQ3w |
|  3 | thing | FVfBSw |
+----+-------+--------+
3 rows in set (0.00 sec)

Ce qui est conforme à ce qu’on avait avec PostgreSQL.

Vous pouvez télécharger directement le script complet pour MySQL pour le tester et le reprendre chez vous.

SQLite

Le cas de SQLite est plus compliqué. On peut passer par la génération de nombre aléatoire grâce à la fonction RANDOMBLOB. Donc, pour faire avec une chaîne en base 16, c’est faisable, il suffit de faire comme ce qui suit :

-- donnera une chaîne en base 16 de 8 caractères
SELECT hex(randomblob(4));

Mais c’est tout, en SQLite natif, on est bloqué. Pour une version à la carte, en base64 ou juste en base 10, soit il faut s’inspirer de ce gist qui utilise les common table expression et adapter le code dans un ou plusieurs déclencheurs selon votre besoin, soit, le plus simple, est d’injecter des fonctions utilisateurs dans SQLite si vous utilisez la base via un langage de programmation comme C ou PHP. Voyez SQLite3::createFunction pour PHP par exemple.

Voyons tout de même la réalisation d’un déclencheur pour une table examples avec un jeton généré en base 16 :

DROP TABLE IF EXISTS examples;
CREATE TABLE examples (
    id            INTEGER  PRIMARY KEY
                           UNIQUE,
    name         TEXT     NOT NULL,
    token        TEXT     UNIQUE
);

CREATE TRIGGER on_insert_example
         AFTER INSERT
            ON examples
      FOR EACH ROW
          WHEN NEW.token IS NULL
BEGIN
-- changez la valeur 6 par le nombre d’octets que vous voulez
    UPDATE examples
       SET token = hex(randomblob(6));
     WHERE id = NEW.id;
END;

Et maintenant, avec la base 10, grâce à une Common Table Expression :

DROP TABLE IF EXISTS examples2;
CREATE TABLE examples2 (
    id            INTEGER  PRIMARY KEY
                           UNIQUE,
    name         TEXT     NOT NULL,
    token        TEXT     UNIQUE
);


-- majeure partie du code de ce trigger
-- par Chichou. Son blog : https://blog.chichou.me/
-- Vu sur <https://gist.github.com/ChiChou/97a53caa2c0b49c1991e>
CREATE TRIGGER on_insert_example2
         AFTER INSERT
            ON examples2
      FOR EACH ROW
          WHEN NEW.token IS NULL
BEGIN
    UPDATE examples2
       SET token = (
            -- Common Table Expression pour convertir hex vers decimal
            -- Voir <https://gist.github.com/ChiChou/97a53caa2c0b49c1991e>
            -- Par Chichou. Son blog : https://blog.chichou.me/
            -- Copyright ChiChou
            WITH RECURSIVE
                unhex(str, val, weight) AS (
                    -- changez la valeur 4 par le nombre
                    -- d’octets que vous voulez
                    SELECT hex(randomblob(4)), 0, 1
                    UNION ALL
                        SELECT 
                        substr(str, 1, length(str) - 1),
                        val + (instr('0123456789ABCDEF', substr(str, length(str), 1)) - 1) * weight,
                        weight * 16
                        FROM unhex WHERE length(str) > 0
                )
            SELECT val FROM unhex ORDER BY weight DESC LIMIT 1
        )
     WHERE id = NEW.id;
END;

Testons ces codes :

INSERT INTO examples(name) VALUES ('foo'), ('bar'), ('thing');
INSERT INTO examples2(name) VALUES ('foo'), ('bar'), ('thing');

Ce qui donne bien ce qu’on veut :

# SELECT * FROM examples;
1           foo         7693521d  
2           bar         4239c1f0  
3           thing       0ee2b138  

# SELECT * FROM examples2;
1           foo         3438703825
2           bar         2207269411
3           thing       894887396 

Mais ces possibilités n’incluent pas un test de l’existant. Je vous recommande dans ce cas de passer par le développement du côté PHP et/ou par l’injection de fonctions utilisateurs, plus pratique et permet d’éviter de répéter le code.

Vous pouvez en tout cas télécharger le script complet pour SQLite pour vous amuser avec.

Comment faire ça en PHP ?

J’en ai eu besoin sur certains projets. Du coup j’ai conçu une petite lib en POO, Token Generator, qui permet de générer des jetons de différentes façons et avec différentes représentations. Tout est expliqué dans le fichier README. Vous pouvez y définir votre propre générateur et votre propre formateur.

Si vous voulez faire ça par vous-même, vous devrez utiliser la fonction random_bytes, et jouez avec les fonctions de conversion de base.

Vous pourrez éventuellement utiliser le code PHP comme fonctions utilisateurs à injecter dans la base de données SQLite.

Pour se caler avec ce qui a été fait dans les exemples de codes pour les bases de données, je vous propose cet exemple d’implémentation :

/**
  * La fonction définissant juste le générateur de token
  */
function short_token(string $type = 'base64', int $size = 6): string
{
    if (!in_array($type, ['base64', 'hex', 'dec'])) {
        throw new \InvalidArgumentException(
            'Type must be "base64", "hex" or "dec".'
        );
    }

    // On génère les octets aléatoires
    $alea = random_bytes($size);

    $funcs = [
        'base64' => function($alea){
            return strtr(
                strtr(
                    trim(
                        base64_encode($alea),
                        '='
                    ),
                    '/',
                    '_'
                ),
                '+',
                '-'
            );
        },
        'hex' => function ($alea) {
            return bin2hex($alea);
        },
        'dec' => function($alea) use ($size) {
            return base_convert(bin2hex($alea), 16, 10);
        }
    ];

    return (string) $funcs[$type]($alea);
}

La fonction prend deux paramètres optionnels :

  • $type pour le type de jeton à générer, trois choix sont possibles parmi base64, hex et dec, le défaut étant base64 ;
  • $size pour le nombre d’octets aléatoires à utiliser à la source.

Voyons si tout fonctionne bien :


var_dump(unique_short_token('base64', 4));
var_dump(unique_short_token('base64', 6));
var_dump(unique_short_token('base64', 8));


var_dump(unique_short_token('hex', 4));
var_dump(unique_short_token('hex', 6));
var_dump(unique_short_token('hex', 8));


var_dump(unique_short_token('dec', 4));
var_dump(unique_short_token('dec', 6));
var_dump(unique_short_token('dec', 8));

Et leurs sorties respectives :

string(6) "MgPWdA"
string(8) "gquhxYZK"
string(11) "UUw6sMSEc0k"
string(8) "bf3ed005"
string(12) "386a99623f35"
string(16) "17e70658734b76ad"
string(9) "167164252"
string(15) "115409802893224"
string(20) "11500403502333684202"

Vérifions la levée des erreurs :

short_token('foo', 4);
/* donne
PHP Fatal error:  Uncaught InvalidArgumentException: Type must be "base64", "hex" or "dec". in /home/michel/token.php:8
Stack trace:
#0 /home/michel/token.php(58): short_token('foo', 4)
#1 {main}
  thrown in /home/michel/token.php on line 8
*/

short_token('base64', 0);
/* donne
PHP Fatal error:  Uncaught Error: Length must be greater than 0 in /home/michel/token.php:14
Stack trace:
#0 /home/michel/token.php(14): random_bytes(0)
#1 /home/michel/token.php(58): short_token('base64', 0)
#2 {main}
  thrown in /home/michel/token.php on line 14

*/

Parfait, les erreurs sont bien levées. On peut maintenant passer à la fonction pour générer un jeton unique par rapport à un ensemble de jetons existants :

/**
  * La fonction générant un token unique par rapport à un ensemble de tokens pré-existants.
  */
function unique_short_token(string $type = 'base64', int $size = 6, array $existing)
{
    $token = short_token($type, $size);

    if (!in_array($token, $existing)) {
        return $token;
    }

    return unique_short_token($type, $size, $existing);
}

C’est une petite fonction récursive, elle prend les mêmes arguments que short_token() avec en plus un troisième permettant de fournir un tableau de valeurs existantes.

Voilà, vous avez une implémentation toute simple en PHP. Pour quelque chose de plus poussée, je vous invite à regarder la lib que j’ai faite.

Conclusion

S’il y a une chose à retenir, c’est l’utilisation de la génération aléatoire d’octets. Pour rappel :

  • gen_random_bytes() en PostgreSQL
  • random_bytes() en MySQL
  • randomblob() en SQLite
  • random_bytes() en PHP

Ensuite, il faut convertir le résultat de ce générateur dans une base qui vous convient, avec d’éventuel post-traitement si c’est le base 64 qui est choisi.

Vous avez maintenant les outils clé en main pour réaliser vos jetons, voir même étendre ce que je vous ai fourni. Avouez que c’est mieux d’utiliser les jetons dans vos projets qu’au casino.

Photo de Amanda Jones sur Unsplash