2017年9月

通过VG方式给CentOS 7的root分区扩容

我有个VPS,因为不能自己安装系统,每次都得提交工单让他们帮助安装,而他们使用的也是模板安装,系统盘只给20G,/dev/mapper/centos-root分区只有8.5G,系统刚安装完一切都还好,大概只占用了不到2G,可是日积月累,这个分区就会吃满。那有没有办法在后期给/dev/mapper/centos-root分区扩展呢?最好是无痛的,答案当然是:有。
这个是有前提条件的,那就是你还得有一块数据盘,当然,我的VPS是有格外有一块150G的数据盘了,我不想把整块硬盘全加到/dev/mapper/centos-root分区,所以,先把这个数据盘分区,比如我要拿出30G加到root分区,那么我就需要把数据盘分成30G和120G这两个区,之后把30G的分区加到root上。下面是简单的步骤了:
1.先看下各盘的占用情况:

[root@localhost ~]# df -h

显示:

[root@localhost ~]# df -h
Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/centos-root  8.5G  1.8G  6.8G  21% /
devtmpfs                 1.9G     0  1.9G   0% /dev
tmpfs                    1.9G     0  1.9G   0% /dev/shm
tmpfs                    1.9G   17M  1.9G   1% /run
tmpfs                    1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/sda1                497M  151M  346M  31% /boot
tmpfs                    379M     0  379M   0% /run/user/0

查看磁盘情况:

fdisk -l

之后给空闲的数据盘分区,分成30G和120G两个区:

fdisk /dev/sdb
n
p
enter
enter
+30G
w

和第二个区:

fdisk /dev/sdb
n
p
enter
enter
enter
w

分完区之后,对两个区分别格式化:

mkfs.ext4 /dev/sdb1

mkfs.ext4 /dev/sdb2

创建PV:

[root@localhost ~]# pvcreate /dev/sdb1
WARNING: ext4 signature detected on /dev/sdb1 at offset 1080. Wipe it? [y/n]: y
  Wiping ext4 signature on /dev/sdb1.
  Physical volume "/dev/sdb1" successfully created.

查看下vg组:

[root@localhost ~]# vgs
  VG     #PV #LV #SN Attr   VSize  VFree
  centos   1   2   0 wz--n- <9.51g 40.00m

使用vgextend命令,扩展vg:

[root@localhost ~]# vgextend centos /dev/sdb1
  Volume group "centos" successfully extended

再查看下vg组:

[root@localhost ~]# vgs
  VG     #PV #LV #SN Attr   VSize  VFree
  centos   2   2   0 wz--n- 39.50g <30.04g

看下lv:

[root@localhost ~]# lvs
  LV   VG     Attr       LSize  Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  root centos -wi-ao---- <8.47g
  swap centos -wi-ao----  1.00g

使用lvextend来扩展lv

[root@localhost ~]# lvextend -L +30G /dev/mapper/centos-root
  Size of logical volume centos/root changed from <8.47 GiB (2168 extents) to <38.47 GiB (9848 extents).
  Logical volume centos/root successfully resized.

查看下lv:

[root@localhost ~]# lvs
  LV   VG     Attr       LSize   Pool Origin Data%  Meta%  Move Log Cpy%Sync Convert
  root centos -wi-ao---- <38.47g
  swap centos -wi-ao----   1.00g

使用df -h看下分区情况:

[root@localhost ~]# df -h
Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/centos-root  8.5G  1.8G  6.8G  21% /
devtmpfs                 1.9G     0  1.9G   0% /dev
tmpfs                    1.9G     0  1.9G   0% /dev/shm
tmpfs                    1.9G   17M  1.9G   1% /run
tmpfs                    1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/sda1                497M  151M  346M  31% /boot
tmpfs                    379M     0  379M   0% /run/user/0

发现大小还是没变,我们使用xfs_growfs来重新读取一次:

[root@localhost ~]# xfs_growfs /dev/mapper/centos-root
meta-data=/dev/mapper/centos-root isize=256    agcount=4, agsize=555008 blks
         =                       sectsz=512   attr=2, projid32bit=1
         =                       crc=0        finobt=0 spinodes=0
data     =                       bsize=4096   blocks=2220032, imaxpct=25
         =                       sunit=0      swidth=0 blks
naming   =version 2              bsize=4096   ascii-ci=0 ftype=0
log      =internal               bsize=4096   blocks=2560, version=2
         =                       sectsz=512   sunit=0 blks, lazy-count=1
realtime =none                   extsz=4096   blocks=0, rtextents=0
data blocks changed from 2220032 to 10084352

我们再来看下加没加上:

