或是出于优化 SEO,或是出于加强网站体验,很多博客都给文章中的外部链接加上了个二次跳转,本博客也不例外。

比如说我在这插入一个 百度 的超链接,你点击访问后先会被跳转到本博客的跳转页面,一段动画后才会真的转至百度的首页。

看似合情合理,实则暗藏一个小漏洞!

情境分析

举个栗子,假设有个不怀好意的坏蛋,要传播一个不怀好意的网站 www.baidu.com (这里用百度做示范好了。。),通常这类网站一发到 QQ 里 QQ 就会有一个大大的红色叹号以示危险:

保护 Go 跳转,防止被恶意利用

如果利用像本站的跳转功能,就能轻易地将链接“洗白”:

http://blog.wanyunet.com/go?url=www.baidu.com

要是有不明真相的小白点击了,在本站的“跳转”下最终访问的就会是恶意的链接网址,无辜的链接跳转功能瞬间成了“帮凶”。

解决办法

那么如何防止这种情况的发生呢?

我的思路是这样的:

在跳转页面中,先用 PHP 的‘$_SERVER[“HTTP_REFERER”]’函数获取来源链接,然后判断来源链接是不是属于本站,如果不是,再判断跳转链接,如果跳转链接也不属于本站,就给出一个提示:你将要访问的网站不属于本站范围,请谨慎访问。反之则正常跳转。

这里有个难点就是判断目标网址是不是属于本站的一个页面,也就是对“敌我”的一个判断。

比方说本站的网址是 http://blog.wanyunet.com ,一些二级页面如 http://www.wanyunet.com、https://blog.wanyunet.com/demo 也属于本站。而 http://www.baidu.com/?blog.wanyunet.com 虽然包含“wanyunet.com”,访问后它却是百度的首页。因此不能简单粗暴的通过判断网址中是否存在 “wanyunet.com” 来得出结论

仔细的观察域名的结构你就会发现只要是属于 wanyunet.com 的网址一定会满足以下规律:

  • 如果 wanyunet.com 的前面有内容,那么一定不会有“?”出现,比如说 abc?.wanyunet.com 是不存在的
  • 如果 wanyunet.com 的后面还跟有内容,那么一定是“/”,或“?”,比如说 wanyunet.com/123 或 wanyunet.com?from=123

利用以上两个规律写了个判断函数如下:

/**
 * 判断是不是自己的域名
 * @param $domain 要进行判断的域名
 * @param $my 自己的域名
 * @return 对比结果
 */
function isMyDomain($domain, $my) {
    preg_match('/([^\?]*)/i', $domain, $match);
    if(isset($match[1])) $domain = $match[1];
    preg_match('/([\w-]*\.[\w-]*)\/.*/i', $domain.'/', $match);
    if(isset($match[1]) && $match[1] == $my) return true;
    return false;
}

最终代码

按照以上思路完成整个跳转页面的编写后,测试了一下效果非常不错:只要是从本站点开的跳转网址,都能正常进行跳转,从而第三方打开的跳转,则会弹出提示。

最终完整的跳转页面源代码如下:(代码中的判断思路肯定不唯一,如果您有更好的判断方式,欢迎在下方留言交流!)

<?php  
/** 
 * 带有来路验证和跳转提示功能的跳转页面 
 * @auth 万裕Get
 * @authUrl http://blog.wanyunet.com 
 * @data 2019/5/8 
 * @url http://blog.wanyunet.com 
 */  
  
// 请将这里的网址改为自己的(顶级)域名地址  
$myDomain = 'wanyunet.com';  
  
// 这里用正则提取 $_SERVER["QUERY_STRING"] 而不是直接 get url  
// 是因为如果链接中自身带有 GET 参数则会导致获取不完整  
preg_match('/url=(.*)/i', $_SERVER["QUERY_STRING"], $jumpUrl);   
  
// 如果没获取到跳转链接,直接跳回首页  
if(!isset($jumpUrl[1])) {  
    header("location:/");  
    exit();  
}  
  
