近日,应一位朋友的邀请写了个Asp.Net Core基于JWT认证的数据接口网关Demo。朋友自己开了个公司,接到的一个升级项目,客户要求用Aps.Net Core做数据网关服务且基于JWT认证实现对前后端分离的数据服务支持,于是想到我一直做.Net开发,问我是否对.Net Core有所了解?能不能做个简单Demo出来看看?我说,分道扬镳之后我不是调用别人的接口就是提供接口给别人调用,于是便有了以下示例代码。

示例要求能演示获取Token及如何使用该Token访问数据资源,在Demo中实现了JWT的颁发及验证以及重写一个ActionAuthorizeAttribute实现对具体数据接口的调用权限控制,先看一下项目截图:

[项目截图]

项目文件介绍

解决方案下只有一个项目,项目名称就叫Jwt.Gateway,包含主要文件有:

  1. Controllers目录下的ApiActionFilterAttribute.cs文件,继承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校验接口调用者对具体接口的访问权限。
  2. Controllers目录下的ApiBase.cs文件,继承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于让所有数据接口用途的控制器继承,定义有CurrentAppKey属性(来访应用程序的身份标识)并在OnActionExecuting事件中统一分析Claims并赋值。
  3. Controllers目录下的TokenController.cs控制器文件,用于对调用方应用程序获取及注销Token。
  4. Controllers目录下的UsersController.cs控制器文件,继承ApiBase.cs,作为数据调用示例。
  5. MiddleWares目录下的ApiCustomException.cs文件,是一个数据接口的统一异常处理中间件。
  6. Models目录下的ApiResponse.cs文件,用于做数据接口的统一数据及错误信息输出实体模型。
  7. Models目录下的User.cs文件,示例数据实体模型。
  8. Program.csStartup.cs文件就不介绍了,随便建个空项目都有。

项目文件代码

ApiActionFilterAttribute.cs

Controllers目录下的ApiActionFilterAttribute.cs文件,继承Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute,用于校验接口调用者对具体接口的访问权限。

设想每一个到访的请求都是一个应用程序,每一个应用程序都分配有基本的Key和Password,每一个应用程序具有不同的接口访问权限,所以在具体的数据接口上应该声明该接口所要求的权限值,比如修改用户信息的接口应该在接口方法上声明需要具有“修改用户”的权限,用例:[ApiActionFilter("用户修改")]。

大部分情况下一个接口(方法)对应一个操作,这样基本上就能应付了,但是不排除有时候可能需要多个权限组合进行验证,所以该文件中有一个对多个权限值进行校验的“”和“”枚举,用例:[ApiActionFilter(new string[] { "用户修改", "用户录入", "用户删除" },ApiActionFilterAttributeOption.AND)],这样好像就差不多了。

由于在一个接口调用之后可能需要将该接口所声明需要的权限值记入日志等需求,因此权限值集合将被写入到HttpContext.Items["Permissions"]中以方便可能的后续操作访问,看代码:

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers
{
public enum ApiActionFilterAttributeOption
{
OR,AND
}
public class ApiActionFilterAttribute : Microsoft.AspNetCore.Mvc.Filters.ActionFilterAttribute
{
List<string> Permissions = new List<string>();
ApiActionFilterAttributeOption Option = ApiActionFilterAttributeOption.AND;
public ApiActionFilterAttribute(string permission)
{
Permissions.Add(permission);
}
public ApiActionFilterAttribute(string[] permissions, ApiActionFilterAttributeOption option)
{
foreach(var permission in permissions) {
if (Permissions.Contains(permission))
{
continue;
}
Permissions.Add(permission);
}
Option = option;
} public override void OnActionExecuting(ActionExecutingContext context)
{
var key = GetAppKey(context);
List<string> keyPermissions = GetAppKeyPermissions(key);
var isAnd = Option == ApiActionFilterAttributeOption.AND;
var permissionsCount = Permissions.Count;
var keyPermissionsCount = keyPermissions.Count;
for (var i = ; i < permissionsCount; i++)
{
bool flag = false;
for (var j = ; j < keyPermissions.Count; j++)
{
if (flag = string.Equals(Permissions[i], keyPermissions[j], StringComparison.OrdinalIgnoreCase))
{
break;
}
}
if (flag)
{
continue;
}
if (isAnd)
{
throw new Exception("应用“" + key + "”缺少“" + Permissions[i] + "”的权限");
}
} context.HttpContext.Items.Add("Permissions", Permissions); base.OnActionExecuting(context);
} private string GetAppKey(ActionExecutingContext context)
{
var claims = context.HttpContext.User.Claims;
if (claims == null)
{
throw new Exception("未能获取到应用标识");
}
var claimKey = claims.ToList().Find(o => string.Equals(o.Type, "AppKey", StringComparison.OrdinalIgnoreCase));
if (claimKey == null)
{
throw new Exception("未能获取到应用标识");
} return claimKey.Value;
}
private List<string> GetAppKeyPermissions(string appKey)
{
List<string> li = new List<string>
{
"用户明细","用户列表","用户录入","用户修改","用户删除"
};
return li;
} }
}