[root@localhost ~]# df -h
Filesystem               Size  Used Avail Use% Mounted on
/dev/mapper/centos-root   39G  1.8G   37G   5% /
devtmpfs                 1.9G     0  1.9G   0% /dev
tmpfs                    1.9G     0  1.9G   0% /dev/shm
tmpfs                    1.9G   17M  1.9G   1% /run
tmpfs                    1.9G     0  1.9G   0% /sys/fs/cgroup
/dev/sda1                497M  151M  346M  31% /boot
tmpfs                    379M     0  379M   0% /run/user/0

现在/dev/mapper/centos-root变成了39G,多出来了30G,说明已经加上了。
到这里操作就已经结束了。

在.Net Core 2.0中使用MySQL

在之前,我简单的介绍过在.net core中使用Mongodb(见文章《.Net Core系列教程(三)——使用Mongodb》),也使用过PostgreSQL(但是没有写文章介绍怎么使用,只是在文章《.Net Core系列教程(一)——环境搭建》中简单介绍过如何安装)。当然,我的文章质量都不高,只是把自己平时遇到的问题记录下来,很多问题是自己经历过之后在网上苦苦的寻找答案但都不适用或者不明了的情况下,自己摸索出来的解决方案,这也算是防止自己再次误入坑,也给遇到同样问题的朋友一点帮助吧。
下面说下怎样在.net core中使用MySQL,这个问题网上随便一搜有很多,我的当然也是从网上搜索来的,只是用自己的语言再次整理下而已。
在使用MySQL时,需要使用到MySQL的驱动,之前MySQL官方没有出驱动的时候,需要使用第三方的,不过现在有官方的驱动,还是尽量使用官方的吧,我这里也以官方的为准。另外还用到了Dapper这个小型ORM,这两个都可以通过Nuget来安装。需要注意的是,MySQL.Data需要安装最新版的(现在是6.10.3-rc版),旧版本不支持.net core 2.0
先在appsettings.json文件中,添加数据库的配置:

  "ConnectionStrings": {
    "MySqlConnection": "server=127.0.0.1;userid=root;pwd=root's password;port=3306;database=database's name;sslmode=none;Charset=utf8;"
  }

之后,增加数据库连接的Model类:

    public class ConnectionStrings
    {
        public string MySqlConnection { get; set; }        
    }

在Startup.cs文件中的ConfigureServices方法里,在services.AddMvc();之前增加添加调用:

services.Configure<Models.ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));

这样就会把appsettings.json中的数据库连接配置注入到Models.ConnectionStrings实体类中。
在控制器中,添加:
private readonly IOptions<Models.ConnectionStrings> _settings;
之后控制器的构造函数:

        public NewsController(IOptions<Models.ConnectionStrings> settings)
        {
            _settings = settings;
        }

这里我是直接把数据库连接的实体类传给了DAL层,这样写:

        public NewsController(IOptions<Models.ConnectionStrings> settings)
        {
            _settings = new BLL.ServiceImp.News(settings.Value);
        }

其中的settings.Value就是数据库连接实体类了

之后在DAL层中使用,同时也附带了几个数据库操作的实例:

        private string ConnString;
        public News(Models.ConnectionStrings Connection)
        {
            ConnString = Connection.MySqlConnection;
        }


        /// <summary>
        /// 取前top条新闻
        /// </summary>
        /// <returns></returns>
        public async Task<Models.ResultModel<List<Models.News>>> Newslatest(int top)
        {
            Models.ResultModel<List<Models.News>> result = new ResultModel<List<Models.News>>();

            using (var Conn = new MySqlConnection(ConnString))
            {
                string sql = "select Id,title,datetime,writer,remark,body,cid,status,isimage,thumb,category from news where status=1 order by datetime desc limit 0,@top";
                var data = await Conn.QueryAsync<Models.News>(sql, new { top = top });
                if(data.Count()>0)
                {
                    result.code = 0;
                    result.status = true;
                    result.message = "OK";
                    result.data = data.AsList();
                }
            }

            return result;
        }

        /// <summary>
        /// 取单条
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<Models.ResultModel<Models.News>> Detail(int id)
        {
            Models.ResultModel<Models.News> result = new ResultModel<Models.News>();
            using (var Conn =  new MySqlConnection(ConnString))
            {
                string sql = "select n.Id,title,datetime,writer,remark,body,cid,status,isimage,thumb,c.category from news n left join category c on n.cid=c.id where n.Id=@id";
                var res = await Conn.QueryAsync<Models.News>(sql, new { id = id });
                var data = res.SingleOrDefault();  //注意这里
                result.code = -404;
                result.message = "没有找到该条新闻";
                if (data != null)
                {
                    result.status = true;
                    result.message = "OK";
                    result.code = 0;
                    result.data = data;
                }
                else
                {
                    result.status = false;
                    result.code = -404;
                    result.message = "没有找到该条新闻";
                }
            }
            return result;
        }


        /// <summary>
        /// 删除
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<Models.ResultModel<object>> Delete(int id)
        {
            Models.ResultModel<object> result = new ResultModel<object>();
            using (var Conn = new MySqlConnection(ConnString))
            {
                string sql = "delete from news where Id=@id";
                var res = await Conn.ExecuteAsync(sql, new { id = id });
                result.status = res > 0;
                if (result.status)
                {
                    result.code = 0;
                    result.message = "success";
                }
            }

            return result;
        }


        /// <summary>
        /// 使用事务
        /// </summary>
        /// <param name="id"></param>
        /// <returns></returns>
        public async Task<Models.ResultModel<object>> DeleteCategory(int id)
        {
            Models.ResultModel<object> result = new ResultModel<object>();

            using (var Conn = new MySqlConnection(ConnString))
            {
                Conn.Open();  //注意这里,使用事务时,需要先open
                using (IDbTransaction transaction = Conn.BeginTransaction())
                {
                    try
                    {
                        string sql = "update news set cid=0 where cid=@id;delete from category where Id=@id;";   //删除分类,同时把该分类下的新闻移动到默认分类下
                        var res = await Conn.ExecuteAsync(sql, new { id = id });
                        transaction.Commit();   //提交事务
                        result.status = res > 0;
                        if (result.status)
                        {
                            result.code = 0;
                            result.message = "success";
                        }
                        else
                        {
                            transaction.Rollback(); //删除失败,回滚
                        }
                    }
                    catch
                    {
                        transaction.Rollback(); //出现异常,回滚
                    }
                }
                Conn.Close();   
                
            }
            return result;
        }

