一、为什么 WebAPI 使用 Token 认证
传统的 ASP.NET MVC / WebForm 项目通常使用 Session 来维护用户的登录状态。但 WebAPI 作为前后端分离架构下的接口层,具有以下特点:
| 对比项 | Session | Token |
|---|---|---|
| 存储位置 | 服务器端(内存/缓存) | 客户端(LocalStorage / Cookie / Header) |
| 跨域支持 | 差(依赖 Cookie 同源) | 好(可跨域携带) |
| 服务器状态 | 有状态(需维护 Session) | 无状态(每次请求自包含凭证) |
| 扩展性 | 多服务器需共享 Session | 天然支持分布式/负载均衡 |
| 移动端适配 | 不便 | 友好(App / 小程序 / SPA) |
Token 相当于”入城令牌”,客户端首次登录后由服务器签发,之后每次请求 API 都需携带此令牌,服务器校验通过才允许访问。
Token 的传递方式有多种:
- URL 参数:如
?token=xxx(微信公众号开发常用) - HTTP Header:如
Authorization: Bearer xxx或自定义 Header(推荐)
本教程采用 HTTP Header 自定义 Token 的方式,并使用 Postman 模拟请求。
二、核心思路
- 客户端登录成功后,服务器生成 Token 并返回
- 客户端将 Token 保存在本地(LocalStorage / Cookie)
- 后续每次请求 API 时,将 Token 放入 HTTP Header 中
- 服务器端通过 自定义
AuthorizeAttribute 拦截请求,从 Header 中提取 Token 并校验 - 校验通过 → 允许访问;校验失败 → 返回 401 Unauthorized
- 标记
[AllowAnonymous]的接口跳过验证(如登录接口本身)
三、自定义 TokenAuthAttribute
3.1 完整代码
在项目中创建 App_Start 文件夹(如不存在),添加 TokenAuthAttribute.cs:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Mvc;
namespace Demo.App_Start
{
/// <summary>
/// 自定义 Token 认证特性
/// </summary>
public class TokenAuthAttribute : AuthorizeAttribute
{
/// <summary>
/// 重写基类的验证方式,加入自定义 Token 验证逻辑
/// </summary>
public override void OnAuthorization(HttpActionContext actionContext)
{
// 从 HTTP Header 中获取 Token
// 通过 MS_HttpContext 访问底层 HttpContext
var content = actionContext.Request.Properties["MS_HttpContext"] as HttpContextBase;
var token = content.Request.Headers["Token"];
if (!string.IsNullOrEmpty(token))
{
// 校验 Token 是否有效
if (ValidateTicket(token))
{
// 验证通过,调用基类方法
base.IsAuthorized(actionContext);
}
else
{
// Token 无效,返回 401
HandleUnauthorizedRequest(actionContext);
}
}
else
{
// Header 中没有 Token,检查是否允许匿名访问
var attributes = actionContext.ActionDescriptor
.GetCustomAttributes<AllowAnonymousAttribute>()
.OfType<AllowAnonymousAttribute>();
bool isAnonymous = attributes.Any(a => a is AllowAnonymousAttribute);
if (isAnonymous)
{
// 允许匿名访问,跳过验证
base.OnAuthorization(actionContext);
}
else
{
// 不允许匿名且未提供 Token,返回 401
HandleUnauthorizedRequest(actionContext);
}
}
}
/// <summary>
/// 校验 Token(实际项目中应查询数据库/缓存/Redis)
/// </summary>
/// <param name="encryptToken">客户端传入的 Token</param>
/// <returns>是否有效</returns>
private bool ValidateTicket(string encryptToken)
{
bool flag = false;
try
{
// ============================================
// 实际项目中,这里应:
// 1. 从数据库/Redis/缓存中查询 Token 是否存在
// 2. 校验 Token 是否过期
// 3. 校验 Token 对应的用户状态是否正常
// ============================================
// 示例:简单比对(实际项目中不要这样写)
if ("ddddd" == encryptToken)
{
flag = true;
}
// 示例:数据库校验伪代码
// var dbToken = db.Tokens.FirstOrDefault(t => t.Value == encryptToken);
// if (dbToken != null && dbToken.ExpireTime > DateTime.Now)
// {
// flag = true;
// }
}
catch (Exception ex)
{
// 记录日志
// Log.Error(ex);
}
return flag;
}
}
}Code language: HTML, XML (xml)
3.2 代码解析
| 步骤 | 说明 |
|---|---|
actionContext.Request.Properties["MS_HttpContext"] | 获取底层 HttpContextBase,用于访问 Request Header |
content.Request.Headers["Token"] | 从 Header 中读取名为 Token 的自定义字段 |
ValidateTicket() | 校验 Token 有效性(查库、比对、过期检查) |
AllowAnonymousAttribute | 检查接口是否标记了 [AllowAnonymous],允许匿名访问 |
HandleUnauthorizedRequest() | 返回 HTTP 401 未授权状态码 |
四、在 Controller 中使用
4.1 全局注册(推荐)
在 App_Start/WebApiConfig.cs 中全局注册,所有 API 默认需要 Token:
public static class WebApiConfig
{
public static void Register(HttpConfiguration config)
{
// 全局 Token 认证
config.Filters.Add(new TokenAuthAttribute());
// Web API 路由
config.MapHttpAttributeRoutes();
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "api/{controller}/{id}",
defaults: new { id = RouteParameter.Optional }
);
}
}Code language: PHP (php)
4.2 控制器/方法级别使用
如果不全局注册,可以在 Controller 或 Action 上单独标记:
using Demo.App_Start;
using System.Web.Http;
using System.Web.Mvc;
public class ValuesController : ApiController
{
/// <summary>
/// 需要 Token 认证的接口
/// </summary>
[TokenAuth]
public string Get(int id)
{
return "value";
}
/// <summary>
/// 允许匿名访问的接口(如登录)
/// </summary>
[AllowAnonymous]
public string Login(string username, string password)
{
// 校验用户名密码,成功后返回 Token
if (username == "admin" && password == "123456")
{
return "ddddd"; // 返回 Token
}
return "登录失败";
}
}Code language: HTML, XML (xml)
五、使用 Postman 测试
5.1 不带 Token 请求(预期 401)
| 步骤 | 操作 |
|---|---|
| 请求方式 | GET |
| URL | http://localhost:端口/api/values/1 |
| Header | 不添加 Token |
| 结果 | 返回 401 Unauthorized |
5.2 带 Token 请求(预期 200)
| 步骤 | 操作 |
|---|---|
| 请求方式 | GET |
| URL | http://localhost:端口/api/values/1 |
| Header | 添加 Token: ddddd |
| 结果 | 返回 "value" |
5.3 登录获取 Token
| 步骤 | 操作 |
|---|---|
| 请求方式 | GET |
| URL | http://localhost:端口/api/values/login?username=admin&password=123456 |
| Header | 无 |
| 结果 | 返回 "ddddd"(Token 值) |
六、完整可运行示例(含登录签发 Token)
using Demo.App_Start;
using System;
using System.Web.Http;
using System.Web.Mvc;
public class AuthController : ApiController
{
/// <summary>
/// 登录接口(匿名访问)
/// </summary>
[AllowAnonymous]
[System.Web.Http.HttpPost]
public object Login([FromBody] LoginModel model)
{
if (model.UserName == "admin" && model.Password == "123456")
{
// 生成 Token(实际项目中应使用 GUID / JWT / 加密字符串)
string token = Guid.NewGuid().ToString("N");
// 将 Token 存入数据库或缓存(此处省略)
// CacheHelper.Set(token, model.UserName, expireMinutes: 120);
return new
{
Success = true,
Token = token,
Message = "登录成功"
};
}
return new
{
Success = false,
Message = "用户名或密码错误"
};
}
/// <summary>
/// 获取用户信息(需 Token)
/// </summary>
[TokenAuth]
[System.Web.Http.HttpGet]
public object GetUserInfo()
{
return new
{
UserName = "admin",
Role = "Administrator",
LoginTime = DateTime.Now
};
}
}
public class LoginModel
{
public string UserName { get; set; }
public string Password { get; set; }
}Code language: HTML, XML (xml)
七、使用要点小结
| 功能 | 实现方式 |
|---|---|
| Token 传递 | HTTP Header 自定义字段 Token |
| 自定义验证 | 继承 AuthorizeAttribute,重写 OnAuthorization |
| 获取 Header | actionContext.Request.Properties["MS_HttpContext"] → HttpContextBase |
| 匿名跳过 | [AllowAnonymous] 特性 |
| 返回 401 | HandleUnauthorizedRequest(actionContext) |
| 全局注册 | WebApiConfig.cs → config.Filters.Add(new TokenAuthAttribute()) |
| 测试工具 | Postman 在 Header 中添加 Token: xxx |
八、实际项目改进建议
| 项目 | 建议 |
|---|---|
| Token 生成 | 使用 Guid.NewGuid() 或 JWT(JSON Web Token) |
| Token 存储 | 数据库 / Redis / MemoryCache,记录用户ID、过期时间 |
| Token 过期 | 设置过期时间,过期后要求重新登录 |
| Token 刷新 | 提供 RefreshToken 机制 |
| Header 规范 | 使用标准 Authorization: Bearer xxx 替代自定义 Token |
| HTTPS | 生产环境强制 HTTPS,防止 Token 被窃听 |
| 加密 | Token 可加密签名,防止篡改 |
Previous: 前后端分离的认证问题Token的理解