概述:
ASP.NET Web API 的好用使用过的都知道,没有复杂的配置文件,一个简单的ApiController加上需要的Action就能工作。但是在使用API的时候总会遇到跨域请求的问题, 特别各种APP万花齐放的今天,对API使用者身份角色验证是不能避免的(完全开发的API不需要对使用者身份角色进行管控,可以绕过),这篇文章就来谈谈基于令牌TOKEN身份验证的实现。
问题:
对于Web API的选择性的开放,使用者无论使用AJAX,还是HttpClient对接,总要对使用者的身份角色进行验证,然而使用API总有跨域使用情况的存在,这样就导致所有基于cookie验证方式都不再适用于API的验证。
原因:
比如,基于form表单验证的基础是登录验证成功后,用户的信息存在缓存或数据库或cookie,无论哪种方式存储用户信息,都不能绕过对cookie的使用,所以form表单验证方法对于禁用cookie的浏览器都不能正常使用,结论就是不能使用cookie 的环境就不能使用基本的form表单验证方式。因此WEB API 由于跨域的使用,导致cookie不能正常工作,所以不能再使用基于表单验证的方式来实现。
基于令牌TOKEN验证方法的实现:
方法一:
1. 实现对缓存TOKEN的管理,以防IIS服务器的宕机,可以对TOKEN进行持久化存储处理,每次IIS重启重新初始化已经登录成功TOKEN缓存。实现如下:
public class UserTokenManager
{
private static readonly IUserTokenRepository _tokenRep;
private const string TOKENNAME = "PASSPORT.TOKEN"; static UserTokenManager()
{
_tokenRep = ContainerManager.Resolve<IUserTokenRepository>();
}
/// <summary>
/// 初始化缓存
/// </summary>
private static List<UserToken> InitCache()
{
if (HttpRuntime.Cache[TOKENNAME] == null)
{
var tokens = _tokenRep.GetAll();
// cache 的过期时间, 令牌过期时间 *2
HttpRuntime.Cache.Insert(TOKENNAME, tokens, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromDays( * ));
}
var ts = (List<UserToken>)HttpRuntime.Cache[TOKENNAME];
return ts;
} public static int GetUId(string token)
{
var tokens = InitCache();
var result = ;
if (tokens.Count > )
{
var id = tokens.Where(c => c.Token == token).Select(c => c.UId).FirstOrDefault();
if (id != null)
result = id.Value;
}
return result;
} public static string GetPermission(string token)
{
var tokens = InitCache();
if (tokens.Count == )
return "NoAuthorize";
else
return tokens.Where(c => c.Token == token).Select(c => c.Permission).FirstOrDefault();
} public static string GetUserType(string token)
{
var tokens = InitCache();
if (tokens.Count == )
return "";
else
return tokens.Where(c => c.Token == token).Select(c => c.UserType).FirstOrDefault();
} /// <summary>
/// 判断令牌是否存在
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static bool IsExistToken(string token)
{
var tokens = InitCache();
if (tokens.Count == ) return false;
else
{
var t = tokens.Where(c => c.Token == token).FirstOrDefault();
if (t == null)
return false;
else if (t.Timeout < DateTime.Now)
{
RemoveToken(t);
return false;
}
else
{
// 小于8小时 更新过期时间
if ((t.Timeout - DateTime.Now).TotalMinutes < * - )
{
t.Timeout = DateTime.Now.AddHours();
UpdateToken(t);
}
return true;
} }
} /// <summary>
/// 添加令牌, 没有则添加,有则更新
/// </summary>
/// <param name="token"></param>
public static void AddToken(UserToken token)
{
var tokens = InitCache();
// 不存在 怎增加
if (!IsExistToken(token.Token))
{
token.ID = ;
tokens.Add(token);
// 插入数据库
_tokenRep.Add(token);
}
else // 有则更新
{
UpdateToken(token);
}
} public static bool UpdateToken(UserToken token)
{
var tokens = InitCache();
if (tokens.Count == ) return false;
else
{
var t = tokens.Where(c => c.Token == token.Token).FirstOrDefault();
if (t == null)
return false;
t.Timeout = token.Timeout;
// 更新数据库
var tt = _tokenRep.FindByToken(token.Token);
if (tt != null)
{
tt.UserType = token.UserType;
tt.UId = token.UId;
tt.Permission = token.Permission;
tt.Timeout = token.Timeout;
_tokenRep.Update(tt);
}
return true;
}
}
/// <summary>
/// 移除指定令牌
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public static void RemoveToken(UserToken token)
{
var tokens = InitCache();
if (tokens.Count == ) return;
tokens.Remove(token);
_tokenRep.Remove(token);
} public static void RemoveToken(string token)
{
var tokens = InitCache();
if (tokens.Count == ) return; var ts = tokens.Where(c => c.Token == token).ToList();
foreach (var t in ts)
{
tokens.Remove(t);
var tt = _tokenRep.FindByToken(t.Token);
if (tt != null)
_tokenRep.Remove(tt);
}
} public static void RemoveToken(int uid)
{
var tokens = InitCache();
if (tokens.Count == ) return; var ts = tokens.Where(c => c.UId == uid).ToList();
foreach (var t in ts)
{
tokens.Remove(t);
var tt = _tokenRep.FindByToken(t.Token);
if (tt != null)
_tokenRep.Remove(tt);
}
}
}
2. 新建ApiAuthorizeAttribute类,继承AuthorizeAttribute,重写方法IsAuthorized,这样基于TOKEN验证方式就完成了。实现如下:
public class ApiAuthorizeAttribute : AuthorizeAttribute
{
protected override bool IsAuthorized(HttpActionContext actionContext)
{
// 验证token
//var token = actionContext.Request.Headers.Authorization;
var ts = actionContext.Request.Headers.Where(c => c.Key.ToLower() == "token").FirstOrDefault().Value;
if (ts != null && ts.Count() > )
{
var token = ts.First<string>();
// 验证token
if (!UserTokenManager.IsExistToken(token))
{
return false;
}
return true;
} if (actionContext.Request.Method == HttpMethod.Options)
return true;
return false;
}
}
3. 登录实现
/// <summary>
/// 账户
/// </summary>
public class AccountController : ApiController
{
/// <summary>
/// 登录
/// </summary>
/// <param name="user">登录人员信息: 账号,密码 ,是否记住密码</param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public ResultData Login([FromBody]LoginUser user)
{
string mobile = user.Mobile;
string password = user.Password;
bool IsRememberMe = user.IsRememberMe; if (string.IsNullOrEmpty(mobile) || string.IsNullOrEmpty(password))
return new ResultData(((int)LoginResultEnum.UserNameOrPasswordError), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameOrPasswordError)); User u=null;
IMembershipService membershipSvc = ContainerManager.Container.Resolve<IMembershipService>();
LoginResultEnum loginResult = membershipSvc.Login(mobile, password, out u);
if (loginResult == LoginResultEnum.Success)
{
//SetAuthenticationTicket(u, IsRememberMe); // token 处理
UserTokenManager.RemoveToken(u.ID);
// 生成新Token
var token = Utility.MD5Encrypt(string.Format("{0}{1}", Guid.NewGuid().ToString("D"), DateTime.Now.Ticks));
// token过期时间
int timeout = ;
if (!int.TryParse(ConfigurationManager.AppSettings["TokenTimeout"], out timeout))
timeout = ;
// 创建新token
var ut = new UserToken()
{
Token = token,
Timeout = DateTime.Now.AddHours(timeout),
UId = u.ID,
UserType = (u.IsSaler.HasValue && u.IsSaler.Value) ? "Saler" : "Vip"
}; UserTokenManager.AddToken(ut); // 登录log
var logRep = ContainerManager.Container.Resolve<ISysLogRepository>();
var log = new Log()
{
Action = "Login",
Detail = "会员登录:" + u.Mobile + "|" + u.Name,
CreateDate = DateTime.Now,
CreatorLoginName = u.Mobile,
IpAddress = GetClientIp(this.Request)
}; logRep.Add(log); var data = new
{
id = u.ID,
issaler = u.IsSaler.HasValue ? u.IsSaler.Value : false,
mobile = u.Mobile,
token = token
};
var result = new ResultData(data);
result.desc = "登录成功";
return result;
} if (loginResult == LoginResultEnum.UserNameUnExists)
{
return new ResultData(((int)LoginResultEnum.UserNameUnExists), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameUnExists));
}
if (loginResult == LoginResultEnum.VerifyCodeError)
{
return new ResultData(((int)LoginResultEnum.VerifyCodeError), EnumExtension.GetEnumDescription(LoginResultEnum.VerifyCodeError));
}
if (loginResult == LoginResultEnum.UserNameOrPasswordError)
{
return new ResultData(((int)LoginResultEnum.UserNameOrPasswordError), EnumExtension.GetEnumDescription(LoginResultEnum.UserNameOrPasswordError));
}
return new ResultData(ResultType.UnknowError, "登录失败,原因未知");
}
/// <summary>
/// 退出当前账号
/// </summary>
/// <returns></returns>
[HttpPost]
public ResultData SignOut()
{
// 登录log
var logRep = ContainerManager.Resolve<ISysLogRepository>();
var log = new Log()
{
Action = "SignOut",
Detail = "会员退出:" + RISContext.Current.CurrentUserInfo.UserName,
CreateDate = DateTime.Now,
CreatorLoginName = RISContext.Current.CurrentUserInfo.UserName,
IpAddress = GetClientIp(this.Request)
};
logRep.Add(log);
//System.Web.Security.FormsAuthentication.SignOut();
UserTokenManager.RemoveToken(this.Token);
return new ResultData(ResultType.Success, "退出成功");
}
}
4. 测试API
这样就可以配合.NET原有的 AllowAnonymousAttribute 属性使用, 使用方法如下:
不需要验证身份的 类或者Action 添加 [AllowAnonymous]属性,否则添加[ApiAuthorize]
/// <summary>
/// 测试
/// </summary>
[ApiAuthorize]
public class TestController : BaseApiController
{
/// <summary>
/// 测试权限1
/// </summary>
[HttpGet]
public string TestAuthorize1()
{
return "TestAuthorize1";
}
/// <summary>
/// 测试权限2
/// </summary>
[AllowAnonymous]
[HttpGet]
public string TestAuthorize2()
{
return "TestAuthorize2";
}
}
测试一:
//TestAuthorize
function TestAuthorize1() {
$.ajax({
type: "get",
url: host + "/mobileapi/test/TestAuthorize1",
dataType: "text",
data: {},
beforeSend: function (request) {
request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token
},
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert("报错无语");
}
});
}
结果如下:
测试二:
//TestAuthorize
function TestAuthorize2() {
$.ajax({
type: "get",
url: host + "/mobileapi/test/TestAuthorize2",
dataType: "text",
data: {},
beforeSend: function (request) {
request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token
},
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert("报错无语");
}
});
}
结果如下:
测试三:
//TestAuthorize
function TestAuthorize1() {
$.ajax({
type: "get",
url: host + "/mobileapi/test/TestAuthorize1",
dataType: "text",
data: {},
beforeSend: function (request) {
//request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token
},
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert("报错无语");
}
});
}
结果如下:
测试四:
//TestAuthorize
function TestAuthorize2() {
$.ajax({
type: "get",
url: host + "/mobileapi/test/TestAuthorize2",
dataType: "text",
data: {},
beforeSend: function (request) {
//request.setRequestHeader("token", $("#token").val()); // 请求发起前在头部附加token
},
success: function (data) {
alert(data);
},
error: function (x, y, z) {
alert("报错无语");
}
});
}
结果如下:
方法二:
此方法缺点就是每次请求都需要附带token请求参数,这对于有强迫症的程序猿来说是一种折磨,不细说,实现代码如下,有需要的自己研究研究:
/// <summary>
/// 用户令牌验证
/// </summary>
public class TokenAuthorizeAttribute : ActionFilterAttribute
{
private const string UserToken = "token";
public override void OnActionExecuting(HttpActionContext actionContext)
{
// 匿名访问验证
var anonymousAction = actionContext.ActionDescriptor.GetCustomAttributes<AllowAnonymousAttribute>();
if (!anonymousAction.Any())
{
// 验证token
var token = TokenVerification(actionContext);
}
base.OnActionExecuting(actionContext);
} /// <summary>
/// 身份令牌验证
/// </summary>
/// <param name="actionContext"></param>
protected virtual string TokenVerification(HttpActionContext actionContext)
{
string msg = "";
// 获取token
var token = GetToken(actionContext, out msg);
if (!string.IsNullOrEmpty(msg))
actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.OK, new NoAuthData() { code = "", msg = msg });
// 判断token是否有效
if (!UserTokenManager.IsExistToken(token))
{
actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.OK, new NoAuthData() { code = "", msg = "Token已失效,请重新登录!" });
//actionContext.Response = actionContext.Request.CreateResponse<NoAuthData>(System.Net.HttpStatusCode.Unauthorized, new NoAuthData() { code = "401", msg = "Token已失效,请重新登录!" });
// actionContext.Response = actionContext.Request.CreateErrorResponse(System.Net.HttpStatusCode.Unauthorized, "Token已失效,请重新登录!");
} return token;
} private string GetToken(HttpActionContext actionContext, out string msg)
{
Dictionary<string, object> actionArguments = actionContext.ActionArguments;
HttpMethod type = actionContext.Request.Method;
msg = "";
var token = "";
if (type == HttpMethod.Post)
{
if (actionArguments.ContainsKey(UserToken))
{
if (actionArguments[UserToken] != null)
token = actionArguments[UserToken].ToString();
}
else
{
foreach (var value in actionArguments.Values)
{
if (value != null && value.GetType().GetProperty(UserToken) != null)
token = value.GetType().GetProperty(UserToken).GetValue(value, null).ToString();
}
} if (string.IsNullOrEmpty(token))
msg = "登录超时,请重新登录!";
}
else if (type == HttpMethod.Get)
{
if (!actionArguments.ContainsKey(UserToken))
msg = "还未登录";
// throw new HttpException(401, "还未登录"); if (actionArguments[UserToken] != null)
token = actionArguments[UserToken].ToString();
else
msg = "登录超时,请重新登录!";
}
else
{
throw new HttpException(, "暂未开放除POST,GET之外的访问方式!");
}
return token;
}
} public class NoAuthData
{
public string code { get; set; }
public string msg { get; set; }
}
此篇到此结束,欢迎大家讨论!