一站式WebAPI与认证授权服务
保护webapi有哪些方法?
微软官方文档推荐了好几个:
- azure active directory
- azure active directory b2c (azure ad b2c)]
- identityserver4
前面两个看着就觉得搞不太明白,第三个倒是非常常见,相关的文章也很多。不过这个东西是独立部署的,太重了,如果我就想写一个简单一点的api,把认证给包括的,是不是有好办法?
准备
假设你的webapi使用jwt token来保存你的认证信息,并且通过jwt token进行保护。那么我们可以设计一个集成有认证授权的webapi服务,一站式解决问题,代码简单且方便自行修改。
要点:
- 使用类似[authorize]的授权,需要基于token中
role
这个claim来实现。 - 密码的保存需要进行特别设计。
- 用户对象返回需要避免password和passwordhash的传递。
项目特点:
- restful设计(正常来说api的资源应该是复数userinfos,但是info应该就是不可数的,不纠结了。)
- 集成swagger
- asp.net core 3.1
- nullable设计
- ef core
- 用户权限控制
- 密码安全存储
- token实现与api集成
- 简单易于理解
用户实体类
所有认证之类的工作都在api这边实现,因此我们需要一个userinfo类来进行处理。
[datacontract] [table("userinfo")] public class userinfo { [datamember] [key] public string userid { get; set; } = default!; //传输的过程中会用到密码,但是这个密码不应该被存入数据库中。 [notmapped] [datamember] public string? password { get; set; } //传输的过程中不会用到密码哈希值,但是哈希值需要存入数据库中。 [ignoredatamember] public string? passwordhash { get; set; } [datamember] public string? role { get; set; } public static string getrole(string? role) { if (string.isnullorwhitespace(role)) return "user"; return role.tolower() switch { "administrator" => "administrator", "supervisor" => "supervisor", _ => "user" }; } }
- 使用json进行序列化,[datacontract]不是必须的,我一般是不喜欢写这个东西,不写的话,那么所有的public属性和字段都会被序列化;如果标记了[datacontract],那么只有标记有[datamember]的会被序列化,使用[ignoredatamember]可以阻止序列化。
- 使用了ef core用来持久化,标记[notmapped]指示属性不被映射到数据库中,一般来说,数据库不应该直接保存密码。
令牌发放
具体实现tokencontroller如下。
[allowanonymous] [httppost] public actionresult post(userinfo login) { actionresult response = badrequest("登录失败,请检查用户名和密码"); var user = authenticateuser(login); if (user != null) { var tokenstring = generatejsonwebtoken(user); response = ok(new { access_token = tokenstring, role = user.role }); } return response; } private string generatejsonwebtoken(userinfo userinfo) { var securitykey = new symmetricsecuritykey(encoding.utf8.getbytes(_config["jwt:key"])); var credentials = new signingcredentials(securitykey, securityalgorithms.hmacsha256); var claims = new[] { new claim(jwtregisteredclaimnames.sub, userinfo.username), new claim(jwtregisteredclaimnames.jti, guid.newguid().tostring()), new claim(claimtypes.role, userinfo.role), }; var token = new jwtsecuritytoken(null, null, claims, expires: datetime.now.addminutes(120), signingcredentials: credentials); return new jwtsecuritytokenhandler().writetoken(token); } private userinfo? authenticateuser(userinfo login) { userinfo? user = null; if (string.isnullorwhitespace(login.password)) return user; using (var context = new managedatacontext()) { var result = context.userinfos.where(w => w.username.tolower() == login.username.tolower()).firstordefault(); if (result != null) if (passwordstorage.verifypassword(login.password, result.passwordhash!)) user = result; } return user; }
上面的类标志有allowanonymous
,表示这个类是可以匿名访问的,用户先请求post请求token,然后再携带token访问其他api。
上面用到一个passwordstorage的库,这个库使用了加盐哈希的形式存储了密码,实践上比较可靠。值得一提的是它的
verifypassword()
函数,使用的比较算法很巧妙,我贴在了文末,推荐大家阅读。
受保护的api
被保护的用户管理api如下,只贴了一小部分:
[enablecors("allowall")] [route("api/[controller]")] //只有角色为admin可以访问 [authorize(roles = "admin")] //如果需要增加种子数据,可以注释上面这行,取消注释下面这一行 //[allowanonymous] [apicontroller] public class userinfocontroller : controllerbase { private readonly managedatacontext _context; public userinfocontroller(managedatacontext context) { _context = context; } /// <summary> /// 有参get请求 /// </summary> /// <param name="id">用户编号id</param> /// <returns></returns> [httpget("{id}")] [producesresponsetype(typeof(userinfo), status200ok)] [producesresponsetype(typeof(string), status404notfound)] public async task<actionresult> get(string id) { var res = await _context.userinfos.findasync(id); if (res != null) return ok(res); else return notfound("cannot find key."); } }
启动配置
startup.cs注意一下顺序的问题。
public void configure(iapplicationbuilder app, iwebhostenvironment env) { //实际测试,这个usecors如果在useauthentication和useauthorization的后面,可能会导致vue.js访问问题。 app.usecors("allowall"); app.useauthentication(); app.useauthorization(); } public void configureservices(iservicecollection services) { services.addmvc().setcompatibilityversion(compatibilityversion.version_3_0); //使用addnewtonsoftjson为了避免json的严格检查。 services.addcontrollers().addnewtonsoftjson(); services.adddbcontext<managedatacontext>(); //后面还有不贴了 }
在configureservices里面,调用了addnewtonsoftjson()
。之所以没有使用到默认的system.text.json
,是因为它对客户端上传的信息要求太严格,如果是integer类型的值,上传使用了string就不能正确识别对象,而newtonsoft.json
没有这个问题。
也可以修改
system.text.json
的默认行为,但是总是没有那么方便了。
调用方法
请求令牌
post请求,api/token,设置header:content-type为application/json。body内容如下:
{ "username": "admin", "password": "123" }
调用即可返回access_token与role。
调用被保护的api
需要设置header:
- authorization值为bearer [获取到的token]
- content-type为application/json
然后就可以*调用自己有权访问的api了。
总结
零零散散写了这么些,直接贴上代码,项目是基于asp.net core 3.1与swagger的,本项目也可以作为一些小型项目的模板。
需要新建用户的话,可以注释掉[authorize]或者我已经准备了一个用户admin,密码是123。
如果需要在windows上进行服务部署,可以参考我之前写的topshelf的。
github项目地址,欢迎fork或者star。
展望
- token刷新与吊销。
- 注册与手机/email验证。
参考资料
下一篇: Kubernetes架构