使用 Razor Pages 打造查看真实 IP、IPv6 检测网站系列文章:
欢迎大家阅读 ASP.NET Core Razor Pages 打造查看真实 IP、IPv6 系列文章,本篇是系列的第二篇,详细介绍代码的编写。如果错过了《使用 Razor Pages 打造查看真实 IP、IPv6 检测网站 —— 思路篇》,可以点击链接阅读。
本篇文章对应的代码可以在 https://github.com/huhubun/BunIp/tree/simple_ver 查看到。
完整实现可以访问 https://ip.bun.plus/ 体验。
开始之前
在上一篇我们已经创建好了一个空的 Razor 页面项目。
但开始编码之前,我们修改一下 Visual Studio 的配置。在 Visual Studio 的调试按钮附近找到 IIS Express 的字样,点击旁边的下拉菜单,修改为和项目一致的选项,表示使用 ASP.NET Core 的 Kestrel 服务器进行调试。
当你运行项目时,弹出一个控制台窗口而非启动 IIS Express 时,说明配置成功。
获取真实 IP
在 Pages
文件夹下找到 Index.cshtml
,它就是我们的首页。Index.cshtml
是 Razor 页面,它还有一个以 .cs
后缀结尾的 IndexModel
类。在 Visual Studio 的“解决方案资源管理器”中,展开 Index.cshtml
文件前面的小三角,或者在 Index.cshtml
文件内容的空白处点击右键,选择 转到 PageModel
,都可以进入页面对应的 Page Model 类中。在 Razor 页面里可以使用 @Model.XXX
这样的方式访问到 Page Model 里的属性。
为了能让页面显示请求者的 IP 地址,按照我们的设计,只需要读取 X-Real-IP
头即可。不过为了兼顾通过 nginx 访问(上线后)和直接访问站点(开发调试时),我们需要对 X-Real-IP
的值进行判断。因为开发中访问的时候是没有这个头的,就需要显示 Kestrel 获取到的请求者的地址。我们添加一个帮助类完成这个操作,新建 Helpers
文件夹,并在其中新建 IpHelper.cs
文件:
using Microsoft.AspNetCore.Http;using System;using System.Net;namespace IpTest.Helpers
{
public static class IpHelper
{
public static IPAddress GetRealIp(HttpContext httpContext)
{
// 如果存在 X-Real-IP header,并且值合法,就是用 X-Real-IP 的值
var headers = httpContext.Request.Headers;var realIpHeader = headers["X-Real-IP"];if (!String.IsNullOrEmpty(realIpHeader) && IPAddress.TryParse(realIpHeader, out var ipAddress))
{
return ipAddress;}
return httpContext.Connection.RemoteIpAddress;}
}
}
然后在 IndexModel
里增加一个只读属性 DisplayIp
,它的值来自于上面的帮助方法获取的 IP 地址:
public IPAddress DisplayIp => IpHelper.GetRealIp(HttpContext);```
接着修改 `Index.cshtml`,把原有的内容替换成这样:
@page @model IndexModel @{ ViewData["Title"] = "Home page";}
@Model.DisplayIp
可以看到我们使用了 @Model.DisplayIp
获取 Model 的内容。按下 Ctrl + F5 运行项目,因为是通过 localhost
访问的,所以我们看见的地址是 IPv6 的 ::1
(也有可能是 127.0.0.1
,取决于操作系统是优先使用 IPv6 还是 IPv4):
向 appsettings.json 增加配置
根据设计,需要准备三个地址,我们把这三个地址添加到配置中,因为要想发起 ajax 请求需要知道请求的地址是什么。
打开 appsettings.Development.json
文件(如果没有可以手动创建一个),在原有内容同一层级增加 IpTest
节点用于存放我们的配置:
{
// 原有的内容省略
// ...
"IpTest": {
// 部署站点信息,通过请求头中的 Host 进行识别
"DeploySite": {
// 混合部署,同时支持 IPv4 和 IPv6 访问
"Hybrid": {
// 域名(不需要填写 HTTP 协议,例如 ip.bun.plus)
"Domain": "localhost",
// 协议(http、https)
"Scheme": "http",
// 端口号
"Port": "5000"
},
// IPv4 Only
"IPv4": {
"Domain": "127.0.0.1",
"Scheme": "http",
"Port": "14444"
},
// IPv6 Only
"IPv6": {
"Domain": "[::1]",
"Scheme": "http",
"Port": "16666"
}
}
}
可以看到,我们有了一个名为 DeploySite
部署站点信息的节点,里面包含了三个地址的域名、协议以及端口号信息。
接着为这个配置创建一个类,以便在程序中直接访问到它们的值。在项目根目录下创建文件夹 /Configs/DeploySites
,并在其中创建 SiteInfo.cs
:
namespace IpTest.Configs.DeploySites
{
public class SiteInfo
{
/// <summary>
/// 域名
/// </summary>
public string Domain { get;set;}
/// <summary>
/// 访问协议(http、https)
/// </summary>
public string Scheme { get;set;}
/// <summary>
/// 端口号
/// </summary>
public int? Port { get;set;}
public Uri Uri
{
get
{
if (Port.HasValue)
{
return (new UriBuilder(Scheme, Domain, Port.Value)).Uri;}
return (new UriBuilder(Scheme, Domain)).Uri;}
}
}
}
最后的 Uri
只读属性用于根据站点的信息生成 Uri,这样就不用我们自己拼字符串了,看起来舒服一点,后面会用到。
然后到 Configs
文件夹下创建 DeploySite.cs
:
using IpTest.Configs.DeploySites;namespace IpTest.Configs
{
public class DeploySite
{
/// <summary>
/// 混合部署,同时支持 IPv4 和 IPv6 访问
/// </summary>
public SiteInfo Hybrid { get;set;}
/// <summary>
/// IPv4 Only
/// </summary>
public SiteInfo IPv4 { get;set;}
/// <summary>
/// IPv6 Only
/// </summary>
public SiteInfo IPv6 { get;set;}
}
}
为了方便的访问和标识站点的部署模式,再在 Configs
文件夹下创建一个枚举 DeployMode
:
namespace IpTest.Configs
{
public enum DeployMode
{
Hybrid,
IPv4,
IPv6
}
}
最后来到 Startup.cs
,把配置的读取加入到 ConfigureServices()
中,以便后续可以通过依赖注入使用它:
public void ConfigureServices(IServiceCollection services)
{
services.AddRazorPages();services.Configure<DeploySite>(Configuration.GetSection("IpTest:DeploySite"));}
注:IpTest:DeploySite
表示 IpTest
节点下的 DeploySite
节点。关于详细的配置文档请参阅 ASP.NET Core 中的配置。
显示当前站点的部署模式
接着在 Index.cshtml.cs
做一些操作:
// using 部分略
public class IndexModel : PageModel
{
// 增加 IOptions<DeploySite> 的注入
private readonly IOptions<DeploySite> _deploySites;public IndexModel(IOptions<DeploySite> deploySites)
{
_deploySites = deploySites;}
// 中间部分没有修改,略
// 当前站点的部署模式,根据 host 头自动判断
public DeployMode CurrentDeployMode
{
get
{
var host = Request.Headers["Host"];// 获取枚举的名称并用于循环
foreach (var name in Enum.GetNames<DeployMode>())
{
// 使用枚举的名称,结合反射,获取配置的值
// (如果这里不这样做,就需要写三个 if 分别判断 Hybrid、IPv4 和 IPv6 的配置)
var siteInfo = typeof(DeploySite).GetProperty(name).GetValue(_deploySites.Value) as SiteInfo;// 拼接配置中的 domain 和 port,并和请求头中的 host 进行比较
// 如果使用的是 80 和 443 端口,host 是没有端口号的,所以需要排除一下
var domainAndPort = siteInfo.Domain;if (siteInfo.Port.HasValue && siteInfo.Port is not (80 or 443))
{
domainAndPort += $":{siteInfo.Port}";}
if (host == domainAndPort)
{
// 如果 host 头与配置匹配,表示找到了当前站点的部署模式,返回相关枚举
return Enum.Parse<DeployMode>(name);}
}
throw new Exception($"Host 的值 {host} 与 DeploySite 的配置没有匹配项");}
}
}
我们看看效果,看效果之前还要做两件事,首先在 Index.cshtml
中输出一下 CurrentDeployMode
@page
@model IndexModel
@{
ViewData["Title"] = "Home page";}
<div class="text-center">
<h1 class="display-4">@Model.DisplayIp</h1>
</div>
当前请求的是 @Model.CurrentDeployMode 的站点
接着找到 /Properties/launchSettings.json
文件的 IpTest
节点中的 applicationUrl
节点,修改为:
"applicationUrl": "http://localhost:5000;http://127.0.0.1:14444;http://[::1]:16666",
注意,这三个地址的需要和 appsettings.Development.json
文件里配置的三个地址对应。这样才能在本地调试的时候一次绑定多个地址和端口,方便我们测试。修改好后 Ctrl + F5 看看效果:
哇,工作的非常好~注意观察服务器启动时绑定的端口以及浏览器的地址栏。
实现只返回 JSON 的 API
根据我们的设计,还需要有一个接收 ajax 请求并返回请求者 IP 地址的接口。但现在有个棘手的问题,Razor Page 不是 Page 吗,怎么返回一段 json 便于 ajax 之后处理呢?
这都不是事,我们在 Pages
文件夹下新建一个名为 IP.cshtml
(IP
两个字母必须大写)的“空 Razor 页面”。直接找到它的 IP.cshtml.cs
文件中的 OnGet()
方法。如果你有做过 MVC 或者 Web API 的开发,那么熟悉的东西来了,把 OnGet()
方法的 void
改为 JsonResult
😂,上代码:
using BunIp.Web.Helpers;using Microsoft.AspNetCore.Mvc;using Microsoft.AspNetCore.Mvc.RazorPages;namespace BunIp.Web.Pages
{
public class IPModel : PageModel
{
public JsonResult OnGet()
{
var ipAddress = IpHelper.GetRealIp(HttpContext);return new JsonResult(new { Ip = ipAddress.ToString() });}
}
}
启动项目,在地址后加上 /ip
。铛铛,熟悉的配方熟悉的结果,接口搞定!
根据 ajax 请求的结果判断 IP 可用性
现在要准备发起 ajax 了!
通过之前增加的 CurrentDeployMode
属性,可以知道当前请求的站点是以哪种模式部署的,再结合请求者的 IP 地址(一开始增加的 DisplayIp
属性),这样我们才能知道 ajax 请求应该发给谁:
- 如果当前是混合模式,并且
DisplayIp
为 IPv6 地址,说明请求者已经有 IPv6 地址,需要检测他有没有 IPv4 地址,所以应该发一个 ajax 请求给只能通过 IPv4 访问的地址 - 如果当前是混合模式,并且
DisplayIp
为 IPv4 地址,说明请求者已经有 IPv4 地址,需要检测他有没有 IPv6 地址,所以应该发一个 ajax 请求给只能通过 IPv6 访问的地址 - 如果当前是 IPv4 模式,说明请求者访问了只能通过 IPv4 访问的站点,不需要发 ajax,把当前请求的地址展示出来即可
- 如果当前是 IPv6 模式,说明请求者访问了只能通过 IPv6 访问的站点,同上
更新 Model 和 Page
向 Index.cshtml.cs
追加下面的内容:
public bool shouldTryIpv4 => CurrentDeployMode == DeployMode.Hybrid && DisplayIp.AddressFamily == AddressFamily.InterNetworkV6;public bool shouldTryIpv6 => CurrentDeployMode == DeployMode.Hybrid && DisplayIp.AddressFamily == AddressFamily.InterNetwork;public Uri ipv4Url => _deploySites.Value.IPv4.Uri;public Uri ipv6Url => _deploySites.Value.IPv6.Uri;```
接着修改 `Index.cshtml`,我们增加了两个 div 并增加了一段 js 代码。通过 jQuery ajax 向对应服务器发起请求,并将返回的 IP 地址呈现在新增加的 div 中:
@page @model IndexModel
当前地址
@Model.DisplayIp
@if (Model.ShouldTryIpv4)
{
<div id="TryIpv4" class="d-none">
<p class="lead">
您也拥有 IPv4 地址 <span class="ip-address"></span>
</p>
</div>
}
@if (Model.ShouldTryIpv6)
{
<div id="TryIpv6" class="d-none">
<p class="lead">
您也拥有 IPv6 地址 <span class="ip-address"></span>
</p>
</div>
}
@section Scripts {