以上。

.Net Core 2.0的一些不大一样的地方(二)——服务器环境

我在《.Net Core 2.0的一些不大一样的地方(一)——基础身份认证》中,说了下关于Cookies的不同之处,在这篇文章中,我说下关于服务器的相关问题。
服务器的环境安装,在官网中介绍的.Net Core 2.0环境的安装方法,是通过源的方法来安装,我尝试了下,失败了,可能是和我的服务器中有之前的.Net Core 1.0环境有关。把原来的环境删掉,先删除/usr/local/bin/dotnet软链接,之后再删除掉.net core的文件夹/opt/dotnet。不过我后面再没有试通过源的方式安装是否没问题,这里我还是使用.net core 1.0的时候安装的方法,下载.net core 2.0 sdk,之后解压缩设置软链接的方式安装。
微软官方给出的通过源安装的方法,参见:《.NET and C# - Get Started in 10 Minutes》一文,自行选择自己使用的操作系统,来按步骤安装。
如果在程序中,使用了身份认证(如Cookies等,见《.Net Core 2.0的一些不大一样的地方(一)——基础身份认证》),那么还需要在Startup.cs文件的Configure方法中,在调用UseAuthentication之前调用UseForwardedHeaders,如下:

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
});

app.UseAuthentication();

这样,在做身份验证的时候,能够通过反代服务器取到用户访问的URL,不然的话,会因为URL不一致(用户访问的是Nginx服务器上绑定的域名,而.net core服务器使用的是localhost)导致验证失败。
nginx服务器的安装及反代服务配置与之前的版本没有什么区别,有区别的是守护程序的配置,使用的还是systemd(systemctl),配置中,需要多设置一项:
WorkingDirectory=站点的根目录
如:

[Unit]
Description=Example .NET Web API Application running on Ubuntu
[Service]
WorkingDirectory=/var/aspnetcore/hellomvc
ExecStart=/usr/local/bin/dotnet /var/aspnetcore/hellomvc/hellomvc.dll
Restart=always
RestartSec=10 # Restart service after 10 seconds if dotnet service crashes
SyslogIdentifier=dotnet-example
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
[Install]
WantedBy=multi-user.target

如果不设置这项的话,所有的静态资源文件都请求不到,返回502或404错误。

好了,基本就这些了吧,遇到其他的再继续补充。

.Net Core 2.0的一些不大一样的地方(一)——基础身份认证

近日,把之前使用.net core 1.0写的网站,使用.net core 2.0改写了一下,发现一些不大一样的地方,如果不注意的话,会出现些问题。
一、先说下关于使用Cookie来验证用户登录的地方:
在.net core 1.x时代,具体作法如我前面的文章《.Net Core系列教程(四)—— 基础身份认证》所说,这里我就不重新写了
而在.net core 2.0中,需要做以下调整:
1)在Startup.cs文件中,ConfigureServices方法下添加:

            services.AddAuthentication(options=> {
                options.DefaultChallengeScheme = "Cookie";
                options.DefaultSignInScheme = "Cookie";
                options.DefaultAuthenticateScheme = "Cookie";
            })
            .AddCookie("Cookie", m =>
            {
                m.LoginPath = new PathString("/Manage/CPanel/Login");
                m.AccessDeniedPath = new PathString("/Manage/CPanel/Forbidden");
                m.LogoutPath = new PathString("/Manage/CPanel/Logout");
                m.Cookie.Path = "/";
            });