ApiActionAuthorizeAttribute.cs

ApiBase.cs

Controllers目录下的ApiBase.cs文件,继承Microsoft.AspNetCore.Mvc.Controller,具有Microsoft.AspNetCore.Authorization.Authorize特性引用,用于让所有数据接口用途的控制器继承,定义有CurrentAppKey属性(来访应用程序的身份标识)并在OnActionExecuting事件中统一分析Claims并赋值。

通过验证之后,Aps.Net Core会在HttpContext.User.Claims中将将来访者的身份信息记录下来,我们可以通过该集合得到来访者的身份信息。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters; namespace Jwt.Gateway.Controllers
{
[Microsoft.AspNetCore.Authorization.Authorize]
public class ApiBase : Microsoft.AspNetCore.Mvc.Controller
{
private string _CurrentAppKey = "";
public string CurrentAppKey { get { return _CurrentAppKey; } }
public override void OnActionExecuting(ActionExecutingContext context)
{
var claims = context.HttpContext.User.Claims.ToList();
var claim = claims.Find(o => o.Type == "appKey");
if (claim == null)
{
throw new Exception("未通过认证");
}
var appKey = claim.Value;
if (string.IsNullOrEmpty(appKey))
{
throw new Exception("appKey不合法");
} _CurrentAppKey = appKey; base.OnActionExecuting(context);
}
}
}

ApiBase.cs

TokenController.cs

Controllers目录下的TokenController.cs控制器文件,用于对调用方应用程序获取及注销Token。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers
{
[Route("api/[controller]/[action]")]
public class TokenController : Controller
{
private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public TokenController(Microsoft.Extensions.Configuration.IConfiguration configuration)
{
_configuration = configuration;
} // /api/token/get
public IActionResult Get(string appKey, string appPassword)
{
try
{
if (string.IsNullOrEmpty(appKey))
{
throw new Exception("缺少appKey");
}
if (string.IsNullOrEmpty(appKey))
{
throw new Exception("缺少appPassword");
}
if (appKey != "myKey" && appPassword != "myPassword")//固定的appKey及appPassword,实际项目中应该来自数据库或配置文件
{
throw new Exception("配置不存在");
} var key = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]));
var creds = new Microsoft.IdentityModel.Tokens.SigningCredentials(key, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.HmacSha256);
var claims = new List<System.Security.Claims.Claim>();
claims.Add(new System.Security.Claims.Claim("appKey", appKey));//仅在Token中记录appKey
var token = new System.IdentityModel.Tokens.Jwt.JwtSecurityToken(
issuer: _configuration["JwtTokenIssuer"],
audience: _configuration["JwtTokenAudience"],
claims: claims,
expires: DateTime.Now.AddMinutes(),
signingCredentials: creds); return Ok(new Models.ApiResponse { status = , message = "OK", data = new System.IdentityModel.Tokens.Jwt.JwtSecurityTokenHandler().WriteToken(token) }); }
catch(Exception ex)
{
return Ok(new Models.ApiResponse { status = , message = ex.Message, data = "" });
}
} // /api/token/delete
public IActionResult Delete(string token)
{
//code: 加入黑名单,使其无效 return Ok(new Models.ApiResponse { status = , message = "OK", data = "" });
} }
}

