VS Code WebApi系列——2、jwt结合数据库校验

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 { getset; }

        [Column("user_no")]
        public string UserNo { getset; }

        [Column("user_pwd")]
        public string UserPwd { getset; }

        [Column("status_no")]
        public int StatusNo { getset; }

        [Column("user_name")]
        public string UserName { getset; }

        [Column("gender")]
        public string Gender { getset; }

        [Column("mobile_phone")]
        public string MobilePhone { getset; }

        [Column("email")]
        public string Email { getset; }

        [Column("create_by")]
        public string CreateBy { getset; }

        [Column("create_at")]
        public DateTime CreateAt { getset; }

        [Column("update_by")]
        public string UpdateBy { getset; }

        [Column("update_at")]
        public DateTimeUpdateAt { getset; }
    }
}

新建针对MySql中所有数据库表主键Id列的通用Restful WebApi方法:

添加DbContext类,采用DI的方式,关键代码如下:

namespace ***DapWebApi.DataAccess.F***DbContext
{
    public class F***DbContext : DbContext
    {
        public F***DbContext(DbContextOptions<F***DbContextoptions) : base(options) { }

        public DbSet<SysUserSysUsers { getset; }
    }
}
添加数据接入接口IDao,使用Restful风格,关键代码如下:
namespace ***.DapWebApi.DataAccess.OperateInterface
{
    public interface IDao 
    {
        bool Create<T>(T modelwhere T:class;

        IEnumerable<TGet<T>() where T:class;

        T GetById<T>(int idwhere T:class;

        bool Update<T>(T modelwhere T:class;

        bool DeleteById<T>(int idwhere 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 modelwhere T : class
        {
            _db.Set<T>().Add(model);
            return _db.SaveChanges()>0;
        }

        public bool DeleteById<T>(int idwhere T : class
        {
            _db.Remove(_db.Set<T>().Find(id));
            return _db.SaveChanges()>0;
        }

        public IEnumerable<TGet<T>() where T : class
        {
            return _db.Set<T>().AsQueryable() as IEnumerable<T>;
        }

        public T GetById<T>(int idwhere T : class
        {
            return _db.Set<T>().Find(id);
        }

        public bool Update<T>(T modelwhere 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 { getset; }
        public string Audience { getset; }
        public string SecretKey { getset; }
    }
}
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<IDaoDao>();
    //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 appIWebHostEnvironment 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([FromBodyAuthorSysUserView 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(keySecurityAlgorithms.HmacSha256);
                        //other setting
       //这里只是示例,其实这里还可以添加一些其它控制信息,具体看项目需求,请根据需求查看官方文档对这里进行处理
                        //create token
                        var token = new JwtSecurityToken(_jwtSettings.Issuer_jwtSettings.AudienceclaimDateTime.NowDateTime.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 userNostring userPwdint statusNostring userNamestring 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<SysUserusers = _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 idstring userNostring userPwdint statusNostring userNamestring 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可以正常访问并获取数据了。

这些都成功了,这次咱就来套煎饼果子算了……

posted @ 2020-06-16 11:35  若雪白衣  阅读(602)  评论(0编辑  收藏  举报