完整的代码如下:

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddApplicationInsightsTelemetry(Configuration);

            services.AddAuthentication(options=> {
                options.DefaultChallengeScheme = "Cookie";
                options.DefaultSignInScheme = "Cookie";
                options.DefaultAuthenticateScheme = "Cookie";
            })
            .AddCookie("Cookie", m =>
            {
                m.LoginPath = new PathString("/Manage/CPanel/Login");
                m.AccessDeniedPath = new PathString("/Manage/CPanel/Forbidden");
                m.LogoutPath = new PathString("/Manage/CPanel/Logout");
                m.Cookie.Path = "/";
            });

            services.AddOptions();
            services.Configure<Models.ConnectionStrings>(Configuration.GetSection("ConnectionStrings"));
            services.AddAuthorization();    //Form基础验证
            services.AddMvc();
        }

之后在Configure方法下添加:

app.UseAuthentication();

完整代码:

        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            loggerFactory.AddConsole(Configuration.GetSection("Logging"));
            loggerFactory.AddDebug();


            app.UseForwardedHeaders(new ForwardedHeadersOptions
            {
                ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
            });

            app.UseAuthentication();


            //app.UseApplicationInsightsRequestTelemetry();

            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseBrowserLink();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //app.UseApplicationInsightsExceptionTelemetry();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "area",
                    template: "{area:exists}/{controller=CPanel}/{action=Index}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

之后在控制器中使用:
登录时:

            result = _manage.Login(login);
            if (result.status)
            {//登录成功
                string token = result.data.ToString();  //登录成功后生成的token,用于验证登录有效性
                var claims = new List<Claim>()
                {
                    new Claim(ClaimTypes.Name,login.username) 
                };
                var userPrincipal = new ClaimsPrincipal(new ClaimsIdentity(claims, token));
                await HttpContext.SignInAsync("Cookie", userPrincipal,
                  new Microsoft.AspNetCore.Authentication.AuthenticationProperties
                  {
                      ExpiresUtc = DateTime.UtcNow.AddHours(12),
                      IsPersistent = true,
                      AllowRefresh = false
                  });
            }

验证有效性:

            var auth = await HttpContext.AuthenticateAsync("Cookie");
            if(auth.Succeeded)
            {    //验证有效
                string username = auth.Principal.Identity.Name;    //用户名
                //通过验证后的其他处理代码
            }

在其他需要验证是否登录状态,可以在对应的Action或Controller上增加特性:[Authorize],就可以了。

-==以下部分为2018年5月13日新增==-
二、使用Jwt方式
Jwt是Json Web Token的缩写,下面是关于Jwt介绍的一些搬运:

JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。

JWT包含了使用.分隔的三部分: Header 头部 Payload 负载 Signature 签名
其结构看起来是这样的Header.Payload.Signature

Header
在header中通常包含了两部分:token类型和采用的加密算法。{ "alg": "HS256", "typ": "JWT"} 接下来对这部分内容使用 Base64Url 编码组成了JWT结构的第一部分。

Payload
Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据,有三种类型的claim:reserved, public 和 private.Reserved claims: 这些claim是JWT预先定义的,在JWT中并不会强制使用它们,而是推荐使用,常用的有 iss(签发者),exp(过期时间戳), sub(面向的用户), aud(接收方), iat(签发时间)。 Public claims:根据需要定义自己的字段,注意应该避免冲突 Private claims:这些是自定义的字段,可以用来在双方之间交换信息 负载使用的例子:{ "sub": "1234567890", "name": "John Doe", "admin": true} 上述的负载需要经过Base64Url编码后作为JWT结构的第二部分。

Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。例如如果希望使用HMAC SHA256算法,那么签名应该使用下列方式创建: HMACSHA256( base64UrlEncode(header) + "." + base64UrlEncode(payload), secret) 签名用于验证消息的发送者以及消息是没有经过篡改的。 完整的JWT 完整的JWT格式的输出是以. 分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。 下列的JWT展示了一个完整的JWT格式,它拼接了之前的Header, Payload以及秘钥签名。

关于概念性的介绍就写到这里,下面是使用方法。
先说流程:
客户端提交用户名和密码,发起登录请求;服务器接收到请求后,验证用户名和密码的合法性,验证通过,给生成token返回给客户端;
客户端得到token之后自行保存;
客户端再次发起其他需要用户登录身份的请求时,在HTTP头中带上前面申请到的token;服务器接受到请求后,验证token的合法性,进行下一步操作。

这里就涉及到了签名和验签两部分。先是签名:
在自己的项目中,找一个适合的地方,添加以下几个类:

  1. RSAKeyHelper.cs

  2. TokenAuthOption.cs

  3. RsaParameterStorage.cs

