Knowledge should be shared free.
我们都知道WebApi最重要的作用就是为外部服务提供相应的数据接口和服务,所以一般WebApi都会连接后台数据库,那么最重要的一件事就是校验,要不然后台数据和服务就等于对所有人开放,那还了得(局域网项目除外,因为系统不挂接外系统,可能安全性要求不那么高,所以就不用那么严格,但是最好也有),总不能直来直去,谁都能用吧,这又不是08年的奥运会,我们一块唱北京欢迎你,我们WebApi不欢迎“陌生人”。这就相当于给WebApi安排一个门卫大爷,那么我们想想一般门卫的流程是什么样的呢?门口站个大爷,来了一个人,大爷问你出入证呢?你给他看,然后看完了,大爷验证证件正确真实,开门让进。我们得给WebApi也安排这么一个大爷,这个大爷就是Token。BlaBla一堆背景,就是想告诉大家WebApi一般都要验证的,用以保证安全,而目前用的比较多的方式都是Token。
Token的好处……很多,百度吧
那么我们该怎么做。
首先Token结构:
Token头,主要数据是Token类型和加密方法;Token载荷,就是有效数据,校验成功后,经Token回传的数据,一般为一个json对象;Token签名,Token的头和尾拼一起,用加密算法和密钥加密后的数据。最后组合一起用点分隔再Base64转义一下,就是Token值了。
Token通信机制:
第一步想进门得申请出入证,所以先向服务提交Token请求。第二步以后进门都得带着出入证,否则门卫大爷会拦住不让进,所以Token获取成功之后的WebApi请求一般要在头部携带Token信息(并不是所有WebApi都需要Token,根据项目需求定)。
代码实现(blabla一堆废话,干货……)
环境:与前一篇一致,不清楚的请查看《VS Code WebApi系列——1、配置》
要引入的Nuget包:
1)Microsoft.AspNetCore.Authentication.JwtBearer V3.0.0
没用最新包,也有其它方式做Token,不过既然选了Net Core,那就尽量微软系下去一路到底。
2)MySql.Data.EntityFrameworkCore V8.0.20
这个不用过多解释,连接数据库的包,Token为什么要用数据库呢?因为很简单,如何发放Token,一般都需要有一个用户表吧,里面存着用户名和密码,申请Token的时候先把用户名和密码提交过来,然后连接后台数据库校验,用户名和密码一致,之后我们再授予Token。
具体编码:
1)EF框架搭建,CodeFirst模式,这里采用原有的一个项目测试数据库,数据库是MySql,创建用户表的Sql如下
CREATE TABLE sys_user (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘主键‘,
`user_no` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘用户编号‘,
`user_pwd` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘用户密码‘,
`status_no` int(11) NOT NULL COMMENT ‘用户状态‘,
`user_name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘用户名称‘,
`gender` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘用户性别‘,
`mobile_phone` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘用户联系方式‘,
`email` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘用户邮箱‘,
`create_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT ‘创建人‘,
`create_at` timestamp(0) NOT NULL COMMENT ‘创建时间‘,
`update_by` varchar(50) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT ‘更新人‘,
`update_at` timestamp(0) NULL DEFAULT NULL COMMENT ‘更新时间‘,
PRIMARY KEY (`id`) USING BTREE,
UNIQUE INDEX `uq_sys_user_no`(`user_no`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 3 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
新建表,并添加几个用户,之后添加用户实体:(单独新建一个命名空间,就是文件夹)
namespace ***.DapWebApi.Model
{
[Table(“sys_user”)]
public class SysUser
{
[Column(“id”)]
public int Id { get; set; }
[Column(“user_no”)]
public string UserNo { get; set; }
[Column(“user_pwd”)]
public string UserPwd { get; set; }
[Column(“status_no”)]
public int StatusNo { get; set; }
[Column(“user_name”)]
public string UserName { get; set; }
[Column(“gender”)]
public string Gender { get; set; }
[Column(“mobile_phone”)]
public string MobilePhone { get; set; }
[Column(“email”)]
public string Email { get; set; }
[Column(“create_by”)]
public string CreateBy { get; set; }
[Column(“create_at”)]
public DateTime CreateAt { get; set; }
[Column(“update_by”)]
public string UpdateBy { get; set; }
[Column(“update_at”)]
public DateTime? UpdateAt { get; set; }
}
}
新建针对MySql中所有数据库表主键Id列的通用Restful WebApi方法:
添加DbContext类,采用DI的方式,关键代码如下:
namespace ***DapWebApi.DataAccess.F***DbContext
{
public class F***DbContext : DbContext
{
public F***DbContext(DbContextOptions<F***DbContext> options) : base(options) { }
public DbSet<SysUser> SysUsers { get; set; }
}
}
添加数据接入接口IDao,使用Restful风格,关键代码如下:
namespace ***.DapWebApi.DataAccess.OperateInterface
{
public interface IDao
{
bool Create<T>(T model) where T:class;
IEnumerable<T> Get<T>() where T:class;
T GetById<T>(int id) where T:class;
bool Update<T>(T model) where T:class;
bool DeleteById<T>(int id) where T:class;
}
}
添加数据实现Dao,关键代码如下:
namespace***.DapWebApi.DataAccess.OperateImplement
{
public class Dao : IDao
{
private ***DbContext _db;
public Dao(***DbContext context)
{
_db=context;
}
public bool Create<T>(T model) where T : class
{
_db.Set<T>().Add(model);
return _db.SaveChanges()>0;
}
public bool DeleteById<T>(int id) where T : class
{
_db.Remove(_db.Set<T>().Find(id));
return _db.SaveChanges()>0;
}
public IEnumerable<T> Get<T>() where T : class
{
return _db.Set<T>().AsQueryable() as IEnumerable<T>;
}
public T GetById<T>(int id) where T : class
{
return _db.Set<T>().Find(id);
}
public bool Update<T>(T model) where T : class
{
_db.Set<T>().Update(model);
return _db.SaveChanges()>0;
}
}
}
以上代码均采用了.Net Core默认的依赖注入方式。
2)编辑配置文件
打开appsetting.json添加以下节点:
“JwtSettings“:{
“Issuer“:“http://localhost:5000”,
“Audience“:“http://localhost:5000”,
“SecretKey“:“1234567890987654321”
},
“ConnectionStrings“: {
“***Connection“: “server=***;port=***;database=***;uid=***;pwd=***;CharSet=utf8”
}
下半部分是数据库连接字符串,上半部分是Jwt的配置
Issuer:Token签发者,生成Token的服务器
Audience:Token签发对象,Token签发给谁
SecretKey:密钥串,Base64 Token信息前生成签名的密钥
3)添加服务提供对象的定位对象(感谢stackoverflow提供的实现方式)ServiceLocator,将其放入到Common对象中,关键代码:
namespace ***.DapWebApi.Common
{
public class ServiceLocator
{
public static IServiceProvider Services{get;set;}
public static void SetServices(IServiceProvider services)
{
Services=services;
}
}
}
之所以添加这一对象,因为一会要在Jwt的代码实现中通过IserviceProvider接口访问数据接口IDao,继而访问数据库。因为采用了DI的模式,不能直接创建IDao对象,所以采用这个模式访问数据库。
4)添加Jwt的模型实体
在Model层添加实体,用来记录Jwt的实体信息。
namespace ***.DapWebApi.Model
{
public class JwtSettings
{
public string Issuer { get; set; }
public string Audience { get; set; }
public string SecretKey { get; set; }
}
}
5)添加用户视图实体,用来精简数据和屏蔽不必要信息
namespace ***.DapWebApi.Model
{
public class AuthorSysUserView
{
public int Id{get;set;}
public string UserNo{get;set;}
public string UserPwd{get;set;}
}
}
我们这里最主要用到的就是Id,UserNo,UserPwd这三个属性。
6)配置并注入相关依赖:
打开app
// This method gets called by the runtime. Use this method to add services to the container.
public void ConfigureServices(IServiceCollection services)
{
//add jwt
services.Configure<JwtSettings>(Configuration.GetSection(“JwtSettings”));
var jwtSettings=new JwtSettings();
Configuration.Bind(“JwtSettings”,jwtSettings); //add db connection and dao
services.AddDbContext<***DbContext>(options => options.UseMySQL(Configuration.GetConnectionString(“***Connection”)));
services.AddScoped<IDao, Dao>(); //jwt setting
services.AddAuthentication(options=>{
options.DefaultAuthenticateScheme=JwtBearerDefaults.AuthenticationScheme;
options.DefaultChallengeScheme=JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(o=>{
o.TokenValidationParameters=new Microsoft.IdentityModel.Tokens.TokenValidationParameters{
//token source
ValidIssuer=jwtSettings.Issuer,
//token apply from
ValidAudience=jwtSettings.Audience,
//encrypt key
IssuerSigningKey=new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtSettings.SecretKey))
//options
//ValidateIssuerSigningKey=true;
//ValidateLifetime=true,
//server time padding
//Clockskew=TimeSpan.Zero
};
});
services.AddControllers();
//add provider
var provider=services.BuildServiceProvider();
ServiceLocator.SetServices(provider);
}
// This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseHttpsRedirection();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
7)添加授权的控制器,实现Token的下发
namespace ***.DapWebApi.Controllers
{
[Route(“api/[controller]”)]
public class AuthorizeController : Controller
{
private JwtSettings _jwtSettings;
public AuthorizeController(IOptions<JwtSettings> _jwtSettingsAccesser)
{
_jwtSettings = _jwtSettingsAccesser.Value;
}
private IServiceProvider _provider;
[HttpGet]
public IActionResult Token([FromBody] AuthorSysUserView userView)
{
if (ModelState.IsValid)
{
//database access
_provider = ServiceLocator.Services;
IDao dao = _provider.GetService(typeof(IDao)) as IDao;
if (dao != null)
{
SysUser user = dao.GetById<SysUser>(userView.Id);
//Md5Tool is a static class which change pwd to md5 string
if (user.UserNo == userView.UserNo && Md5Tool.GetMd5ByString(userView.UserPwd) == user.UserPwd)
{
//add payload
//添加JWT有效载荷,想要回传什么信息,就可以在这添加,不要太复杂,觉得这挺安全的,所有数据一股脑都扔到有效载荷里,程序跑死出错自己想办法去,这里它只是相当于Session或者Cookie的变种,并不适合承载大量数据
Claim[] claim = new Claim[]{
new Claim(ClaimTypes.Sid,userView.UserNo.ToString()),
new Claim(ClaimTypes.UserData,userView.Id.ToString())
};
//get key
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_jwtSettings.SecretKey));
//register token
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
//other setting //这里只是示例,其实这里还可以添加一些其它控制信息,具体看项目需求,请根据需求查看官方文档对这里进行处理
//create token
var token = new JwtSecurityToken(_jwtSettings.Issuer, _jwtSettings.Audience, claim, DateTime.Now, DateTime.Now.AddHours(1), creds);
return Ok(new { token = new JwtSecurityTokenHandler().WriteToken(token) });
}
}
}
return BadRequest();
}
}
}
8)WebApi 关于SysUser的控制器实现:
注意在控制器前添加了属性Authorize,那么这个控制器的访问就必须配备令牌Token,否则将被拒绝访问,我们的门卫大爷就上岗了,当然如果希望给某个Action添加豁免行为,也就是说某个请求不再希望进行Token验证,只需要把属性从这个控制器上转移到其它Action上就可了。
namespace ***.DapWebApi.Controllers
{
[Authorize]
[ApiController]
[Route(“api/sysuser”)]
public class SysUserController:ControllerBase
{
private IDao _dao=null;
public SysUserController(IDao dao)
{
_dao=dao;
}
[HttpPost]
public IActionResult Create(string userNo, string userPwd, int statusNo, string userName, string createBy)
{
if (string.IsNullOrWhiteSpace(userNo))
{
return Content(“编号不能为空”);
}
if (string.IsNullOrWhiteSpace(userPwd))
{
return Content(“密码不能为空”);
}
if (statusNo <= 0)
{
return Content(“状态数据错误”);
}
if (string.IsNullOrWhiteSpace(userName))
{
return Content(“姓名不能为空”);
}
if (string.IsNullOrWhiteSpace(createBy))
{
return Content(“创建者不能为空”);
}
SysUser user = new SysUser()
{
UserNo = userNo,
UserPwd = userPwd,
StatusNo = statusNo,
UserName = userName,
Gender = “男”,
MobilePhone = “130XXXXXXXX”,
Email = “godisME@Hoven.com”,
CreateBy = createBy,
CreateAt = DateTime.Now,
UpdateBy = createBy,
UpdateAt = DateTime.Now
};
if (_dao.Create(user))
{
return Content(“用户添加成功”);
}
else
{
return Content(“用户添加失败”);
}
}
[HttpGet]
public IActionResult Gets()
{
IEnumerable<SysUser> users = _dao.Get<SysUser>();
return new JsonResult(users);
}
[HttpGet(“{id}”)]
public IActionResult Get(int id)
{
SysUser user=_dao.GetById<SysUser>(id);
return new JsonResult(user);
}
[HttpPut]
public IActionResult Update(int id, string userNo, string userPwd, int statusNo, string userName, string createBy)
{
if (id <= 0)
{
return Content(“主键数据错误”);
}
if (string.IsNullOrWhiteSpace(userNo))
{
return Content(“编号不能为空”);
}
if (string.IsNullOrWhiteSpace(userPwd))
{
return Content(“密码不能为空”);
}
if (statusNo <= 0)
{
return Content(“状态数据错误”);
}
if (string.IsNullOrWhiteSpace(userName))
{
return Content(“姓名不能为空”);
}
if (string.IsNullOrWhiteSpace(createBy))
{
return Content(“创建者不能为空”);
}
SysUser user = new SysUser()
{
Id = id,
UserNo = userNo,
UserPwd = userPwd,
StatusNo = statusNo,
UserName = userName,
Gender = “男”,
MobilePhone = “130XXXXXXXX”,
Email = “godisME@Hoven.com”,
CreateBy = createBy,
CreateAt = DateTime.Now,
UpdateBy = createBy,
UpdateAt = DateTime.Now
};
if (_dao.Update(user))
{
return Content(“用户更新成功”);
}
else
{
return Content(“用户更新失败”);
}
}
[HttpDelete]
public IActionResult Delete(int id)
{
if (id<=0)
{
return Content(“主键数据错误”);
}
if (_dao.DeleteById<SysUser>(id))
{
return Content(“用户删除成功”);
}
else
{
return Content(“用户删除失败”);
}
}
}
以上就是所有的代码配置,其实其中除了JWT还包括一些其它的WebApi知识,东西比较多大家慢慢搭建,都搭建好了之后就可以调试运行了。项目结构如下图所示
进行调试,使用postman访问api,首先不申请Token访问,看看结果
很明显,结果为401,未授权, 接下来申请Token,这里测试的用户名为001,密码为123,id为1,通过body传入数据

成功获得Token,之后携带token访问之前的WebApi
可以看到,携带token后,WebApi可以正常访问并获取数据了。
这些都成功了,这次咱就来套煎饼果子算了……