diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md
index ce067f4..da83a19 100644
--- a/framework/CHANGELOG.md
+++ b/framework/CHANGELOG.md
@@ -129,6 +129,7 @@ Yii Framework 2 Change Log
     - Added unit test for saving and loading data.
 - Enh #4080: Added proper handling and support of the symlinked directories in `FileHelper`, added $options parameter in `FileHelper::removeDirectory()` (resurtm)
 - Enh #4086: changedAttributes of afterSave Event now contain old values (dizews)
+- Enh #4114: Added Security::generateRandomHexKey(), used it for various tokens and default key generation (samdark)
 - Enh: Added support for using sub-queries when building a DB query with `IN` condition (qiangxue)
 - Enh: Supported adding a new response formatter without the need to reconfigure existing formatters (qiangxue)
 - Enh: Added `yii\web\UrlManager::addRules()` to simplify adding new URL rules (qiangxue)
diff --git a/framework/base/Security.php b/framework/base/Security.php
index 07ad837..0df9c94 100644
--- a/framework/base/Security.php
+++ b/framework/base/Security.php
@@ -298,30 +298,46 @@ class Security extends Component
         }
 
         if (!isset($this->_keys[$name]) || $regenerate) {
-            $this->_keys[$name] = utf8_encode($this->generateRandomKey($length));
+            $this->_keys[$name] = $this->generateRandomKey($length);
             file_put_contents($keyFile, json_encode($this->_keys));
         }
 
-        return utf8_decode($this->_keys[$name]);
+        return $this->_keys[$name];
     }
 
     /**
-     * Generates a random key.
-     * Note the generated key is a binary string with the specified number of bytes in it.
-     * @param integer $length the length of the key that should be generated
+     * Generates specified number of random bytes.
+     * Note that output may not be ASCII.
+     * @see generateRandomKey() if you need a string.
+     *
+     * @param integer $length the number of bytes to generate
      * @throws Exception on failure.
-     * @return string the generated random key
+     * @return string the generated random bytes
      */
-    public function generateRandomKey($length = 32)
+    public function generateRandomBytes($length = 32)
     {
         if (!extension_loaded('mcrypt')) {
             throw new InvalidConfigException('The mcrypt PHP extension is not installed.');
         }
-        $key = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
-        if ($key === false) {
-            throw new Exception('Unable to generate random key.');
+        $bytes = mcrypt_create_iv($length, MCRYPT_DEV_URANDOM);
+        if ($bytes === false) {
+            throw new Exception('Unable to generate random bytes.');
         }
-        return $key;
+        return $bytes;
+    }
+
+    /**
+     * Generates a random string of specified length.
+     * The string generated matches [A-Za-z0-9_.-]+
+     *
+     * @param integer $length the length of the key in characters
+     * @throws Exception Exception on failure.
+     * @return string the generated random key
+     */
+    public function generateRandomKey($length = 32)
+    {
+        $bytes = $this->generateRandomBytes($length);
+        return strtr(StringHelper::byteSubstr(base64_encode($bytes), 0, $length), '+/=', '_-.');
     }
 
     /**
@@ -456,7 +472,7 @@ class Security extends Component
         }
 
         // Get 20 * 8bits of random entropy
-        $rand = $this->generateRandomKey(20);
+        $rand = $this->generateRandomBytes(20);
 
         // Add the microtime for a little more entropy.
         $rand .= microtime(true);
diff --git a/tests/unit/framework/base/SecurityTest.php b/tests/unit/framework/base/SecurityTest.php
index 6fac532..ff23083 100644
--- a/tests/unit/framework/base/SecurityTest.php
+++ b/tests/unit/framework/base/SecurityTest.php
@@ -132,10 +132,18 @@ class SecurityTest extends TestCase
         $this->assertEquals($data, $decryptedData);
     }
 
+    public function testGenerateRandomBytes()
+    {
+        $length = 21;
+        $key = $this->security->generateRandomBytes($length);
+        $this->assertEquals($length, strlen($key));
+    }
+
     public function testGenerateRandomKey()
     {
-        $keyLength = 20;
-        $key = $this->security->generateRandomKey($keyLength);
-        $this->assertEquals($keyLength, strlen($key));
+        $length = 21;
+        $key = $this->security->generateRandomKey($length);
+        $this->assertEquals($length, strlen($key));
+        $this->assertEquals(1, preg_match('/[A-Za-z0-9_.-]+/', $key));
     }
 }