RSAKeyHelper.cs类内容:

    public class RSAKeyHelper
    {
        public static RSAParameters GenerateKey()
        {
            string keyDir = $"{PlatformServices.Default.Application.ApplicationBasePath}key";
            if (TryGetKeyParameters(keyDir, true, out RSAParameters keyParams) == false)
            {
                keyParams = GenerateAndSaveKey(keyDir);
            }
            return keyParams;
        }

        /// <summary>
        /// 从本地文件中读取用来签发 Token 的 RSA Key
        /// </summary>
        /// <param name="filePath">存放密钥的文件夹路径</param>
        /// <param name="withPrivate"></param>
        /// <param name="keyParameters"></param>
        /// <returns></returns>
        public static bool TryGetKeyParameters(string filePath, bool withPrivate, out RSAParameters keyParameters)
        {
            string filename = withPrivate ? "key.json" : "key.public.json";
            keyParameters = default(RSAParameters);
            string path = Path.Combine($"{filePath}/", filename);
            //Console.WriteLine($"filePath:{path}");
            if (File.Exists(path) == false) return false;
            string json = File.ReadAllText(path);
            var parameterStorage = JsonConvert.DeserializeObject<RsaParameterStorage>(json);
            keyParameters.D = parameterStorage.D;
            keyParameters.DP = parameterStorage.DP;
            keyParameters.DQ = parameterStorage.DQ;
            keyParameters.Exponent = parameterStorage.Exponent;
            keyParameters.InverseQ = parameterStorage.InverseQ;
            keyParameters.Modulus = parameterStorage.Modulus;
            keyParameters.P = parameterStorage.P;
            keyParameters.Q = parameterStorage.Q;            
            return true;
        }

        /// <summary>
        /// 生成并保存 RSA 公钥与私钥
        /// </summary>
        /// <param name="filePath">存放密钥的文件夹路径</param>
        /// <returns></returns>
        public static RSAParameters GenerateAndSaveKey(string filePath)
        {
            RSAParameters publicKeys, privateKeys;
            using (var rsa = new RSACryptoServiceProvider(2048))
            {
                try
                {
                    privateKeys = rsa.ExportParameters(true);
                    publicKeys = rsa.ExportParameters(false);
                }
                finally
                {
                    rsa.PersistKeyInCsp = false;
                }
            }
            if (Directory.Exists(filePath) == false) Directory.CreateDirectory(filePath);
            File.WriteAllText(Path.Combine(filePath, "key.json"), ToJsonString(privateKeys));
            File.WriteAllText(Path.Combine(filePath, "key.public.json"), ToJsonString(publicKeys));
            return privateKeys;
        }

        // 转换成 json 字符串
        static string ToJsonString(RSAParameters parameters)
        {
            var parameterStorage = new RsaParameterStorage
            {
                D = parameters.D,
                DP = parameters.DP,
                P = parameters.P,
                DQ = parameters.DQ,
                Q = parameters.Q,
                Exponent = parameters.Exponent,
                InverseQ = parameters.InverseQ,
                Modulus = parameters.Modulus
            };
            return JsonConvert.SerializeObject(parameterStorage);
        }
    }

这个类主要干了这些事:
1.生成公钥私钥对并转成JSON保存到本地;
2.读取保存在本地的公钥私钥对并生成Key

TokenAuthOption.cs类内容:

    public class TokenAuthOption
    {
        /// <summary>
        /// 受众,代表将要使用这些token的实体
        /// </summary>
        public static string Audience { get; };

        /// <summary>
        /// 发行者,表示生成token的实体
        /// </summary>
        public static string Issuer { get; };

        public static RsaSecurityKey Key { get; } = new RsaSecurityKey(RSAKeyHelper.GenerateKey());
        
        /// <summary>
        /// 安全密钥和创建签名算法
        /// </summary>
        public static SigningCredentials SigningCredentials { get; } = new SigningCredentials(Key, SecurityAlgorithms.RsaSha256Signature);

        /// <summary>
        /// 有效期
        /// </summary>
        public static TimeSpan ExpiresSpan { get; } = TimeSpan.FromDays(30);
    }

RsaParameterStorage.cs类,是用于保存和读取生成的公私钥对的转换,因为ASP.NET Core 2.0之后,私密信息在转成JSON时不允许保存了,必须要转一下,为了省事,这里就用了个最笨的办法:

    public class RsaParameterStorage
    {
        public byte[] D { get; set; }
        public byte[] DP { get; set; }
        public byte[] DQ { get; set; }
        public byte[] Exponent { get; set; }
        public byte[] InverseQ { get; set; }
        public byte[] Modulus { get; set; }
        public byte[] P { get; set; }
        public byte[] Q { get; set; }
    }

