首页
关于
留言
Search
1
红米 AX3000 (AX6) 路由器解锁 SSH 教程
6,676 阅读
2
网盘挂载程序sharelist美化教程
4,216 阅读
3
小米路由器 AX3600 开启SSH教程,官方固件即可安装 ShellClash开启科学上网
2,168 阅读
4
Oracle 甲骨文 ARM VPS 自动抢购脚本
1,819 阅读
5
编译带PassWall和SSR-plus插件的Openwrt系统
1,393 阅读
前端
Vue
React
后端
Java
Python
PHP
数据库
运维
杂谈
小程序
影视资源
登录
Search
标签搜索
Java
Linux
Mysql
IDEA
Debian
Docker
Springboot
CentOS
Cloudflare
Maven
JavaScript
SQL
Wordpress
宝塔
Nginx
Windows
MacBook
JS
CSS
Openwrt
William
累计撰写
144
篇文章
累计收到
702
条评论
首页
栏目
前端
Vue
React
后端
Java
Python
PHP
数据库
运维
杂谈
小程序
影视资源
页面
关于
留言
搜索到
40
篇与
的结果
2026-01-27
给PHP页面设置访问密码,PHP页面怎么设置页面访问密码
为你的页面支持加密访问无论什么程序只要是PHP程序都是支持这代码的,来实现加密访问你的加密页面或文章等等的页面,不保证其他程序可以正常使用麻烦先测试! 密码正确才能进去页面,进入后会存下cookies值,下一次登录的时候则不需要再次输入了。如果不想给人看了,只需要更改密码,cookies就会立即失效,必须要重新登陆才能看。安装方法:1、新建PHP文件,命名为:mima.php,把下面代码放进这个新建为PHP文件里面<?php /** * 使用说明: * 1. 将本脚本保存为 PageEncrypt.php。 * 2. 在您想要加密的PHP页面开头包含这个文件: * require_once('PageEncrypt.php'); * 3. 设置页面访问密码: * PageEncrypt('页面密码'); */ // 定义密码Cookie加密盐,若未定义则设置默认值 if (!defined('MK_ENCRYPT_SALT')) { define('MK_ENCRYPT_SALT', 'Kgs$JC!V'); } /** * 设置页面访问密码 * @param string $password 访问密码 * @param string $pageid 页面唯一ID,用于区分同一网站的不同加密页面,默认为'default' */ function PageEncrypt($password, $pageid = 'default') { // 生成页面ID并进行MD5加密 $pageid = md5($pageid); // 对密码进行双重MD5加密,并附加盐值 $md5pw = md5(md5($password) . MK_ENCRYPT_SALT); // 获取POST提交的密码,若不存在则设为空字符串 $postpwd = filter_input(INPUT_POST, 'pagepwd', FILTER_SANITIZE_STRING); // 获取Cookie中保存的密码,若不存在则设为空字符串 $cookieName = 'mk_encrypt_' . $pageid; $cookiepwd = filter_input(INPUT_COOKIE, $cookieName, FILTER_SANITIZE_STRING); // 如果Cookie中的密码匹配,则直接返回 if (hash_equals($md5pw, $cookiepwd)) { return; } // 如果提交的密码正确,则设置Cookie并返回 if (hash_equals($md5pw, md5(md5($postpwd) . MK_ENCRYPT_SALT))) { setcookie($cookieName, $md5pw, time() + 3600000, '/'); return; } // 密码验证失败,显示密码输入页面 header('Content-Type: text/html; charset=UTF-8'); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>该页面已被加密</title> <style type="text/css"> *{font-family:"Microsoft Yahei",微软雅黑,"Helvetica Neue",Helvetica,"Hiragino Sans GB","WenQuanYi Micro Hei",sans-serif;box-sizing:border-box;margin:0px;padding:0px;font-size:14px;-webkit-transition:.2s;-moz-transition:.2s;-ms-transition:.2s;-o-transition:.2s;transition:.2s} html,body{width:100%;height:100%} body{background-color:#F4F6F9;color:#768093} input,button{font-size:1em;border-radius:3px;-webkit-appearance:none} input{width:100%;padding:5px;box-sizing:border-box;border:1px solid #e5e9ef;background-color:#f4f5f7;resize:vertical} input:focus{background-color:#fff;outline:none} button{border:0;background:#6abd09;color:#fff;cursor:pointer;opacity:1;user-select:none} button:hover,button:focus{opacity:.9} button:active{opacity:1} .main{width:100%;max-width:500px;height:300px;padding:30px;background-color:#fff;border-radius:2px;box-shadow:0 10px 60px 0 rgba(29,29,31,0.09);transition:all .12s ease-out;position:absolute;left:0;top:0;bottom:0;right:0;margin:auto;text-align:center} .alert{width:80px} .mk-side-form{margin-bottom:28px} .mk-side-form input{float:left;padding:2px 10px;width:77%;height:37px;border:1px solid #ebebeb;border-right-color:transparent;border-radius:2px 0 0 2px;line-height:37px} .mk-side-form button{position:relative;overflow:visible;width:23%;height:37px;border-radius:0 2px 2px 0;text-transform:uppercase} .pw-tip{font-weight:normal;font-size:26px;text-align:center;margin:25px auto} #pw-error {color: red;margin-top: 15px;margin-bottom: -20px;} .return-home{text-decoration:none;color:#b1b1b1;font-size:16px} .return-home:hover{color:#1E9FFF;letter-spacing:5px} </style> </head> <body> <div class="main"> <svg class="alert" viewBox="0 0 1084 1024" xmlns="http://www.w3.org/2000/svg" width="80" height="80"> <defs><style/></defs> <path d="M1060.744 895.036L590.547 80.656a55.959 55.959 0 0 0-96.919 0L22.588 896.662a55.959 55.959 0 0 0 48.43 83.907h942.14a55.959 55.959 0 0 0 47.525-85.534zm-470.619-85.172a48.008 48.008 0 1 1-96.015 0v-1.567a48.008 48.008 0 1 1 96.015 0v1.567zm0-175.345a48.008 48.008 0 1 1-96.015 0V379.362a48.008 48.008 0 1 1 96.015 0v255.157z" fill="#FF9800"/> </svg> <form action="" method="post" class="mk-side-form"> <h2>该页面已被加密</h2> <input type="password" name="pagepwd" placeholder="请输入访问密码查看" required> <button type="submit">提交</button> <?php if ($postpwd): ?> <p id="pw-error">哎哟!密码不对哦~</p> <script>setTimeout(function() {document.getElementById('pw-error').style.display = 'none';}, 2000);</script> <?php endif; ?> </form> <a href="/" class="return-home">- 返回首页 -</a> </div> </body> </html> <?php exit(); } ?>2、把下面的代码放进你要加密的PHP页面里面,比如你要给首页:index.php 加密,就把下面代码放在index.php 的最前面<?php require_once('PageEncrypt.php'); PageEncrypt('1234');//这个就是密码 ?>20260127:Update: 重构页面加密脚本至 Ultimate 版本Security: 替换 MD5 为 HMAC-SHA256;增加 Cookie HttpOnly/Secure 属性;新增防暴力破解锁定机制(5次/15分钟)。Feat: 新增注销功能 (?action=logout);支持 Config 数组动态配置;解决表单重复提交问题。UI: 全新 Glassmorphism 毛玻璃风格界面,增加动态背景与交互动画,优化移动端显示。Fix: 修复 PHP 8.1+ FILTER_SANITIZE_STRING 弃用警告。<?php /** * PageEncrypt Ultimate - 全功能单页加密脚本 * 集成功能:防暴力破解、自动注销、动态配置、HMAC加密 * * 使用方法: * require_once('PageEncrypt.php'); * PageEncrypt('你的密码', [ * 'page_id' => 'my_private_page', * 'title' => '内部数据', * ]); */ // 默认加密盐 (建议在引用此文件前定义该常量,或者直接修改此处) if (!defined('MK_ENCRYPT_SALT')) { define('MK_ENCRYPT_SALT', 'Change_This_To_Something_Random_&_Complex_999'); } /** * 核心加密函数 * @param string $password 访问密码 * @param array $options 配置数组 */ function PageEncrypt($password, $options = []) { // 1. 初始化配置 $defaults = [ 'page_id' => 'default', // 页面ID,用于区分不同页面 'title' => '访问受限', // 页面标题 'cookie_time' => 86400 * 7, // Cookie有效期 (默认7天) 'max_attempts' => 5, // 最大尝试次数 'lockout_time' => 900, // 锁定时间 (秒),默认15分钟 'logout_param' => 'logout' // 触发退出的URL参数名 (?action=logout) ]; $config = array_merge($defaults, $options); // 生成唯一标识 $pageIdHash = md5($config['page_id']); $cookieName = 'mk_encrypt_' . $pageIdHash; $sessionKey = 'mk_encrypt_limit_' . $pageIdHash; // 开启 Session (用于防爆破) if (session_status() === PHP_SESSION_NONE) { session_start(); } // 计算正确密码的哈希 (HMAC-SHA256) $correctHash = hash_hmac('sha256', $password, MK_ENCRYPT_SALT); // 2. 处理退出登录逻辑 // 访问方式: yourpage.php?action=logout if (isset($_GET['action']) && $_GET['action'] === $config['logout_param']) { setcookie($cookieName, '', time() - 3600, '/'); // 清除Cookie // 清除URL参数并跳转回原页面 $cleanUrl = strtok($_SERVER["REQUEST_URI"], '?'); header("Location: " . $cleanUrl); exit(); } // 3. 检查防暴力破解限制 $attempts = $_SESSION[$sessionKey]['count'] ?? 0; $lastTime = $_SESSION[$sessionKey]['time'] ?? 0; $isLocked = false; if ($attempts >= $config['max_attempts']) { if (time() - $lastTime < $config['lockout_time']) { $isLocked = true; $remainingTime = ceil(($config['lockout_time'] - (time() - $lastTime)) / 60); $errorMsg = "尝试次数过多,请 {$remainingTime} 分钟后再试。"; } else { // 锁定时间已过,重置计数 unset($_SESSION[$sessionKey]); $attempts = 0; } } // 4. 验证 Cookie (如果未锁定) if (!$isLocked) { $cookiePwd = $_COOKIE[$cookieName] ?? ''; if (!empty($cookiePwd) && hash_equals($correctHash, $cookiePwd)) { // 验证通过,重置错误计数并返回 unset($_SESSION[$sessionKey]); return; } } // 5. 处理 POST 提交 (如果未锁定) if (!$isLocked && $_SERVER['REQUEST_METHOD'] === 'POST') { $inputPwd = $_POST['pagepwd'] ?? ''; // 验证密码 if (hash_equals($correctHash, hash_hmac('sha256', $inputPwd, MK_ENCRYPT_SALT))) { // 密码正确 setcookie($cookieName, $correctHash, [ 'expires' => time() + $config['cookie_time'], 'path' => '/', 'secure' => isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] === 'on', 'httponly' => true, 'samesite' => 'Strict' ]); // 登录成功,清空错误计数 unset($_SESSION[$sessionKey]); // 刷新页面 header("Location: " . strtok($_SERVER["REQUEST_URI"], '?')); exit(); } else { // 密码错误:记录次数 $_SESSION[$sessionKey] = [ 'count' => $attempts + 1, 'time' => time() ]; $errorMsg = "密码错误!剩余尝试次数: " . ($config['max_attempts'] - ($attempts + 1)); // 如果刚才是最后一次尝试,立即显示锁定 if (($attempts + 1) >= $config['max_attempts']) { $errorMsg = "尝试次数过多,已被锁定。"; $isLocked = true; } } } // 6. 渲染登录界面 // 如果执行到这里,说明没有通过验证,显示登录框并终止脚本 renderLoginForm($config['title'], $errorMsg ?? null, $isLocked); exit(); } /** * 渲染 HTML 界面 (Modern Glass UI) */ function renderLoginForm($title, $errorMsg, $isLocked) { header('Content-Type: text/html; charset=UTF-8'); ?> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"> <title><?php echo htmlspecialchars($title); ?></title> <style> :root { --glass-bg: rgba(255, 255, 255, 0.75); --glass-border: rgba(255, 255, 255, 0.6); --primary-color: #4f46e5; --primary-hover: #4338ca; --text-main: #1f2937; --text-sub: #6b7280; --error-bg: #fee2e2; --error-text: #ef4444; } * { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; } body { height: 100vh; width: 100%; display: flex; align-items: center; justify-content: center; /* 动态渐变背景 */ background: linear-gradient(-45deg, #ee7752, #e73c7e, #23a6d5, #23d5ab); background-size: 400% 400%; animation: gradientBG 15s ease infinite; overflow: hidden; } @keyframes gradientBG { 0% { background-position: 0% 50%; } 50% { background-position: 100% 50%; } 100% { background-position: 0% 50%; } } .container { position: relative; width: 100%; max-width: 420px; padding: 20px; } .glass-card { background: var(--glass-bg); backdrop-filter: blur(16px); -webkit-backdrop-filter: blur(16px); border: 1px solid var(--glass-border); border-radius: 24px; padding: 40px 32px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); text-align: center; transition: transform 0.3s ease; } .icon-wrapper { width: 64px; height: 64px; background: rgba(79, 70, 229, 0.1); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 20px; } .icon-wrapper svg { width: 32px; height: 32px; color: var(--primary-color); } h1 { font-size: 24px; font-weight: 700; color: var(--text-main); margin-bottom: 8px; letter-spacing: -0.5px; } p.subtitle { font-size: 14px; color: var(--text-sub); margin-bottom: 32px; } .input-group { position: relative; margin-bottom: 20px; } input { width: 100%; padding: 14px 16px; padding-left: 44px; border: 2px solid transparent; background: rgba(255, 255, 255, 0.6); border-radius: 12px; font-size: 16px; color: var(--text-main); transition: all 0.3s ease; outline: none; } input:focus { background: #fff; border-color: var(--primary-color); box-shadow: 0 0 0 4px rgba(79, 70, 229, 0.1); } input:disabled { background: #e5e7eb; cursor: not-allowed; } .input-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: #9ca3af; width: 20px; height: 20px; } input:focus + .input-icon { color: var(--primary-color); } button { width: 100%; padding: 14px; background: var(--primary-color); color: white; border: none; border-radius: 12px; font-size: 16px; font-weight: 600; cursor: pointer; transition: all 0.2s ease; box-shadow: 0 4px 6px -1px rgba(79, 70, 229, 0.2); } button:hover:not(:disabled) { background: var(--primary-hover); transform: translateY(-1px); box-shadow: 0 6px 8px -1px rgba(79, 70, 229, 0.3); } button:active:not(:disabled) { transform: translateY(0); } button:disabled { background: #9ca3af; cursor: not-allowed; opacity: 0.8; box-shadow: none; } .error-message { margin-top: 16px; padding: 10px; background: var(--error-bg); color: var(--error-text); font-size: 14px; border-radius: 8px; display: flex; align-items: center; justify-content: center; gap: 6px; animation: shake 0.5s cubic-bezier(.36,.07,.19,.97) both; } .footer { margin-top: 24px; font-size: 13px; } .footer a { color: var(--text-sub); text-decoration: none; transition: color 0.2s; } .footer a:hover { color: var(--primary-color); text-decoration: underline; } @keyframes shake { 10%, 90% { transform: translate3d(-1px, 0, 0); } 20%, 80% { transform: translate3d(2px, 0, 0); } 30%, 50%, 70% { transform: translate3d(-4px, 0, 0); } 40%, 60% { transform: translate3d(4px, 0, 0); } } /* 移动端优化 */ @media (max-width: 480px) { .glass-card { padding: 32px 24px; } h1 { font-size: 22px; } } </style> </head> <body> <div class="container"> <div class="glass-card"> <div class="icon-wrapper"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <rect x="3" y="11" width="18" height="11" rx="2" ry="2"></rect> <path d="M7 11V7a5 5 0 0 1 10 0v4"></path> </svg> </div> <h1><?php echo htmlspecialchars($title); ?></h1> <p class="subtitle">此页面内容受密码保护</p> <form method="post"> <div class="input-group"> <input type="password" name="pagepwd" placeholder="请输入访问密码" <?php echo $isLocked ? 'disabled' : 'autofocus'; ?> required autocomplete="current-password"> <svg class="input-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <path d="M21 2l-2 2m-7.61 7.61a5.5 5.5 0 1 1-7.778 7.778 5.5 5.5 0 0 1 7.777-7.777zm0 0L15.5 7.5m0 0l3 3L22 7l-3-3m-3.5 3.5L19 4"></path> </svg> </div> <button type="submit" <?php echo $isLocked ? 'disabled' : ''; ?>> <?php echo $isLocked ? '暂时锁定' : '立即查看'; ?> </button> <?php if ($errorMsg): ?> <div class="error-message"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"> <circle cx="12" cy="12" r="10"></circle> <line x1="12" y1="8" x2="12" y2="12"></line> <line x1="12" y1="16" x2="12.01" y2="16"></line> </svg> <span><?php echo htmlspecialchars($errorMsg); ?></span> </div> <?php endif; ?> </form> <div class="footer"> <a href="/">← 返回首页</a> </div> </div> </div> </body> </html> <?php } ?>Ultimate版本基础用法跟原来一样,只需要一行密码即可,其他使用默认配置。<?php require_once('PageEncrypt.php'); PageEncrypt('mypassword123'); // 默认配置:5次机会,锁定15分钟 ?> <h1>欢迎访问加密页面</h1>高级用法:自定义标题和限制如果你想修改标题,或者让密码试错机会更多/更少。<?php require_once('PageEncrypt.php'); PageEncrypt('super_secret_pw', [ 'page_id' => 'admin_dashboard', // 必须设置,用于区分 Session 'title' => '管理员后台 - 请登录', 'max_attempts' => 3, // 只能错3次 'lockout_time' => 300, // 锁定 5 分钟 (300秒) 'cookie_time' => 3600 // 登录有效期 1 小时 ]); ?>如何添加“退出登录”按钮在你的加密页面内容中,添加一个链接即可:<h1>内部资料</h1> <p>这里是机密内容...</p> <a href="?action=logout" style="color: red;">[退出登录]</a>
2026年01月27日
45 阅读
0 评论
0 点赞
2024-04-20
使用企业微信API获取个人微信的openid实现用户鉴权!无需认证公众号!完全免费!
在微信中,需要鉴别用户身份的常用做法是什么?当然是微信授权,获取用户的openid进行确认身份。如果是h5网页,那么需要使用微信公众号提供的网页授权接口实现,如果是小程序那就好办,个人小程序的 wx.login 接口也可以获取openid,那么问题来了,微信公众号提供的网页授权只有认证的服务号才有权限,个人根本没机会使用,这便是门槛。但是我发现,企业微信的接口,个人也可以使用网页授权链接实现获取用户的openid,即便你的网页不是在企业微信环境下使用,也是可以获取到用户的openid,本次文章就是我要实现的。准备1、登录企业微信;2、创建一个应用;3、创建完毕之后,进入应用,获取 AgentId 和 Secret4、获取 appid以上3个参数准备好,就可以用代码实现了。getCode.php<?php // 获取访问令牌的函数 function getAccessToken($corpid, $corpsecret) { $url = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=$corpid&corpsecret=$corpsecret"; $response = file_get_contents($url); $data = json_decode($response, true); if(isset($data['access_token'])) { return $data['access_token']; } else { // 处理错误 return false; } } // 从数组中检索访问令牌的函数 function getCachedAccessToken() { if(file_exists('access_token.php')) { include 'access_token.php'; return $access_token_data['access_token']; } else { // 如果文件不存在,返回空字符串 return ''; } } // 检查访问令牌是否过期的函数 function isAccessTokenExpired() { if(file_exists('access_token.php')) { include 'access_token.php'; return $access_token_data['expires_at'] < time(); } else { // 如果文件不存在,返回 true 表示过期 return true; } } // 将访问令牌保存到数组中的函数 function saveAccessToken($access_token) { $access_token_data = array( 'access_token' => $access_token, // 设置访问令牌 'expires_at' => time() + 7200 // 设置过期时间为2小时后 ); file_put_contents('access_token.php', '<?php $access_token_data = ' . var_export($access_token_data, true) . ';'); } // 你的企业微信 API 凭证 $corpid = '你的企业微信appid'; $corpsecret = '你的企业微信所创建的应用的Secret'; // 检查访问令牌是否过期或未缓存 if(isAccessTokenExpired()) { // 从企业微信 API 获取新的访问令牌 $access_token = getAccessToken($corpid, $corpsecret); if($access_token) { // 将访问令牌保存到数组中 saveAccessToken($access_token); } else { // 处理错误 die("从企业微信 API 获取访问令牌失败。"); } } // 获取用户信息 function getUserInfo($access_token, $code) { $url = "https://qyapi.weixin.qq.com/cgi-bin/auth/getuserinfo?access_token=$access_token&code=$code"; $response = file_get_contents($url); $data = json_decode($response, true); return $data; } // 从数组中检索访问令牌 $access_token = getCachedAccessToken(); // code $code = $_GET['code']; $userInfo = getUserInfo($access_token, $code); if($userInfo['errcode'] == 0) { // 判断当前是userid还是openid if($userInfo['userid']) { // userid $openid = $userInfo['userid']; }else { // openid $openid = $userInfo['openid']; } // 跳转到展示页面 echo '<script>location.href="showOpenid.php?openid='.$openid.'";</script>'; }else { // 错误 echo $userInfo['errmsg']; } ?> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0,viewport-fit=cover">以上代码两处需要配置,这两处已经在上面准备阶段获取到了。// 你的企业微信 API 凭证 $corpid = '你的企业微信appid'; $corpsecret = '你的企业微信所创建的应用的Secret';showOpenid.php<html> <head> <title>展示页面</title> <meta name="viewport" content="width=device-width,initial-scale=1.0,maximum-scale=1.0,user-scalable=0,viewport-fit=cover"> </head> <body> <p style="width:90%;margin: 50px auto 0;text-align:center;padding:20px 5px;border:2px dashed #ccc;"> <?php echo $_GET['openid']; ?> </p> </body> </html>以上两个php文件上传至你服务器即可。例如你的域名是:https://www.qq.com你将 getCode.php 和 showOpenid.php 上传至根目录下的 Auth 目录。那么继续往下看:构造网页授权链接根据官方API文档可知:构造格式如下:https://open.weixin.qq.com/connect/oauth2/authorize?appid=CORPID&redirect_uri=REDIRECT_URI&response_type=code&scope=snsapi_base&state=STATE&agentid=AGENTID#wechat_redirectCORPID: 替换为你的appidredirect_uri: 替换为getCode.php页面的链接AGENTID: 替换为你的AgentId假设你的CORPID是:ww1f1289a1254d49b2假设你的AGENTID是:1000002最终的网页授权链接如下:https://open.weixin.qq.com/connect/oauth2/authorize?appid=ww1f1289a1254d49b2&redirect_uri=https://www.qq.com/Auth/getCode.php&response_type=code&scope=snsapi_base&state=STATE&agentid=1000002#wechat_redirect还要去配置一下网页授权的域名(应用管理点进去往下拉-网页授权及JS-SDK)配置完授权域名后,也要配置信任IP,不然会提示你从不被允许的IP访问将构造完成的链接发到微信:点击即可获取到openid了!注意:如果你用登录企业微信的微信扫码,可能就不是获取到openid了,而是获取到userid,因为文档说了,当用户为企业成员时(无论是否在应用可见范围之内)返回示例如下:所以你要测试的话得找个不是你的企业微信成员的微信进行扫码,才可以看到openid,所以这一块要注意,你以后的业务逻辑也是需要对这一块进行特殊处理。
2024年04月20日
34 阅读
0 评论
0 点赞
2024-03-06
WordPress纯代码实现SMTP邮件发送功能
WordPress 本身是有集成 phpmailer, 这里只是调用 phpmailer 而已。WordPress SMTP 邮件功能的全部代码如下,修改相关参数后,直接添加到当前主题的 functions.php 文件中即可://smtp发送邮件功能 add_action('phpmailer_init', 'mail_smtp'); function mail_smtp( $phpmailer ) { $phpmailer->FromName = ''; //名字 $phpmailer->Host = ''; //smtp地址,可以到你使用的邮件设置里面找 $phpmailer->Port = 587; //端口,一般不用修改 $phpmailer->Username = ''; //邮件账号 $phpmailer->Password = ''; //邮件密码 $phpmailer->From = '';//邮件账号 $phpmailer->SMTPAuth = true; $phpmailer->SMTPSecure = 'tls'; //tls or ssl (port=25留空,465为ssl)一般不用修改 $phpmailer->IsSMTP(); }常用邮箱 SMTP 服务器阿里企业云邮箱POP3/SMTP 协议收发邮件服务器地址分别如下: 收件服务器地址: POP 服务器地址:pop3.mxhichina.com 端口110,SSL 加密端口995 发件服务器地址: SMTP 服务器地址:smtp.mxhichina.com 端口25, SSL 加密端口465 腾讯企业邮箱 POP3/SMTP 协议收发邮件服务器地址分别如下: 接收邮件服务器: POP 服务器地址:pop.exmail.qq.com (端口 110),使用SSL,端口号995 发送邮件服务器: SMTP 服务器地址:smtp.exmail.qq.com (端口 25),使用SSL,端口号465
2024年03月06日
27 阅读
0 评论
0 点赞
2022-10-30
什么叫做Redis - 一个深入的教程
Redis("REmote DIctionary Service")是一个开源的键值数据库服务器。对Redis最准确的描述是,它是一个数据结构服务器。Redis的这一特定性质导致了它在开发者中的大部分流行和采用。与其迭代、分类和排序行,如果数据是你从头开始想要的数据结构,会怎么样呢?早期,它的使用方式很像Memcached,但随着Redis的改进,它在许多其他用例中变得可行,包括发布-订阅机制、流和队列。主要而言,Redis是一个内存数据库,在另一个 "真正的 "数据库(如MySQL或PostgreSQL)前面用作缓存,以帮助提高应用程序的性能。它利用了内存的速度,减轻了中央应用数据库的负载。不经常变化和经常被请求的数据对任务要求不高但经常演变的数据。上述数据的例子可以包括会话或数据缓存和排行榜或仪表盘的滚动分析。然而,对于许多用例,Redis提供了足够的保证,它可以作为一个成熟的主数据库使用。再加上Redis插件和它的各种高可用性(HA)设置,Redis作为一个数据库对于某些场景和工作负载来说已经变得非常有用。另一个重要方面是,Redis模糊了缓存和数据存储之间的界限。这里需要理解的重要一点是,在内存中读取和操作数据的速度比使用SSD或HDD的传统数据存储的速度要快得多。最初,Redis最常被拿来与Memcached比较,后者在当时缺乏任何非易失性的持久性。Memcached是由Brad Fitzpatrick在2003年创建的,比Redis早了6年。它最初是一个Perl项目,后来用C语言重写。它是当时事实上的缓存工具。它和Redis的主要区别在于它缺乏数据类型,而且它的驱逐策略有限,只有LRU(最近使用最少的)。另一个区别是,Redis是单线程的,而Memcached是多线程的。Memcached在严格的缓存环境中可能表现良好,但在分布式集群中需要一些设置,而Redis天生就支持。以下是目前这两个缓存之间的能力细分。 内存缓存雷迪斯亚毫秒延迟是的是的开发者易用性是的是的数据分区是的是的支持广泛的编程语言是的是的高级数据结构-是的多线程架构是的-快照-是的复制-是的交易-是的发布/订阅-是的Lua 脚本-是的地理空间支持-是的虽然现在可以配置它将数据持久化到磁盘的方式,但在刚推出时,Redis使用快照,将内存中的数据的异步拷贝持久化到磁盘上进行长期存储。不幸的是,这种机制有一个缺点,就是在快照之间可能会丢失你的数据。自2009年成立以来,Redis已经发展成熟。我们将介绍它的大部分架构和拓扑结构,这样你就可以把Redis加入你的数据存储系统库中。Redis 架构在我们开始讨论Redis的内部结构之前,让我们讨论一下各种Redis的部署和它们的取舍。我们将主要关注这些配置。单个 Redis 实例Redis 高可用性Redis 哨兵Redis 集群根据您的用例和规模,您可以决定使用一种设置或另一种设置。单个 Redis 实例单个 Redis 实例是最直接的 Redis 部署。它允许用户设置和运行可以帮助他们发展和加速服务的小型实例。但是,这种部署并非没有缺点。例如,如果此实例失败或不可用,则所有客户端对 Redis 的调用都会失败,从而降低系统的整体性能和速度。如果有足够的内存和服务器资源,这个实例可以很强大。主要用于缓存的场景可能会以最少的设置显着提升性能。给定足够的系统资源,您可以在应用程序运行的同一机器上部署此 Redis 服务。了解一些有关管理系统内数据的 Redis 概念是必不可少的。发送到 Redis 的命令首先在内存中处理。然后,如果在这些实例上设置了持久性,则在某个时间间隔上会有一个fork进程,以促进数据持久性 RDB(Redis 数据的非常紧凑的时间点表示)快照或 AOF(仅附加文件)。这两个流程让 Redis 拥有长期存储,支持各种复制策略,并启用更复杂的拓扑。如果 Redis 未设置为保留数据,则在重新启动或故障转移时数据会丢失。如果在重启时启用了持久性,它会将 RDB 快照或 AOF 中的所有数据加载回内存中,然后实例可以支持新的客户端请求。话虽如此,让我们看看您可能想要使用的更多分布式 Redis 设置。Redis 高可用性Redis 的另一个流行设置是主部署和与复制保持同步的辅助部署。当数据写入主实例时,它会将这些命令的副本发送到辅助实例的副本客户端输出缓冲区,这有助于复制。辅助实例可以是部署中的一个或多个实例。这些实例可以帮助扩展从 Redis 的读取或提供故障转移,以防 main 丢失。高可用性高可用性(HA)是一个系统的特点,旨在确保在高于平均水平的时间内,达到约定的运行性能水平,通常是正常运行时间。在这些HA系统中,不出现单点故障是至关重要的,这样系统就可以优雅而快速地恢复。这导致了可靠的交叉,所以数据在从主站到副站的过渡期间不会丢失,此外,还可以自动检测故障并从中恢复。由于我们现在已经进入了一个分布式系统,因此您需要在此拓扑中考虑许多新事物。以前简单的事情现在变得更加复杂。Redis 复制Redis 的每个主实例都有一个复制 ID 和一个偏移量。这两条数据对于确定副本可以继续其复制过程的时间点或确定它是否需要进行完整同步至关重要。对于主 Redis 部署上发生的每个操作,此偏移量都会增加。Replication ID, offset更明确地说,当 Redis 副本实例仅落后于主实例几个偏移量时,它会从主实例接收剩余的命令,然后在其数据集上重播,直到同步。如果两个实例无法就复制 ID 达成一致,或者主实例不知道偏移量,则副本将请求完全同步。这涉及到一个主实例创建一个新的 RDB 快照并将其发送到副本。在发生此传输时,主实例正在缓冲快照截止和当前偏移之间的所有中间更新,以便在与快照同步后发送到辅助实例。完成后,复制可以正常继续。如果一个实例具有相同的复制 ID 和偏移量,则它们具有完全相同的数据。现在您可能想知道为什么需要复制 ID。当 Redis 实例被提升为主实例或作为主实例从头开始重新启动时,它会被赋予一个新的复制 ID。这用于推断此新提升的辅助实例从中复制的先前主实例。这允许执行部分同步(与其他辅助节点),因为新的主实例会记住其旧的复制 ID。例如,两个实例(主实例和辅助实例)具有相同的复制 ID,但偏移量相差几百个命令,这意味着如果在偏移量后面的实例上重放这些实例,它们将具有相同的数据集。现在,如果复制 ID 完全不同,并且我们不知道新降级(并重新加入)辅助节点的先前复制 ID(没有共同祖先)。我们将需要执行昂贵的完全同步。或者,如果我们知道以前的复制 ID,我们就可以推断如何使数据同步,因为我们能够推断出它们共享的共同祖先,并且偏移量对于部分同步再次有意义。Redis 哨兵Sentinel 是一个分布式系统。与所有分布式系统一样,Sentinel 有几个优点和缺点。Sentinel 的设计方式是,一组哨兵进程协同工作以协调状态,从而为 Redis 提供高可用性。毕竟,您不希望保护您免受故障影响的系统有自己的单点故障。Sentinel 负责一些事情。首先,它确保当前的主实例和辅助实例正常运行并做出响应。这是必要的,因为哨兵(与其他哨兵进程)可以在主节点和/或辅助节点丢失的情况下发出警报并采取行动。其次,它在服务发现中发挥作用,就像其他系统中的 Zookeeper 和 Consul 一样。所以当一个新的客户端尝试向 Redis 写东西时,Sentinel 会告诉客户端当前的主实例是什么。因此,哨兵不断监控可用性并将该信息发送给客户端,以便他们能够在他们确实进行故障转移时对其做出反应。以下是它的职责:监控——确保主要和次要实例按预期工作。通知—通知系统管理员 Redis 实例中的事件。故障转移管理——如果主实例不可用并且足够多的(法定人数)节点同意这是真的,Sentinel 节点可以启动故障转移过程。配置管理——Sentinel 节点还充当当前主要 Redis 实例的发现点。以这种方式使用 Redis Sentinel 可以进行故障检测。此检测涉及多个哨兵进程同意当前主实例不再可用。这个协议过程称为 Quorum。这可以提高鲁棒性并防止一台机器行为异常并且无法访问主 Redis 节点。法定人数法定人数是指一个分布式系统为了被允许执行故障转移等操作而必须获得的最低票数。这个数字是可配置的,但应该反映出该分布式系统中的节点数量。大多数分布式系统的规模为3个或5个,四分位数分别为2个和3个。在系统需要打破平局的情况下,奇数的节点是比较好的。此设置并非没有缺点,因此我们将在使用 Redis Sentinel 时介绍一些建议和最佳实践。您可以通过多种方式部署 Redis Sentinel。老实说,要提出任何明智的建议,我需要比目前有关您的系统的更多背景信息。作为一般指导,我建议在每个应用程序服务器旁边运行一个哨兵节点(如果可能),这样你也不需要考虑哨兵节点和实际使用 Redis 的客户端之间的网络可达性差异。您可以将 Sentinel 与 Redis 实例一起运行,甚至可以在独立节点上运行,但这会以不同的方式使事情复杂化。我建议至少运行三个具有至少两个法定人数的节点。这是一个简单的图表,分解了集群中的服务器数量以及相关的法定人数和可容忍的可持续故障。服务器数量法定人数允许的故障数110220321431532642743服务器数量和允许故障数量的仲裁表。这会因系统而异,但总体思路是不变的。让我们花点时间思考一下这样的设置会出现什么问题。如果你运行这个系统足够长的时间,你会遇到所有这些。如果哨兵节点超出法定人数怎么办?如果网络分裂将旧的主实例置于少数群体中怎么办?这些写入会发生什么?(剧透:当系统完全恢复时它们会丢失)如果哨兵节点和客户端节点(应用程序节点)的网络拓扑错位会发生什么?没有持久性保证,特别是因为磁盘的持久性(见下文)是异步的。还有一个烦人的问题,当客户发现新的primary时,我们失去了多少写给一个不知道的primary?Redis 建议在建立新连接时查询新的主节点。根据系统配置,这可能意味着大量数据丢失。如果您强制主实例将写入复制到至少一个辅助实例,有几种方法可以减轻损失程度。请记住,所有 Redis 复制都是异步的,并且有其权衡。因此,它需要独立跟踪确认,如果它们没有得到至少一个辅助实例的确认,主实例将停止接受写入。Redis 集群我相信很多人都想过当您无法将所有数据存储在一台机器上的内存中时会发生什么。目前,单个服务器中可用的最大 RAM 为 24TIB,目前在 AWS 上在线列出。当然,这很多,但对于某些系统来说,这还不够,即使对于缓存层也是如此。Redis Cluster 允许 Redis 的水平扩展。垂直和水平缩放随着系统的发展,您有三个选择。少做(没有人会完全这样做,因为我们是贪得无厌的怪物)。放大。向外扩展。认真对待后两者,放大和缩小分别称为垂直和水平缩放。垂直扩展是一种技术,您可以让更大更好的机器更快地完成工作,并希望您的所有问题都能与您的硬件一起很好地扩展。即使这是可能的,您最终也会受到您使用的硬件的限制。一旦达到这一点(更有可能并且希望早于这一点),您将需要通过将工作负载分散到负责整体较小部分的多台较小机器上来水平扩展系统。因此,让我们先弄清楚一些术语;一旦我们决定使用Redis集群,我们就决定将我们要存储的数据分散到多台机器上,称为分片。因此,集群中的每个Redis实例被认为是整个数据的一个分片。这就带来了一个新的问题。如果我们向集群推送一个密钥,我们如何知道哪个Redis实例(分片)在保存该数据?有几种方法可以做到这一点,但Redis Cluster使用算法分片。为了找到一个给定密钥的分片,我们对密钥进行散列,并将总结果与分片的数量相乘。然后,使用一个确定的哈希函数,这意味着一个给定的密钥将总是映射到同一个分片,我们可以推理出一个特定的密钥在未来读取它时将在哪里。当我们后来想在系统中添加一个新的分片时会发生什么?这个过程被称为重新分片。假设钥匙 "foo "在引入一个新的分片后被映射到零号分片,它可能被映射到五号分片。然而,如果我们需要快速增长系统,移动数据以反映新的分片映射将是缓慢和不现实的。这对Redis集群的可用性也有不利影响。Redis Cluster为这个问题设计了一个叫做Hashslot的解决方案,所有的数据都被映射到这个Hashslot中。有16K个hashslot。这为我们提供了一个合理的方法,将数据分散到整个集群中,当我们添加新的分片时,我们只需在系统中移动哈希槽。通过这样做,我们只需要将哈希槽从分片区移动到分片区,并简化了向集群中添加新的主实例的过程。这是在没有任何停机时间的情况下实现的,对性能的影响也很小。让我们通过一个例子来谈谈。M1包含从0到8191的哈希槽。M2包含从8192到16383的哈希槽。因此,为了映射 "foo",我们对密钥(foo)进行确定性的哈希运算,然后用哈希槽的数量(16K)进行修改,从而得到M2的映射。现在我们假设增加一个新的实例,M3。新的映射将是M1包含从0到5460的哈希槽。M2包含从5461到10922的哈希槽。M3包含从10923到16383的哈希槽。所有映射M1中的哈希槽的键现在都映射到了M2中,这些键都需要移动。但是单个键对哈希槽的散列就不需要移动了,因为它们已经在哈希槽中被划分了。因此,这一级的误导解决了算法分片的重新分片问题。GossipingRedis Cluster使用gossip协议来确定整个集群的健康状况。在上面的插图中,我们有3个M节点和3个S节点。所有这些节点不断进行通信,以了解哪些分片是可用的,并准备为请求提供服务。如果有足够多的分片同意M1没有响应,它们可以决定将M1的二级S1提升为一级,以保持集群的健康。触发这一点所需的节点数量是可配置的,而且必须正确对待这个问题。如果你做得不恰当,你可能会出现这样的情况:当分区的两边都相等时,如果不能打破平局,集群就会分裂。这种现象被称为分裂的大脑。作为一般规则,必须要有奇数的主节点和每个节点的两个副本,这样的设置才是最稳健的。Redis 持久化模型如果我们要使用Redis来存储任何种类的数据进行安全保管,那么了解Redis是如何做到的就很重要了。在很多情况下,如果你丢失了Redis所存储的数据,并不是世界末日。把它作为一个缓存,或者在它为实时分析提供动力的情况下,如果数据丢失,也不是世界末日。在其他情况下,我们希望对数据的持久性和恢复有一些保证。无持久性无持久性:如果你愿意,你可以完全禁用持久性。这是运行Redis的最快方式,没有持久性保证。RDBRDB (Redis数据库)。RDB持久化在指定的时间间隔内对你的数据集进行时间点快照。这种机制的主要缺点是,快照之间的数据会丢失。此外,这种存储机制还依赖于fork主进程,在一个较大的数据集中,这可能导致服务请求的瞬间延迟。也就是说,RDB文件在内存中的加载速度比AOF快得多。AOFAOF(仅附加文件):AOF持久性记录了服务器收到的每一个写操作,这些操作将在服务器启动时再次播放,重建原始数据集。这种确保持久性的方式比RDB快照更持久,因为它是一个只需追加的文件。当操作发生时,我们把它们缓冲到日志中,但它们还没有被持久化。这个日志包含了我们实际运行的命令,以便在需要时进行重放。然后在可能的情况下,我们用fsync把它冲到磁盘上(何时运行是可配置的),它将被持久化。缺点是格式不紧凑,比RDB文件占用更多磁盘。fsync()将文件描述符fd所指的文件的所有修改的核心数据(即修改的缓冲缓存页)转移("冲刷")到磁盘设备(或其他永久存储设备),这样即使系统崩溃或重启,也可以检索到所有改变的信息。由于各种原因,当对文件进行修改时,它们是在缓存中进行的,对fsync()的调用确保它们被持久化到磁盘上,以后可以访问。能不能两个都用?RDB + AOF:可以将 AOF 和 RDB 组合在同一个 Redis 实例中。如果以某种速度换取耐用性是一种折衷,那么您愿意做到。我认为这是设置 Redis 的一种可接受的方式。在重启的情况下,请记住如果两者都启用,Redis 将使用 AOF 来重建数据,因为它是最完整的。Fork同步现在我们了解了持久化的类型,让我们来讨论一下在像Redis这样的单线程应用程序中,我们究竟如何去做。在我看来,Redis最酷的部分是它如何利用fork和写时拷贝来促进数据的持久性。Fork是操作系统通过创建自己的副本来创建新进程的一种方式。通过这种方式,你可以得到一个新的进程ID和其他一些信息和句柄,所以新fork的进程(子进程)可以与原始进程的父进程对话。现在是事情变得有趣的地方。Redis是一个分配有大量内存的进程,那么它如何在不耗尽内存的情况下进行复制呢?当你fork一个进程时,父进程和子进程共享内存,在该子进程中Redis开始快照(Redis)进程。这是由一种称为 "写时复制 "的内存共享技术实现的--该技术传递对fork创建时的内存的引用。如果在子进程持久化到磁盘时没有发生变化,就不会进行新的分配。在有变化的情况下,内核会跟踪每个页面的引用,如果对特定页面有多个引用,那么这些变化会被写入新的页面。子进程完全不知道这些变化,并且有一致的内存快照。因此,只有一小部分内存被使用,我们能够非常快速和有效地实现潜在的数千兆字节内存的时间点快照!
2022年10月30日
32 阅读
0 评论
0 点赞
2022-10-29
如何利用Maven将代码打包成第三方公共jar包?
一、摘要在项目开发过程中,我们经常需要将一些公共方法提取出来,然后单独封装成一个第三方公共jar包,采用普通的方式打包后的jar,依赖的工程执行编译时,却提示找不到对应的依赖包,那么如何将工程打包为可执行jar包呢?下面向大家介绍三种通过maven将工程打包成可执行的打包方式。二、方法实践2.1、assembly插件2.1.1、pom.xml的相关配置文件如下<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.java</groupId> <artifactId>example-frame-fatJar</artifactId> <version>1.0.0</version> ..... <build> <finalName>sso-api</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- maven-assembly-plugin --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.6</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <encoding>UTF-8</encoding> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>2.1.2、运行如下命令,进行打包mvn clean package会在target文件夹里生成一个jar-with-dependencies的jar是可执行的。2.1.3、验证jar是否可执行在带有 jar-with-dependencies 的jar文件下,打开终端,输入如下命令#验证jar是否可执行,如果没有报错,说明没有问题 java -jar xxx-jar-with-dependencies.jar2.2、shade插件2.2.1、pom.xml的相关配置文件如下<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.example.java</groupId> <artifactId>example-frame-fatJar</artifactId> <version>1.0.0</version> ...... <build> <finalName>sso-api</finalName> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.2</version> <configuration> <source>1.7</source> <target>1.7</target> <encoding>UTF-8</encoding> </configuration> </plugin> <!-- maven-shade-plugin,不同的是shade可以将多个相同的配置文件追加合并 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>2.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.xxg.Main</mainClass> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.handlers</resource> </transformer> <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer"> <resource>META-INF/spring.schemas</resource> </transformer> </transformers> </configuration> </execution> </executions> </plugin> </plugins> </build> </project>2.2.2、运行如下命令,进行打包mvn clean package发现生成了2个jar包,一个为:original-XXX.jar,另一个为:XXX.jar,其中original...jar里只包含了工程自己的class文件,而另外的一个jar包则包含了工程本身以及所有依赖的jar包的class文件。我们只需要使用第二个jar包就可以了。2.2.3、验证jar是否可执行在XXX.jar文件下,打开终端,输入如下命令#验证jar是否可执行,如果没有报错,说明没有问题 java -jar XXX.jar不同点:shade可以将多个相同的配置文件追加合并,比如,多个子项目下有相同的配置文件,shade在打包的时候,会将相同的配置文件合并。2.3、Fatjar打包工具(eclipse插件)2.3.1、eclipse在线安装插件1、打开eclipse,打开菜单help > Install New Sofware > Add...name:FatJarURL:http://kurucz-grafika.de/fatjar根据提示下载安装并重启 eclipse!2.3.2、FatJar 使用在使用Eclipse进行导出时,点击项目右键,在弹出的右键菜单中选择 Build Fat Jar, 打开配置Fat Jar弹出框;或者,项目右键,点击Export,然后在打开的Export选择框中选择 Other 下面的 Fat Jar Exporter, 选择需要导出的项目,点击下一步打开配置Fat Jar弹出框。2.3.3、验证jar是否可执行在 XXX.jar 文件下,打开终端,输入如下命令#验证jar是否可执行,如果没有报错,说明没有问题 java -jar XXX.jar
2022年10月29日
11 阅读
0 评论
0 点赞
2022-10-22
如何编写一套多线程的测试用例?
一、摘要很多时候,新开发的功能在上线之前,我们都会进行压力测试,以防上线之后,突然出现性能瓶颈或者出现线程安全问题。那么问题来了,如何进行压力测试呢?实践的手段有很多种,比如采用 jmeter 、fiddler、postman 等第三方工具,可以快速实现性能压力测试。当然除此之外,其实我们也利用 java 的多线程特性,完全可以自行编写一套多线程的压力测试。下面我们以 访问百度首页服务 为例,向大家演示一下,采用 java 的多线程特性,该如何编写并发测试。二、代码实践方案一说到多线程,大家可能想到的就是实例化一个Thread对象,然后启动它,就可以实现异步处理,以模拟100个用户同时请求百度首页为例,代码实践如下:public static void main(String[] args) throws InterruptedException { //模拟100个线程,同时请求百度首页 long start = System.currentTimeMillis(); final int threadNum = 100; final CountDownLatch countDownLatch = new CountDownLatch(threadNum); for (int i = 0; i < threadNum; i++) { final int threadCount = i + 1; new Thread(new Runnable() { @Override public void run() { System.out.println("thread " + threadCount + " start"); //访问百度首页 String url = "https://www.baidu.com"; String rs = HttpUtils.getUrl(url); System.out.println("thread " + threadCount + " run result:" + rs); System.out.println("thread " + threadCount + " final"); //执行完成之后,计数器减一 countDownLatch.countDown(); } }).start(); } //线程同步阻塞 countDownLatch.await(); System.out.println("执行耗时:" + (System.currentTimeMillis() - start) + "ms"); }实践过程非常简单,采用Thread + CountDownLatch组合,进行阻塞测试。但是实际上往往我们进行多线程模拟用户进行访问某个服务的时候,每个用户的请求参数是不一样的,这个时候我们应该如何更加真实的贴近用户实际请求去测试呢?请看下面这个方案!方案二实际上在多线程并发编程中,它还有一个完美搭档,那就是 队列,采用 多线程+队列 组合编程模型,可以实现带任务的异步处理,并且性能高效!下面我们还是以 访问百度首页服务 为例,采用 多线程+队列 组合模式来模拟 100 个用户总共发起了1000次访问百度首页,代码实践如下!public static void main(String[] args) throws InterruptedException { //将每个用户访问百度服务的请求参数,存入阻塞队列BlockingQueue中 BlockingQueue<String> queue = new LinkedBlockingQueue<>(); for (int i = 0; i < 1000; i++) { queue.put("https://www.baidu.com?paramKey=" + i); } //模拟100个线程,执行1000次请求访问百度 long start = System.currentTimeMillis(); final int threadNum = 100; final CountDownLatch countDownLatch = new CountDownLatch(threadNum); for (int i = 0; i < threadNum; i++) { final int threadCount = i + 1; new Thread(new Runnable() { @Override public void run() { System.out.println("thread " + threadCount + " start"); boolean over = false; while (!over) { String url = queue.poll(); if(Objects.nonNull(url)) { //发起请求 String result =HttpUtils.getUrl(url); System.out.println("thread " + threadCount + " run result:" + result); }else { //任务结束 over = true; System.out.println("thread " + threadCount + " final"); countDownLatch.countDown(); } } } }).start(); } countDownLatch.await(); System.out.println("执行耗时:" + (System.currentTimeMillis() - start) + "ms"); }当然,你还可以自由调整线程数,也可以采用 juc 包的线程池来实现多线程编程,改造逻辑如下:public static void main(String[] args) throws InterruptedException { //将每个用户访问百度服务的请求参数,存入阻塞队列BlockingQueue中 BlockingQueue<String> queue = new LinkedBlockingQueue<>(); for (int i = 0; i < 1000; i++) { queue.put("https://www.baidu.com?paramKey=" + i); } //模拟100个线程,执行1000次请求访问百度 long start = System.currentTimeMillis(); final int threadNum = 100; //线程计数器 final CountDownLatch countDownLatch = new CountDownLatch(threadNum); //执行线程池 ExecutorService fixedThreadPool = Executors.newFixedThreadPool(threadNum); for (int i = 0; i < threadNum; i++) { final int threadCount = i + 1; fixedThreadPool.execute(new Runnable() { @Override public void run() { System.out.println("thread " + threadCount + " start"); boolean over = false; while (!over) { String url = queue.poll(); if(Objects.nonNull(url)) { //发起请求 String result =HttpUtils.getUrl(url); System.out.println("thread " + threadCount + " run result:" + result); }else { //任务结束 over = true; System.out.println("thread " + threadCount + " final"); countDownLatch.countDown(); } } } }); } countDownLatch.await(); fixedThreadPool.shutdown(); System.out.println("执行耗时:" + (System.currentTimeMillis() - start) + "ms"); }其中 BlockingQueue 阻塞队列,支持线程数据共享,当一个线程把数据取出之后,另一个线程无法再取,最后的运行效果是一样的!
2022年10月22日
27 阅读
0 评论
0 点赞
2022-07-10
Spring Boot 接入支付宝,手把手带你实战!
支付宝推出了新的转账接口 alipay.fund.trans.uni.transfer(升级后安全性更高,功能更加强大) ,老转账接口 alipay.fund.trans.toaccount.transfer 将不再维护,新老接口的一个区别就是新接口采用的证书验签方式。1、将支付宝开放平台里下载的3个证书放在 resources 下面2、写支付宝支付的配置文件 alipay.propertiesalipay.appId=你的应用id alipay.serverUrl=https://openapi.alipay.com/gateway.do alipay.privateKey=你的应用私钥 alipay.format=json alipay.charset=UTF-8 alipay.signType=RSA2 alipay.appCertPath=/cert/appCertPublicKey_2021001164652941.crt alipay.alipayCertPath=/cert/alipayCertPublicKey_RSA2.crt alipay.alipayRootCertPath=/cert/alipayRootCert.crt3、引入pom依赖<dependency> <groupId>com.alipay.sdk</groupId> <artifactId>alipay-sdk-java</artifactId> <version>4.10.97.ALL</version> </dependency>4、将配置信息注入AliPayBeanimport lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.PropertySource; import org.springframework.stereotype.Component; @Component @PropertySource("classpath:/production/alipay.properties") @ConfigurationProperties(prefix = "alipay") @Data public class AliPayBean { private String appId; private String privateKey; private String publicKey; private String serverUrl; private String domain; private String format; private String charset; private String signType; private String appCertPath; private String alipayCertPath; private String alipayRootCertPath; }5、写配置类import com.alipay.api.AlipayClient; import com.alipay.api.CertAlipayRequest; import com.alipay.api.DefaultAlipayClient; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.util.FileCopyUtils; import java.io.InputStream; @Configuration public class AliConfig { @Value("${custom.http.proxyHost}") private String proxyHost; @Value("${custom.http.proxyPort}") private int proxyPort; @Value("${spring.profiles.active}") private String activeEnv; @Autowired private AliPayBean aliPayBean; @Bean(name = {"alipayClient"}) public AlipayClient alipayClientService() throws Exception{ CertAlipayRequest certAlipayRequest = new CertAlipayRequest(); //设置网关地址 certAlipayRequest.setServerUrl(aliPayBean.getServerUrl()); //设置应用Id certAlipayRequest.setAppId(aliPayBean.getAppId()); //设置应用私钥 certAlipayRequest.setPrivateKey(aliPayBean.getPrivateKey()); //设置请求格式,固定值json certAlipayRequest.setFormat(aliPayBean.getFormat()); //设置字符集 certAlipayRequest.setCharset(aliPayBean.getCharset()); //设置签名类型 certAlipayRequest.setSignType(aliPayBean.getSignType()); //如果是生产环境或者预演环境,则使用代理模式 if ("prod".equals(activeEnv) || "stage".equals(activeEnv) || "test".equals(activeEnv)) { //设置应用公钥证书路径 certAlipayRequest.setCertContent(getCertContentByPath(aliPayBean.getAppCertPath())); //设置支付宝公钥证书路径 certAlipayRequest.setAlipayPublicCertContent(getCertContentByPath(aliPayBean.getAlipayCertPath())); //设置支付宝根证书路径 certAlipayRequest.setRootCertContent(getCertContentByPath(aliPayBean.getAlipayRootCertPath())); certAlipayRequest.setProxyHost(proxyHost); certAlipayRequest.setProxyPort(proxyPort); }else { //local String serverPath = this.getClass().getResource("/").getPath(); //设置应用公钥证书路径 certAlipayRequest.setCertPath(serverPath+aliPayBean.getAppCertPath()); //设置支付宝公钥证书路径 certAlipayRequest.setAlipayPublicCertPath(serverPath+aliPayBean.getAlipayCertPath()); //设置支付宝根证书路径 certAlipayRequest.setRootCertPath(serverPath+aliPayBean.getAlipayRootCertPath()); } return new DefaultAlipayClient(certAlipayRequest); } public String getCertContentByPath(String name){ InputStream inputStream = null; String content = null; try{ inputStream = this.getClass().getClassLoader().getResourceAsStream(name); content = new String(FileCopyUtils.copyToByteArray(inputStream)); }catch (Exception e){ e.printStackTrace(); } return content; } }6、写支付工具类import com.alipay.api.AlipayApiException; import com.alipay.api.AlipayClient; import com.alipay.api.domain.AlipayTradeAppPayModel; import com.alipay.api.domain.AlipayTradeQueryModel; import com.alipay.api.request.AlipayTradeAppPayRequest; import com.alipay.api.request.AlipayTradeQueryRequest; import com.alipay.api.response.AlipayTradeAppPayResponse; import com.alipay.api.response.AlipayTradeQueryResponse; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.stereotype.Service; /** * @description:支付宝工具类 * @Date:2022-07-10 */ @Slf4j @Service public class AliPayUtils { @Autowired @Qualifier("alipayClient") private AlipayClient alipayClient; /** * 交易查询接口 * @param request * @return * @throws Exception */ public boolean isTradeQuery(AlipayTradeQueryModel model) throws AlipayApiException { AlipayTradeQueryRequest request = new AlipayTradeQueryRequest(); request.setBizModel(model); AlipayTradeQueryResponse alipayTradeQueryResponse = alipayClient.certificateExecute(request); if(alipayTradeQueryResponse.isSuccess()){ return true; } else { return false; } } /** * app支付 * @param model * @param notifyUrl * @return * @throws AlipayApiException */ public String startAppPay(AlipayTradeAppPayModel model, String notifyUrl) throws AlipayApiException { AlipayTradeAppPayRequest aliPayRequest = new AlipayTradeAppPayRequest(); model.setProductCode("QUICK_MSECURITY_PAY"); aliPayRequest.setNotifyUrl(notifyUrl); aliPayRequest.setBizModel(model); // 这里和普通的接口调用不同,使用的是sdkExecute AlipayTradeAppPayResponse aliResponse = alipayClient.sdkExecute(aliPayRequest); return aliResponse.getBody(); } /** * 转账接口 * * @param transferParams * @return AlipayFundTransToaccountTransferResponse */ public AlipayFundTransUniTransferResponse doTransferNew(TransferParams transferParams) throws Exception { String title = (StringUtils.isNotBlank(transferParams.getRemark()) ? transferParams .getRemark() : "转账"); //转账请求入参 AlipayFundTransUniTransferRequest request = new AlipayFundTransUniTransferRequest(); //转账参数 BizContentForUniTransfer bizContent = new BizContentForUniTransfer(); bizContent.setOut_biz_no(transferParams.getOutBizNo()); bizContent.setTrans_amount(MathUtil.changeF2Y(Math.abs(Integer.parseInt(transferParams.getAmount())))); bizContent.setProduct_code("TRANS_ACCOUNT_NO_PWD"); bizContent.setBiz_scene("DIRECT_TRANSFER"); bizContent.setOrder_title(title); Participant participant = new Participant(); participant.setIdentity(transferParams.getPayeeAccount()); participant.setIdentity_type(transferParams.getPayeeType()); participant.setName((StringUtils.isNotBlank(transferParams.getPayeeRealName()) ? transferParams .getPayeeRealName() : StringUtils.EMPTY)); bizContent.setPayee_info(participant); bizContent.setRemark(title); request.setBizContent(JSON.toJSONString(bizContent)); //转账请求返回 AlipayFundTransUniTransferResponse response = null; try { response = alipayClient.certificateExecute(request); } catch (Exception e) { log.info("doTransfer exception,异常信息:{}", e.toString()); log.info("doTransfer exception,支付宝返回信息:{}", JSONObject.toJSONString(response)); } log.info("doTransfer,AlipayFundTransUniTransferResponse:{}", JSONObject.toJSONString(response)); return response; } }Tips:转账用到的类@Data public class TransferParams { /** * 应用编号 */ private Long appId; /** * 创建人id */ private Long createdBy; /** * 转账业务订单号 */ private String outBizNo; /** * 收款方识别方式 */ private String payeeType; /** * 收款方账号,可以是支付宝userId或者支付宝loginId */ private String payeeAccount; /** * 转账金额,单位分 */ private String amount; /** * 付款方名称 */ private String payerShowName; /** * 收款方名称 */ private String payeeRealName; /** * 备注 */ private String remark; /** * 支付宝转账流水号 */ private String orderId; }import lombok.Data; import java.math.BigDecimal; /** * 支付宝转账参数 */ @Data public class BizContentForUniTransfer { /** * 业务订单号 */ private String out_biz_no; /** * 订单总金额,单位为元,精确到小数点后两位, */ private BigDecimal trans_amount; /** * 业务产品码, * 单笔无密转账到支付宝账户固定为:TRANS_ACCOUNT_NO_PWD; * 单笔无密转账到银行卡固定为:TRANS_BANKCARD_NO_PWD; * 收发现金红包固定为:STD_RED_PACKET; */ private String product_code; /** * 描述特定的业务场景,可传的参数如下: * DIRECT_TRANSFER:单笔无密转账到支付宝/银行卡, B2C现金红包; * PERSONAL_COLLECTION:C2C现金红包-领红包 */ private String biz_scene; /** * 转账业务的标题,用于在支付宝用户的账单里显示 */ private String order_title; /** * 原支付宝业务单号。C2C现金红包-红包领取时,传红包支付时返回的支付宝单号; * B2C现金红包、单笔无密转账到支付宝/银行卡不需要该参数。 */ private String original_order_id; /** * 业务备注 */ private String remark; /** * 转账业务请求的扩展参数,支持传入的扩展参数如下: * 1、sub_biz_scene 子业务场景,红包业务必传,取值REDPACKET,C2C现金红包、B2C现金红包均需传入; * 2、withdraw_timeliness为转账到银行卡的预期到账时间,可选(不传入则默认为T1), * 取值T0表示预期T+0到账,取值T1表示预期T+1到账,因到账时效受银行机构处理影响,支付宝无法保证一定是T0或者T1到账; */ private String business_params; /** * 支付收款对象 */ private Participant payee_info; }@Data public class Participant { /** * 参与方的唯一标识 */ private String identity; /** * 参与方的标识类型,目前支持如下类型: * 1、ALIPAY_USER_ID 支付宝的会员ID * 2、ALIPAY_LOGON_ID:支付宝登录号,支持邮箱和手机号格式 */ private String identity_type; /** * 参与方真实姓名,如果非空,将校验收款支付宝账号姓名一致性。 * 当identity_type=ALIPAY_LOGON_ID时,本字段必填。 */ private String name; }
2022年07月10日
14 阅读
0 评论
0 点赞
2022-05-18
2022最新版Macbook安装配置Maven环境教程
方法一:brew 一键安装在终端输入命令 brew install maven,并自动配置好了环境变量MacBook 没安装 brew 的看下面方法二,或者先装 brew然后就可以在终端输入 mvn -v 查看版本方法二:官网下载,手动安装官网下载并解压 Maven打开 Maven 官网下载页面: https://maven.apache.org/download.cgi 比如下载最新的: apache-maven-3.8.5-bin.tar.gz解压下载的安装包到某一目录,比如:/usr/loca/注意:apache-maven-3.8.5 此名称不可以改变配置环境变量打开终端窗口,输入 open ~/.bash_profile,打开 .bash_profile 文件向文件中添加以下内容:# MAVEN PATH MAVEN_HOME=/usr/local/apache-maven-3.8.5 export MAVEN_HOME export PATH=$PATH:$MAVEN_HOME/bin添加之后退出,终端执行以下命令使配置生效source ~/.bash_profile最后终端执行 mvn -version 能看到正确版本号即配置正确
2022年05月18日
65 阅读
0 评论
1 点赞
1
2
...
5