TokenController.cs

UsersController.cs

Controllers目录下的UsersController.cs控制器文件,继承ApiBase.cs,作为数据调用示例。

该控制器定义了对User对象常规的 明细、列表、录入、修改、删除 等操作。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc; namespace Jwt.Gateway.Controllers
{
[Produces("application/json")]
[Route("api/[controller]/[action]")]
public class UsersController : ApiBase
{
/*
* 1.要访问访问该控制器提供的接口请先通过"/api/token/get"获取token
* 2.访问该控制器提供的接口http请求头必须具有值为"Bearer+空格+token"的Authorization键,格式参考:
* "Authorization"="Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiQXBwIiwiYXBwS2V5IjoibXlLZXkiLCJleHAiOjE1NTE3ODc2MDMsImlzcyI6IkdhdGV3YXkiLCJhdWQiOiJhdWRpZW5jZSJ9.gQ9_Q7HUT31oFyfl533T-bNO5IWD2drl0NmD1JwQkMI"
*/ /// <summary>
/// 临时用户测试数据,实际项目中应该来自数据库等媒介
/// </summary>
static List<Models.User> _Users = null;
static object _Lock = new object();
public UsersController()
{
if (_Users == null)
{
lock (_Lock)
{
if (_Users == null)
{
_Users = new List<Models.User>();
var now = DateTime.Now;
for(var i = ; i < ; i++)
{
var num = i + ;
_Users.Add(new Models.User { UserId = num, UserName = "name"+num, UserPassword = "pwd"+num, UserJoinTime = now });
}
}
}
}
} // /api/users/detail
[ApiActionFilter("用户明细")]
public IActionResult Detail(long userId)
{
/*
//获取appKey(在ApiBase中写入)
var appKey = CurrentAppKey;
//获取使用的权限(在ApiActionAuthorizeAttribute中写入)
var permissions = HttpContext.Items["Permissions"];
*/ var user = _Users.Find(o => o.UserId == userId);
if (user == null)
{
throw new Exception("用户不存在");
} return Ok(new Models.ApiResponse { data = user, status = , message = "OK" });
} // /api/users/list
[ApiActionFilter("用户列表")]
public IActionResult List(int page, int size)
{
page = page < ? : page;
size = size < ? : size;
var total = _Users.Count();
var pages = total % size == ? total / size : ((long)Math.Floor((double)total / size + ));
if (page > pages)
{
return Ok(new Models.ApiResponse { data = new List<Models.User>(), status = , message = "OK", total = total });
}
var li = new List<Models.User>();
var startIndex = page * size - size;
var endIndex = startIndex + size - ;
if (endIndex > total - )
{
endIndex = total - ;
}
for(; startIndex <= endIndex; startIndex++)
{
li.Add(_Users[startIndex]);
}
return Ok(new Models.ApiResponse { data = li, status = , message = "OK", total = total });
} // /api/users/add
[ApiActionFilter("用户录入")]
public IActionResult Add()
{
return Ok(new Models.ApiResponse { status = , message = "OK" });
} // /api/users/update
[ApiActionFilter(new string[] { "用户修改", "用户录入", "用户删除" },ApiActionFilterAttributeOption.AND)]
public IActionResult Update()
{
return Ok(new Models.ApiResponse { status = , message = "OK" });
} // /api/users/delete
[ApiActionFilter("用户删除")]
public IActionResult Delete()
{
return Ok(new Models.ApiResponse { status = , message = "OK" });
}
}
}

UsersController.cs

ApiCustomException.cs

MiddleWares目录下的ApiCustomException.cs文件,是一个数据接口的统一异常处理中间件。

该文件整理并抄袭自:https://www.cnblogs.com/ShenNan/p/10197231.html