上面这些都是准备工作,下面开始配置:
打开Startup.cs文件,在ConfigureServices方法中添加:

           //Jwt验证
            //http://www.cnblogs.com/rocketRobin/p/8058760.html
            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = "JwtBearer";
                options.DefaultChallengeScheme = "JwtBearer";                
            })            
            .AddJwtBearer("JwtBearer", jwtBearerOptions =>
            {
                jwtBearerOptions.RequireHttpsMetadata = false;
                
                jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
                {                    
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = TokenAuthOption.Key,
                    ValidateIssuer = true,
                    ValidIssuer = TokenAuthOption.Issuer,//The name of the issuer,
                    ValidateAudience = true,
                    ValidAudience = TokenAuthOption.Audience,//The name of the audience,
                    ValidateLifetime = true, //validate the expiration and not before values in the token
                    ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
                };
                jwtBearerOptions.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 200;
                        c.Response.ContentType = "application/json";
                        return c.Response.WriteAsync(
                            JsonConvert.SerializeObject(
                                new Biz126.Models.ReturnModel<object>()
                                {
                                    Status = false,
                                    Message = "登录信息已失效",
                                    Code = 403,
                                    ResultData = 
                                    new
                                    {
                                        tokenExpired = true
                                    }
                                }
                            )
                        );
                    }
                };
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();  

这里需要注意:如果有开启CORS跨域的话,一定要放在CORS的配置之后。
还是在Startup.cs文件中,在Configure方法内,如果有CORS配置的话,也是在CORS配置之后,添加:

app.UseAuthentication();

我的完整的Startup.cs文件如下,包含了Redis、数据库连接、CORS跨域、log4net和Jwt验证:

    public class Startup
    {
        public static ILoggerRepository repository { get; set; }
        public Startup(IHostingEnvironment env)
        {
            var builder = new ConfigurationBuilder()
                .SetBasePath(env.ContentRootPath)
                .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
                .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
                .AddEnvironmentVariables();

            if (env.IsDevelopment())
            {
                // This will push telemetry data through Application Insights pipeline faster, allowing you to view results immediately.
                builder.AddApplicationInsightsSettings(developerMode: true);
            }
            Configuration = builder.Build();
            RedisHelper.InitializeConfiguration(Configuration);          //Redis  

            repository = LogManager.CreateRepository("NETCoreRepository");
            XmlConfigurator.Configure(repository, new FileInfo("log4net.config"));
        }

        public IConfigurationRoot Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddOptions();
            services.Configure<Dictionary<string, string>>(Configuration.GetSection("ConnectionStrings"));   //数据库连接

            //CORS跨域
            services.AddCors(options =>
            {
                options.AddPolicy("AnyOrigin", builder =>
                {
                    builder
                        .WithOrigins("http://localhost:65176")
                        .AllowAnyOrigin()
                        .AllowAnyMethod()
                        .AllowAnyHeader()
                        .AllowCredentials();
                });
            });

            services.Configure<MvcOptions>(options =>
            {
                options.Filters.Add(new CorsAuthorizationFilterFactory("AnyOrigin"));
            });

            //Jwt验证
            //http://www.cnblogs.com/rocketRobin/p/8058760.html
            services.AddAuthentication(options => {
                options.DefaultAuthenticateScheme = "JwtBearer";
                options.DefaultChallengeScheme = "JwtBearer";                
            })            
            .AddJwtBearer("JwtBearer", jwtBearerOptions =>
            {
                jwtBearerOptions.RequireHttpsMetadata = false;
                
                jwtBearerOptions.TokenValidationParameters = new TokenValidationParameters
                {                    
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = TokenAuthOption.Key,
                    ValidateIssuer = true,
                    ValidIssuer = TokenAuthOption.Issuer,//The name of the issuer,
                    ValidateAudience = true,
                    ValidAudience = TokenAuthOption.Audience,//The name of the audience,
                    ValidateLifetime = true, //validate the expiration and not before values in the token
                    ClockSkew = TimeSpan.FromMinutes(5) //5 minute tolerance for the expiration date
                };
                jwtBearerOptions.Events = new JwtBearerEvents()
                {
                    OnAuthenticationFailed = c =>
                    {
                        c.NoResult();
                        c.Response.StatusCode = 200;
                        c.Response.ContentType = "application/json";
                        return c.Response.WriteAsync(
                            JsonConvert.SerializeObject(
                                new Biz126.Models.ReturnModel<object>()
                                {
                                    Status = false,
                                    Message = "登录信息已失效",
                                    Code = 403,
                                    ResultData = 
                                    new
                                    {
                                        tokenExpired = true
                                    }
                                }
                            )
                        );
                    }
                };
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();         
            services.AddMvc();
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }

            loggerFactory.AddLog4Net();

            //CORS跨域
            app.UseCors("AnyOrigin");
            app.UseAuthentication();

            app.UseStaticFiles();

            app.UseMvc(routes =>
            {
                routes.MapRoute(
                    name: "area",
                    template: "{area:exists}/{controller=Product}/{action=List}/{id?}");

                routes.MapRoute(
                    name: "default",
                    template: "{controller=Home}/{action=Index}/{id?}");
            });
        }

    }