$jumpUrl = $jumpUrl[1];  
  
// 判断是否包含 http:// 头,如果没有则加上  
preg_match('/(http|https):\/\//', $jumpUrl, $matches);      
  
$url = $matches? $jumpUrl: 'http://'. $jumpUrl;  
  
  
// 判断网址是否完整  
preg_match('/[\w-]*\.[\w-]*/i', $url, $matche);      
  
// 是否需要给出跳转提示  
$echoTips = false;  
  
if($matche){  
    // 如果是本站的链接,不展示动画直接跳转  
    if(isMyDomain($url, $myDomain)) {  
        header("location:{$url}");  
        exit();    // 后续操作不再执行  
    }  
      
    $title = '页面加载中,请稍候...';  
    $fromUrl = isset($_SERVER["HTTP_REFERER"])? $_SERVER["HTTP_REFERER"]: ''; // 获取来源url  
      
    // 如果来源和跳转后的地址都不是本站,那么就要给出提示  
    if(!isMyDomain($fromUrl, $myDomain)) {  
        $echoTips = true;  
    }  
} else {    // 网址参数不完整  
    $url = '/';  
    $title = '参数错误,正在返回首页...';  
}  
  
  
/** 
 * 判断是不是自己的域名 
 * @param $domain 要进行判断的域名 
 * @param $my 自己的域名 
 * @return 对比结果 
 */  
function isMyDomain($domain, $my) {  
    preg_match('/([^\?]*)/i', $domain, $match);  
    if(isset($match[1])) $domain = $match[1];  
    preg_match('/([\w-]*\.[\w-]*)\/.*/i', $domain.'/', $match);  
    if(isset($match[1]) && $match[1] == $my) return true;  
    return false;  
}  
  