在此特别感谢一下作者的先行贡献,并请原谅我无耻的抄袭。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway.MiddleWares
{
//参考: https://www.cnblogs.com/ShenNan/p/10197231.html public enum ApiCustomExceptionHandleType
{
JsonHandle = ,
PageHandle = ,
Both =
}
public class ApiCustomExceptionMiddleWareOption
{
public ApiCustomExceptionMiddleWareOption(
ApiCustomExceptionHandleType handleType = ApiCustomExceptionHandleType.JsonHandle,
IList<PathString> jsonHandleUrlKeys = null,
string errorHandingPath = "")
{
HandleType = handleType;
JsonHandleUrlKeys = jsonHandleUrlKeys;
ErrorHandingPath = errorHandingPath;
}
public ApiCustomExceptionHandleType HandleType { get; set; }
public IList<PathString> JsonHandleUrlKeys { get; set; }
public PathString ErrorHandingPath { get; set; }
}
public class ApiCustomExceptionMiddleWare
{
private RequestDelegate _next;
private ApiCustomExceptionMiddleWareOption _option;
private IDictionary<int, string> _exceptionStatusCodeDic; public ApiCustomExceptionMiddleWare(RequestDelegate next, ApiCustomExceptionMiddleWareOption option)
{
_next = next;
_option = option;
_exceptionStatusCodeDic = new Dictionary<int, string>
{
{ , "未授权的请求" },
{ , "找不到该页面" },
{ , "访问被拒绝" },
{ , "服务器发生意外的错误" }
//其余状态自行扩展
};
} public async Task Invoke(HttpContext context)
{
Exception exception = null;
try
{
await _next(context);
}
catch (Exception ex)
{
context.Response.Clear();
context.Response.StatusCode = ;//手动设置状态码(总是成功)
exception = ex;
}
finally
{
if (_exceptionStatusCodeDic.ContainsKey(context.Response.StatusCode) &&
!context.Items.ContainsKey("ExceptionHandled"))
{
var errorMsg = string.Empty;
if (context.Response.StatusCode == && exception != null)
{
errorMsg = $"{_exceptionStatusCodeDic[context.Response.StatusCode]}\r\n{(exception.InnerException != null ? exception.InnerException.Message : exception.Message)}";
}
else
{
errorMsg = _exceptionStatusCodeDic[context.Response.StatusCode];
}
exception = new Exception(errorMsg);
}
if (exception != null)
{
var handleType = _option.HandleType;
if (handleType == ApiCustomExceptionHandleType.Both)
{
var requestPath = context.Request.Path;
handleType = _option.JsonHandleUrlKeys != null && _option.JsonHandleUrlKeys.Count(
k => requestPath.StartsWithSegments(k, StringComparison.CurrentCultureIgnoreCase)) > ?
ApiCustomExceptionHandleType.JsonHandle :
ApiCustomExceptionHandleType.PageHandle;
} if (handleType == ApiCustomExceptionHandleType.JsonHandle)
await JsonHandle(context, exception);
else
await PageHandle(context, exception, _option.ErrorHandingPath);
}
}
}
private Jwt.Gateway.Models.ApiResponse GetApiResponse(Exception ex)
{
return new Jwt.Gateway.Models.ApiResponse() { status = , message = ex.Message };
}
private async Task JsonHandle(HttpContext context, Exception ex)
{
var apiResponse = GetApiResponse(ex);
var serialzeStr = Newtonsoft.Json.JsonConvert.SerializeObject(apiResponse);
context.Response.ContentType = "application/json";
await context.Response.WriteAsync(serialzeStr, System.Text.Encoding.UTF8);
}
private async Task PageHandle(HttpContext context, Exception ex, PathString path)
{
context.Items.Add("Exception", ex);
var originPath = context.Request.Path;
context.Request.Path = path;
try
{
await _next(context);
}
catch { }
finally
{
context.Request.Path = originPath;
}
}
}
public static class ApiCustomExceptionMiddleWareExtensions
{
public static IApplicationBuilder UseApiCustomException(this IApplicationBuilder app, ApiCustomExceptionMiddleWareOption option)
{
return app.UseMiddleware<ApiCustomExceptionMiddleWare>(option);
}
}
}

ApiCustomException.cs

配置相关

appsettings.json