这里配置结束,下面是开始使用了:
先增加一个生成token的方法,我这里是单独拿的一个类来做的:
Authorize.cs类:

    public static class Authorize
    {
        /// <summary>
        /// 生成Token
        /// </summary>
        /// <param name="userToken"></param>
        /// <returns></returns>
        public static string GenerateToken(Biz126.User.Models.UserToken userToken)
        {
            var handler = new JwtSecurityTokenHandler();
            string jti = TokenAuthOption.Audience + userToken.User_Name + DateTime.Now.Add(TokenAuthOption.ExpiresSpan).Millisecond;
            jti = WebTools.Tools.MD5Cryptog(jti); // Jwt 的一个参数,用来标识 Token
            ClaimsIdentity identity = new ClaimsIdentity(
                new GenericIdentity(userToken.User_Name, "TokenAuth"),
                new[] {
                    new Claim("user_id", userToken.User_Id.ToString()), //用户ID
                    new Claim("user_name",userToken.User_Name), //用户名
                    new Claim("user_type",userToken.User_Type.ToString()),   //身份
                    new Claim("jti",jti,ClaimValueTypes.String) // jti,用来标识 token
                }
            );
            var securityToken = handler.CreateToken(new SecurityTokenDescriptor
            {
                Issuer = TokenAuthOption.Issuer,
                Audience = TokenAuthOption.Audience,
                SigningCredentials = TokenAuthOption.SigningCredentials,
                Subject = identity,
                NotBefore = DateTime.Now,
                Expires = DateTime.Now.Add(TokenAuthOption.ExpiresSpan)
            });
            return handler.WriteToken(securityToken);
        }
    }

重写IHttpContextAccessorExtension.cs类,得到当前用户信息:

    public static class IHttpContextAccessorExtension
    {
        public static User.Models.UserToken CurrentUser(this IHttpContextAccessor httpContextAccessor)
        {
            var claimsIdentity = httpContextAccessor?.HttpContext?.User?.Identity as ClaimsIdentity;
            //var stringUserId = claimsIdentity?.Claims?.FirstOrDefault(x => x.Type.Equals("user_id", StringComparison.CurrentCultureIgnoreCase))?.Value;
            //int.TryParse(stringUserId ?? "0", out int userId);

            var userToken = new User.Models.UserToken();
            var claims = claimsIdentity?.Claims?.ToList();
            var propertys = userToken.GetType().GetProperties();
            foreach (PropertyInfo property in propertys)
            {
                string name = property.Name.ToLower();
                var value = claims?.FirstOrDefault(x => x.Type.Equals(name, StringComparison.CurrentCultureIgnoreCase))?.Value;
                property.SetValue(userToken, string.IsNullOrEmpty(value) ? null : Convert.ChangeType(value, property.PropertyType), null);
            }
            return userToken;
        }
    }

用户登录时,验证账号密码通过,调用Authorize.GenerateToken(userinfo)方法生成token返回给客户端;
客户端在请求头中增加"Authorization",值为"Bearer"+空格+Token,如“Bearer Header.Payload.Signature”

服务器WebAPI接口在控制器的构造函数中,这样写:

        private Biz126.Models.UserToken current_userToken = new Biz126.Models.UserToken();   //当前用户基本信息

        public MemberController(IOptions<Dictionary<string, string>> settings, IHttpContextAccessor httpContextAccessor)
        {
            current_userToken = httpContextAccessor.CurrentUser();
        }

就可以拿到保存在token中的用户信息了。

当然,记得要在需要使用验证的控制器或者方法上,增加[Authorize]特性,如果不需要验证的话,要增加[AllowAnonymous]。

以上。

使用iText5来处理PDF

项目要求,通过pdf模板,把用户提交的数据保存到一个PDF文件中。其中有文字内容,也有图片。之前选了aspose.pdf,因为抠门,不能花钱买,就从网上找的的开心版,好不容易出来点模板,结果插入图片的时候,同一页只能插入一张图片,而官方的试用版是可以正常两张的,另外字段比较多,速度比较慢,几百个字段需要一分多钟,效率很低,放弃。之后尝试iText,发现要比aspose.pdf好用的多,下面就说下用法。
需要通过nuget安装iTextSharp,选第一个,版本号是5.5.12,也就是iText5版本,该版本是AGPL许可。
完整代码如下:

using iTextSharp.text;
using iTextSharp.text.pdf;
using System.IO;

namespace Tools
{
    public class iTextPdf
    {
        Document pdfDocument = new Document();
        private string _path = "";

        #region 内容属性
        /// <summary>
        /// 文字
        /// </summary>
        public string Content { get; set; }

        /// <summary>
        /// X坐标
        /// </summary>
        public float X { get; set; }

        /// <summary>
        /// Y坐标
        /// </summary>
        public float Y { get; set; }

        /// <summary>
        /// 页索引(从0开始)
        /// </summary>
        public int PageIndex { get; set; }