?> 
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    
    <?php  
		if($echoTips) {  
			echo '<title>跳转提示</title>';  
		} else {  
			echo '<meta http-equiv="refresh" content="0;url='.$url.'">';  
			echo '<title>'.$title.'</title>';  
		}  
		?>
	
    <style>
        body{background:#000}.loading{-webkit-animation:fadein 2s;-moz-animation:fadein 2s;-o-animation:fadein 2s;animation:fadein 2s}@-moz-keyframes fadein{from{opacity:0}to{opacity:1}}@-webkit-keyframes fadein{from{opacity:0}to{opacity:1}}@-o-keyframes fadein{from{opacity:0}to{opacity:1}}@keyframes fadein{from{opacity:0}to{opacity:1}}.spinner-wrapper{position:absolute;top:0;left:0;z-index:300;height:100%;min-width:100%;min-height:100%;background:rgba(255,255,255,0.93)}.spinner-text{position:absolute;top:50%;left:50%;margin-left:-90px;margin-top: 2px;color:#BBB;letter-spacing:1px;font-weight:700;font-size:36px;font-family:Arial}.spinner{position:absolute;top:50%;left:50%;display:block;margin-left:-160px;width:1px;height:1px;border:25px solid rgba(100,100,100,0.2);-webkit-border-radius:50px;-moz-border-radius:50px;border-radius:50px;border-left-color:transparent;border-right-color:transparent;-webkit-animation:spin 1.5s infinite;-moz-animation:spin 1.5s infinite;animation:spin 1.5s infinite}@-webkit-keyframes spin{0%,100%{-webkit-transform:rotate(0deg) scale(1)}50%{-webkit-transform:rotate(720deg) scale(0.6)}}@-moz-keyframes spin{0%,100%{-moz-transform:rotate(0deg) scale(1)}50%{-moz-transform:rotate(720deg) scale(0.6)}}@-o-keyframes spin{0%,100%{-o-transform:rotate(0deg) scale(1)}50%{-o-transform:rotate(720deg) scale(0.6)}}@keyframes spin{0%,100%{transform:rotate(0deg) scale(1)}50%{transform:rotate(720deg) scale(0.6)}}
		html,body,div,dl,dt,dd,ul,ol,li,h1,h2,h3,h4,h5,h6,pre,code,form,fieldset,legend,input,textarea,select,p,blockquote,th,td{margin:0;padding:0}
ol,ul{list-style:none}
h1,h2,h3,h4,h5,h6{font-size:100%;font-weight:normal}
input,textarea,select{font-family:inherit;font-size:inherit;font-weight:inherit}
input,textarea,select{*font-size:100%}
body{font-family:"Microsoft YaHei",Arial,sans-serif;font-size:12px;color:#333}
a{color:#747474;text-decoration:none;cursor:pointer}
input,button{outline:none}
html,body{height:100%}
body{background-color:#fcfcfc}
body{height:100vh;font-family:"微软雅黑";overflow:hidden}
html,body{width:100%;height:100%}
body .center-box{left:0;right:0;top:0;bottom:150px;height:250px;width:100%;max-width:600px;position:absolute;margin:auto;z-index:10;text-align:left;box-sizing:border-box;padding:10px}
.jump-tips h3{width:100%;height:48px;font-size:28px;line-height:44px;padding-left:57px;box-sizing:border-box;position:relative}
.jump-tips h3 span{width:46px;height:46px;background-color:#f34c3c;border-radius:50%;display:inline-block;position:absolute;left:0;top:0}
.jump-tips h3 span i{width:4px;height:20px;background-color:#fff;border-radius:2px;display:block;margin:10px auto 4px}
.jump-tips h3 span em{width:4px;height:4px;background-color:#fff;border-radius:2px;display:block;margin:0 auto}
.jump-tips dl{width:100%;height:auto;overflow:hidden;box-sizing:border-box;padding-left:57px;color:#404040;font:14px/24px "微软雅黑"}
.jump-tips dl dt a{color:#2b92f2}
.jump-tips dl dt a:hover{text-decoration:underline}
.jump-tips dl dd{width:100%;height:auto;overflow:hidden;box-sizing:border-box;margin-top:10px;color:#858585;font-size:12px}
.jump-tips .button{width:100%;height:33px;position:absolute;bottom:0;left:0}
.jump-tips .button .button-left{float:left;margin-left:58px}
.jump-tips .button .button-left label{width:110px;height:34px;color:#858585;font-size:12px;line-height:34px;box-sizing:border-box;padding-left:20px;position:relative;cursor:default;user-select:none}
.jump-tips .button .button-left label input{position:absolute;left:0;top:0px;background-color:#fff}
input[type='checkbox']{-webkit-appearance:none;border-radius:2px;height:16px;width:16px;background-color:#fff;border:1px solid #A6A6A6}
input[type='checkbox']:hover{border-color:#8C8C8C}
input[type='checkbox']:checked:hover{border-color:#0DC561}
input[type='checkbox']:checked::before{color:#808080;content:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAAKCAYAAACE2W/HAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAABx0RVh0U29mdHdhcmUAQWRvYmUgRmlyZXdvcmtzIENTNAay06AAAACBSURBVCiRlc2xCcJQFEDRE7VI2oCV4A6CvaiIvQs4kLOIDmAvZAALxVZwAbGwsHoQUvwktz/cbFzt9SjDEL9BD1SiwhN5V1jigjk+XY+BZrhjHXCEK47IW9ASLwg4xQ5nFG0o4BcbvLHFCZMUCgg3rGr4kUJ12MRFCjVh4AUOKQR/PfIlGJGAEgYAAAAASUVORK5CYII=);font-size:13px;height:16px;left:0px;top:-3px;position:absolute}
.jump-tips .button .button-right{padding-right:10px;width:235px;height:33px;float:right;position:relative}
.jump-tips .button .button-right a:last-child{display:inline-block;width:107px;height:31px;border:1px solid #0DC561;background-image:linear-gradient(150deg,#15ca5f,#10ce67);color:#fff;border-radius:3px;text-align:center;font-size:14px;line-height:31px;cursor:pointer;user-select:none}
.jump-tips .button .button-right a:last-child:hover{border-color:#0BD166;background-image:linear-gradient(150deg,#10d560,#12dd6f)}
.jump-tips .button .button-right a:last-child:active{border-color:#0EC361;background-image:linear-gradient(150deg,#12c35a,#10cc65)}
.jump-tips .button .button-right a:first-child{width:108px;height:33px;color:#1c8af1;margin-right:12px;font:12px/33px "微软雅黑";cursor:pointer}
.jump-tips .button .button-right a:first-child:hover{text-decoration:underline}
body .loader_overlay{width:150px;height:150px;background:transparent;box-shadow:0px 0px 0px 1000px rgba(255,255,255,0.67),0px 0px 19px 0px rgba(0,0,0,0.16) inset;border-radius:100%;z-index:-1;position:absolute;left:0;right:0;top:0;bottom:0;margin:auto}
body .loader_cogs{z-index:-2;width:100px;height:100px;top:-120px !important;position:absolute;left:0;right:0;top:0;bottom:0;margin:auto}
body .loader_cogs__top{position:relative;width:100px;height:100px;-webkit-transform-origin:50px 50px;transform-origin:50px 50px;-webkit-animation:rotate 6s infinite linear;animation:rotate 6s infinite linear}
body .loader_cogs__top div:nth-of-type(1){-webkit-transform:rotate(30deg);transform:rotate(30deg)}
body .loader_cogs__top div:nth-of-type(2){-webkit-transform:rotate(60deg);transform:rotate(60deg)}
body .loader_cogs__top div:nth-of-type(3){-webkit-transform:rotate(90deg);transform:rotate(90deg)}
body .loader_cogs__top div.top_part{width:100px;border-radius:10px;position:absolute;height:100px;background:#f98db9}
body .loader_cogs__top div.top_hole{width:50px;height:50px;border-radius:100%;background:white;position:absolute;position:absolute;left:0;right:0;top:0;bottom:0;margin:auto}
body .loader_cogs__left{position:relative;width:80px;-webkit-transform:rotate(16deg);transform:rotate(16deg);top:28px;-webkit-transform-origin:40px 40px;transform-origin:40px 40px;-webkit-animation:rotate_left 3s .1s infinite reverse linear;animation:rotate_left 3s .1s infinite reverse linear;left:-24px;height:80px}
body .loader_cogs__left div:nth-of-type(1){-webkit-transform:rotate(30deg);transform:rotate(30deg)}
body .loader_cogs__left div:nth-of-type(2){-webkit-transform:rotate(60deg);transform:rotate(60deg)}
body .loader_cogs__left div:nth-of-type(3){-webkit-transform:rotate(90deg);transform:rotate(90deg)}
body .loader_cogs__left div.left_part{width:80px;border-radius:6px;position:absolute;height:80px;background:#97ddff}
body .loader_cogs__left div.left_hole{width:40px;height:40px;border-radius:100%;background:white;position:absolute;position:absolute;left:0;right:0;top:0;bottom:0;margin:auto}
body .loader_cogs__bottom{position:relative;width:60px;top:-65px;-webkit-transform-origin:30px 30px;transform-origin:30px 30px;-webkit-animation:rotate_left 2s infinite linear;animation:rotate_left 2s infinite linear;-webkit-transform:rotate(4deg);transform:rotate(4deg);left:79px;height:60px}
body .loader_cogs__bottom div:nth-of-type(1){-webkit-transform:rotate(30deg);transform:rotate(30deg)}
body .loader_cogs__bottom div:nth-of-type(2){-webkit-transform:rotate(60deg);transform:rotate(60deg)}
body .loader_cogs__bottom div:nth-of-type(3){-webkit-transform:rotate(90deg);transform:rotate(90deg)}
body .loader_cogs__bottom div.bottom_part{width:60px;border-radius:5px;position:absolute;height:60px;background:#ffcd66}
body .loader_cogs__bottom div.bottom_hole{width:30px;height:30px;border-radius:100%;background:white;position:absolute;position:absolute;left:0;right:0;top:0;bottom:0;margin:auto}
.loading-text{font-size:20px;position:absolute;bottom:-40px;text-align:center;left:0;right:0;color:#b9b9b9}
dot{display:inline-block;height:1em;line-height:1;text-align:left;vertical-align:-.25em;overflow:hidden}
dot::before{display:block;content:'...\A..\A.';white-space:pre-wrap;animation:dot 2s infinite step-start both}
@keyframes dot{33%{transform:translateY(-2em)}
66%{transform:translateY(-1em)}
}@-webkit-keyframes rotate{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}
to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}
}@keyframes rotate{from{-webkit-transform:rotate(0deg);transform:rotate(0deg)}
to{-webkit-transform:rotate(360deg);transform:rotate(360deg)}
}@-webkit-keyframes rotate_left{from{-webkit-transform:rotate(16deg);transform:rotate(16deg)}
to{-webkit-transform:rotate(376deg);transform:rotate(376deg)}
}@keyframes rotate_left{from{-webkit-transform:rotate(16deg);transform:rotate(16deg)}
to{-webkit-transform:rotate(376deg);transform:rotate(376deg)}
}@-webkit-keyframes rotate_right{from{-webkit-transform:rotate(4deg);transform:rotate(4deg)}
to{-webkit-transform:rotate(364deg);transform:rotate(364deg)}
}@keyframes rotate_right{from{-webkit-transform:rotate(4deg);transform:rotate(4deg)}
to{-webkit-transform:rotate(364deg);transform:rotate(364deg)}
}
@media screen and (max-width: 500px) {body .center-box {padding: 15px;}.button-left {display: none;}.jump-tips dl {padding-left: 5px;}.jump-tips .button .button-right {padding-right: 15px;}}
    </style>
</head>
<body>
	<?php if($echoTips) { ?>  
	<div class='center-box jump-tips'>
    <h3>
        <span class="alert-icon"><i></i><em></em></span>
        页面跳转提示
    </h3>
    <dl>
      <dt>您将要访问的网站不属于万裕Get,
        我们无法确认该网页是否安全,它可能包含未知的安全隐患。</dt>
      <dd>您访问的网址是:<span id="url"><?php echo $url;?></span></dd>
    </dl>
    <div class="button">
        <div class="button-left">
            <label>
                <input type="checkbox" id="trust_url"> 不再提示此消息
            </label>
        </div>
        <div class="button-right">
            <a id="go_on" href="<?php echo $url;?>" rel="nofollow">忽略警告,继续访问</a>
            <a id="close" onclick="closePage()">关闭页面</a>
        </div>
    </div>
</div>
	<script>
		function closePage() {
			/* 设个定时器,如果页面未关闭,则跳转至首页 */
			setTimeout(function() { 
				window.location.href = 'http://blog.wanyunet.com';
			}, 200);

			/* 通用窗口关闭 */ 
			window.opener=null;
			window.open('','_self');
			window.close();
			/* 微信浏览器关闭 */ 
			WeixinJSBridge.call('closeWindow');
		}
	</script>
	
	 <?php } else { ?>  
	<div class="loading">
		<div class="spinner-wrapper">
			<span class="spinner-text">页面加载中,请稍候...</span>
			<span class="spinner"></span>
		</div>
	</div>
	<?php } ?>  
</body>
</html>

效果演示

还是以百度为例,如果你 点击 http://blog.wanyunet.com/go/?url=www.baidu.com 访问,直接就跳转了,如果你手动复制这个跳转网址再粘贴到浏览器访问,则会弹出提示。

如果是本站的站内链接,如 http://blog.wanyunet.com/go/?url=www.wanyunet.com 无论以何种方式打开都是直接跳转。