算法'HS256'要求SecurityKey.KeySize大于'128'位,所以JwtSecurityKey可不要太短了哦。

 {
"Urls": "http://localhost:60000",
"AllowedHosts": "*",
"JwtSecurityKey": "areyouokhhhhhhhhhhhhhhhhhhhhhhhhhhh",
"JwtTokenIssuer": "Jwt.Gateway",
"JwtTokenAudience": "App"
}

appsettings.json

Startup.cs

关于JWT的配置可以在通过JwtBearerOptions加入一些自己的事件处理逻辑,共有4个事件可供调用:OnAuthenticationFailed,OnMessageReceived,OnTokenValidated,OnChallenge, 本示例中是在OnTokenValidated中插入Token黑名单的校验逻辑。黑名单应该是Jwt应用场景中主动使Token过期的主流做法了。

 using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Jwt.Gateway.MiddleWares;
using Microsoft.Extensions.DependencyInjection; namespace Jwt.Gateway
{
public class Startup
{
private readonly Microsoft.Extensions.Configuration.IConfiguration _configuration; public Startup(Microsoft.Extensions.Configuration.IConfiguration configuration)
{
_configuration = configuration;
} public void ConfigureServices(IServiceCollection services)
{
services.AddAuthentication(Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options => {
options.Events = new Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
{
/*OnMessageReceived = context =>
{
context.Token = context.Request.Query["access_token"];
return Task.CompletedTask;
},*/
OnTokenValidated = context =>
{
var token = ((System.IdentityModel.Tokens.Jwt.JwtSecurityToken)context.SecurityToken).RawData;
if (InBlacklist(token))
{
context.Fail("token in blacklist");
}
return Task.CompletedTask;
}
};
options.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidAudience = _configuration["JwtTokenAudience"],
ValidIssuer = _configuration["JwtTokenIssuer"],
IssuerSigningKey = new Microsoft.IdentityModel.Tokens.SymmetricSecurityKey(System.Text.Encoding.UTF8.GetBytes(_configuration["JwtSecurityKey"]))
};
});
services.AddMvc().AddJsonOptions(option=> {
option.SerializerSettings.DateFormatString = "yyyy-MM-dd HH:mm:ss.fff";
});
} public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
} app.UseApiCustomException(new ApiCustomExceptionMiddleWareOption(
handleType: ApiCustomExceptionHandleType.Both,
jsonHandleUrlKeys: new PathString[] { "/api" },
errorHandingPath: "/home/error")); app.UseAuthentication(); app.UseMvc();
} bool InBlacklist(string token)
{
//code: 实际项目中应该查询数据库或配置文件进行比对 return false;
} }
}

Startup.cs

Program.cs

 using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging; namespace Jwt.Gateway
{
public class Program
{
public static void Main(string[] args)
{
BuildWebHost(args).Run();
} public static IWebHost BuildWebHost(string[] args)
{
var config = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddJsonFile("appsettings.json", optional: true)
.Build(); return WebHost.CreateDefaultBuilder(args)
.UseKestrel()
.UseConfiguration(config)
.UseStartup<Startup>()
.Build();
}
}
}

Program.cs

运行截图

[运行截图-获取Token]

[运行截图-配置Fiddler调用接口获取数据]

[运行截图-获取到数据]

如果Token校验失败将会返回401错误!

如果你发现有错误,请善意指出,谢谢!