        /// <summary>
        /// 类型
        /// 1-文字
        /// 2-勾选(图片)
        /// 3-签名
        /// 4-身份证正面
        /// 5-身份证反而
        /// </summary>
        public int Type { get; set; }

        /// <summary>
        /// 字体大小
        /// </summary>
        public float textSize { get; set; }

        /// <summary>
        /// 宽度(图片使用)
        /// </summary>
        public float Width { get; set; }

        /// <summary>
        /// 高度(图片使用)
        /// </summary>
        public float Height { get; set; }
        #endregion


        public iTextPdf()
        {
            _path = string.Format("{0}/template.pdf", System.Web.HttpContext.Current.Request.MapPath(System.Web.Configuration.WebConfigurationManager.AppSettings["ConfigDir"]));
        }

        public iTextPdf(string path)
        {
            _path = path;
        }

        public void SavePdf(List<iTextPdf> pdflist,string savefile)
        {
            using (Stream inputPdfStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.Read))   //打开pdf模板
            using (Stream outputPdfStream = new FileStream(savefile, FileMode.Create, FileAccess.Write, FileShare.None))    //创建新的pdf对象用于保存
            {
                var reader = new PdfReader(inputPdfStream);
                var stamper = new PdfStamper(reader, outputPdfStream);
                var list = pdflist.OrderBy(x => x.PageIndex);   //对页号运行排序
                //FontFactory.RegisterDirectory("C:\\WINDOWS\\Fonts");  //字体目录
                list.Select(x => x.PageIndex).Distinct().ToList().ForEach(page =>
                {   //先按页进行遍历,可以减少实例化pdfContentByte的次数,减少资源占用
                    var pdfContentByte = stamper.GetOverContent(page);
                    Rectangle pagesize = reader.GetPageSizeWithRotation(page);
                    float height = pagesize.Height;
                    list.Where(x => x.PageIndex.Equals(page)).ToList().ForEach(item =>
                    { //针对每页进行要插入的内容进行遍历
                        switch (item.Type)
                        {
                            case 1:
                            case 2:
                                string content = item.Content;
                                float fontsize = 12;    //字号
                                if (!string.IsNullOrEmpty(content))
                                {   //只处理有文字内容的
                                    if ((2).Equals(item.Type))
                                    {   //对勾选的内容进行处理
                                        if ("1".Equals(content))
                                        {   //标记为1的,内容换成√,按插入文字处理
                                            content = "√"; 
                                            fontsize = 16;
                                        }
                                        else
                                        {
                                            content = "";
                                            break;
                                        }

                                    }
                                    BaseFont bfTimes = BaseFont.CreateFont(System.Web.HttpContext.Current.Request.MapPath("~/fonts/simhei.ttf"), BaseFont.IDENTITY_H, BaseFont.EMBEDDED);   //注册字体,支持中文
                                    pdfContentByte.SetColorFill(BaseColor.BLACK);

                                    pdfContentByte.SetFontAndSize(bfTimes, fontsize);   //设置字体及字号
                                    pdfContentByte.BeginText(); //开始处理文字
                                    pdfContentByte.ShowTextAligned(Element.ALIGN_LEFT, content, item.X, height - item.Y, 0);
                                    pdfContentByte.EndText();   //文字处理达成
                                }

                                break;
                            case 4:
                            case 5:  //身份证图片
                                string imgpath = System.Web.HttpContext.Current.Request.MapPath(item.Content);  //图片物理路径
                                using (Stream inputImageStream = new FileStream(imgpath, FileMode.Open, FileAccess.Read, FileShare.Read))   //打开图片
                                {
                                    Image image = Image.GetInstance(inputImageStream);
                                    image.SetAbsolutePosition(item.X, item.Y);  //图片坐标
                                    image.ScaleAbsolute(item.Width, item.Height);   //设置图片宽度和高度
                                    pdfContentByte.AddImage(image);
                                }
                                break;
                        }
                    });
                });

                stamper.Close();    //关闭同时保存
            }
        }
    }
}

分类

最新文章

最近回复

  • 老徐: 已经加上了,抱歉才看到
  • 青山: 某种原因,暂停友链,抱歉。
  • 搬瓦工: 朋友 交换链接吗
  • 飞刀说: 名称:飞刀说 描述:...
  • 青山: 计划搬迁到腾讯云,正...
  • 河边的飞刀: 网站名称:飞刀说 网...
  • 老徐: 具体要哪个呢?
  • 老徐: 是不是有点老?
  • 青山: 哇,林志炫
  • 老白: 哇,这改的可以,能不...

归档

标签云

C# .net core asp.net 情感 SQL mongodb sql server EasyUI 安全 激活 linux 身份验证 https typecho .net sql注入 kms MVC IIS 高并发 IE 坑爹 服务器 mysql Oracle Combobox Datagrid 口语 数据抓取

其它