Asp.Net Core基于JWT认证的数据接口网关Demo的更多相关文章

  1. ASP.NET Core 基于JWT的认证(一)

    ASP.NET Core 基于JWT的认证(一) Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计 ...

  2. ASP.NET Core 基于JWT的认证(二)

    ASP.NET Core 基于JWT的认证(二) 上一节我们对 Jwt 的一些基础知识进行了一个简单的介绍,这一节我们将详细的讲解,本次我们将详细的介绍一下 Jwt在 .Net Core 上的实际运用 ...

  3. ASP.NET WebApi 基于JWT实现Token签名认证

    一.前言 明人不说暗话,跟着阿笨一起玩WebApi!开发提供数据的WebApi服务,最重要的是数据的安全性.那么对于我们来说,如何确保数据的安全将会是需要思考的问题.在ASP.NET WebServi ...

  4. asp.net core 集成JWT(二)token的强制失效,基于策略模式细化api权限

    [前言] 上一篇我们介绍了什么是JWT,以及如何在asp.net core api项目中集成JWT权限认证.传送门:https://www.cnblogs.com/7tiny/p/11012035.h ...

  5. ASP.NET Core的JWT的实现(中间件).md

    既然选择了远方,便只顾风雨兼程 __ HANS许 JWT(JSON Web Token) ASP.NET Core 的Middleware实现 引言:挺久没更新了,之前做了Vue的系列,后面想做做服务 ...

  6. asp.net core 集成JWT(一)

    [什么是JWT] JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案. JWT的官网地址:https://jwt.io/ 通俗地来讲,JWT是能代表用户身份的令牌,可以使用JWT ...

  7. ASP.NET Core的身份认证框架IdentityServer4--入门

    ASP.NET Core的身份认证框架IdentityServer4--入门 2018年08月11日 10:09:00 qq_42606051 阅读数 4002   https://blog.csdn ...

  8. ASP.NET Core基于K8S的微服务电商案例实践--学习笔记

    摘要 一个完整的电商项目微服务的实践过程,从选型.业务设计.架构设计到开发过程管理.以及上线运维的完整过程总结与剖析. 讲师介绍 产品需求介绍 纯线上商城 线上线下一体化 跨行业 跨商业模式 从0开始 ...

  9. Dotnet core使用JWT认证授权最佳实践(二)

    最近,团队的小伙伴们在做项目时,需要用到JWT认证.遂根据自己的经验,整理成了这篇文章,用来帮助理清JWT认证的原理和代码编写操作. 第一部分:Dotnet core使用JWT认证授权最佳实践(一) ...

随机推荐

  1. vue图片onerror加载路径写法

    vue里,img加载错误的时候,onerror属性可以加载错误图片的默认图片写法如下: <img class=avator' :src="data.picture" :one ...

  2. v-charts简介

    一, v-charts简介 在使用 echarts 生成图表时,经常需要做繁琐的数据类型转化.修改复杂的配置项,v-charts 的出现正是为了解决这个痛点.基于 Vue2.0 和 echarts 封 ...

  3. 自娱自乐RN版小说APP历程记录

    当前rn版本 "react": "16.6.3" "react-native": "0.58.5" 通过react-na ...

  4. java0424 wen 集合框架2

  5. element-ui 2.4.3 如何实现对form部分字段验证的解决方法?

    这是实际项目中的一个例子: 新增人员信息功能: 必填:姓名 .电话(验证电话格式): 非必填:备注.微信.邮箱(验证邮箱格式) 必填验证: 邮箱格式验证: 今天偶然看到 element-ui 2.4. ...

  6. ADO.Net的发展史

    1.演变历史: 它们是按照这个时间先后的顺序逐步出现的,史前->ODBC->OLEDB->ADO->ADO.Net. 2.下面分别介绍一下这几个. a. 史前的数据访问是什么样 ...

  7. PyGame实现情人节表白利器

    前提:写不出那么那个的话哇,随便写写,随便看看,重在代码(文章末尾有免费完整源代码) 实验环境: pygame 1.9.4 pycharm python3.6 实现思路: pygame.display ...

  8. 就是要用Vim写Vue

    Vim关于Vue的生态链还是很少,不过凑活凑活还是能用的. 缩进 缩进采用的是两个空格,.vimrc配置: au BufNewFile,BufRead *.html,*.js,*.vue set ta ...

  9. EF中防止sql注入

    EF作为一个orm框架,本身以及放置了sql的注入,但是如果我们需要执行sql语句的时候了?比如,我们需要查询视图"select * from VM where 条件 = {0}" ...

  10. Java类的加载和对象创建流程的详细分析

    相信我们在面试Java的时候总会有一些公司要做笔试题目的,而Java类的加载和对象创建流程的知识点也是常见的题目之一.接下来通过实例详细的分析一下: package com.test; public ...