JWT认证服务与授权 .netCore

1.实现流程图

2.认证信息概述

Header:System.IdentityModel.Tokens.Jwt.JwtHeader

Payload: System.IdentityModel.Tokens.Jwt.JwtPayload

Issuer: http://localhost:7200

Audience: http://localhost:7200

Expiration: 2025/4/11 15:06:14

Claim - Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid, Value: 19

Claim-Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone, Value: string

Claim -Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone, Value: string Claim-Type:http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress, Value: string Claim - Type: http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, Value: string

Claim -Type: userName, Value: string

Claim-Type: imageUrl, Value: string

Claim-Type: QQ, Value: string

Claim-Type: WeChat, Value: string

Claim - Type: Sex, Value: 1

Claim - Type: exp, Value: 1744383974

Claim - Type: iss, Value: http://localhost:7200

Claim - Type: aud, Value: http://localhost:7200

解释

Header(头部):

Header: System.IdentityModel.Tokens.Jwt.JwtHeader 表明了 JWT 的头部信息,它一般包含两部分内容:令牌的类型(通常是 JWT)和使用的签名算法,如 HmacSHA256 或 RSA 等。在这个输出中,只是显示了头部所属的类,并没有具体的头部信息内容。不过在实际的 JWT 中,头部会以 JSON 格式存在并经过 Base64Url 编码成为 JWT 的第一部分。

Payload(负载):Payload: System.IdentityModel.Tokens.Jwt.JwtPayload 指出了 JWT 的负载部分。负载包含声明(claims),这些声明是关于实体(通常是用户)和其他数据的陈述。同样,这里显示的是负载所属的类,实际的负载信息也会以 JSON 格式存在并经过 Base64Url 编码成为 JWT 的第二部分。

Issuer(签发者):Issuer: http://localhost:7200 表示该 JWT 的签发者,即生成和颁发这个令牌的实体,这里是本地的一个地址 http://localhost:7200

Audience(受众):Audience: http://localhost:7200 指明了该 JWT 的预期接收者,也就是这个令牌是为谁准备的。这里的受众也是 http://localhost:7200,意味着该令牌是给本地这个地址的服务使用的。

Expiration(过期时间):Expiration: 2025/4/11 15:06:14 显示了 JWT 的过期时间,在这个时间之后,该令牌将不再被认为是有效的。

Claims(声明):http://schemas.xmlsoap.org/ws/2005/05/identity/claims/sid, Value: 19:表示用户的安全标识符(Sid),值为 19,用于唯一标识用户。

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/mobilephone, Value: string:声明了用户的手机号码,但值显示为 string,可能是实际值未正确获取或填充。

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/otherphone, Value: string:其他电话号码的声明,值同样为 string。

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/streetaddress, Value: string:用户街道地址的声明,值为 string。

http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress, Value: string:用户电子邮件地址的声明,值为 string。

userName, Value: string:用户名的声明,值为 string。

imageUrl, Value: string:用户头像地址的声明,值为 string。

QQ, Value: string:用户 QQ 号码的声明,值为 string。

WeChat, Value: string:用户微信号码的声明,值为 string。

Sex, Value: 1:用户性别的声明,值为 1,可能表示某种性别标识(如 1 代表男性等,具体含义需参考应用的定义)。

exp, Value: 1744383974:这是一个标准的 JWT 声明,表示过期时间的 Unix 时间戳(从 1970 年 1 月 1 日 00:00:00 UTC 到过期时间的秒数),与前面显示的 2025/4/11 15:06:14 是对应的。

iss, Value: http://localhost:7200:再次声明了签发者,与前面的 Issuer 一致。

aud, Value: http://localhost:7200:再次声明了受众,与前面的 Audience 一致。

3.基础概念

3.1 JwtSecurityToken

JwtSecurityToken是在.NET 中用于处理 JSON Web Token(JWT)的一个类,它属于System.IdentityModel.Tokens.Jwt命名空间。以下是对其作用的详细介绍:

表示 JWT 结构:JWT 是一种紧凑、自包含的方式,用于在各方之间以 JSON 对象的形式安全地传输信息。JwtSecurityToken类将 JWT 的各个部分,如头部(Header)、载荷(Payload)和签名(Signature),表示为对象的属性,使得开发人员可以方便地访问和操作 JWT 的各个元素。例如,可以通过该类的属性获取 JWT 中的签发者(Issuer)、受众(Audience)、过期时间(Expiration Time)等信息。

创建 JWT:可以使用JwtSecurityToken类来创建新的 JWT。开发人员可以设置 JWT 的各种参数,如声明(Claims)、有效期、签名密钥等,然后通过相关的 JWT 生成机制将其转换为字符串形式的 JWT,以便在网络中传输或存储。

验证 JWT:在接收方,JwtSecurityToken类可用于验证接收到的 JWT 的有效性。它可以验证 JWT 的签名是否正确,以确保令牌在传输过程中未被篡改;还可以验证令牌的有效期、签发者和受众等信息是否符合预期,从而确保令牌的合法性和安全性。

提取信息:从 JWT 中提取出包含在载荷中的各种声明信息,这些声明可以包含用户的身份信息、权限信息等。开发人员可以通过JwtSecurityToken类提供的方法和属性,方便地获取这些声明,以便在应用程序中进行授权、身份验证等操作。例如,从 JWT 中提取用户的角色信息,以确定用户是否有权访问特定的资源或执行特定的操作。

3.2 ‌MemoryCache

‌‌MemoryCache是一种基于内存的缓存服务,主要用于存储和快速访问对象,减少对外部数据源(如数据库)的频繁访问,从而提高应用程序的性能。MemoryCache是ASP.NET Core框架的一部分,位于Microsoft.Extensions.Caching.Memory命名空间中,实现了IMemoryCache接口‌1。

工作原理和过期策略

MemoryCache使用键值对来存储数据,每个数据项都可以设置一个过期时间。当缓存项目到达其过期时间或系统在资源压力下时,该项目会从缓存中删除。MemoryCache使用两种基本算法:LRU(Least Recently Used)和Expiration策略。

LRU‌:这是一种基于使用频率的算法。当内存不足以容纳新的缓存项时,此算法会移除最近最少使用的缓存项。这种策略假定未来数据访问模式将类似于过去的数据访问模式,优先保留最近频繁访问的数据。

‌Expiration‌:每个缓存项被添加到缓存时都可以设置一个过期时间。当缓存项达到其设定的过期时间后,它将从缓存中自动被移除。这种策略确保了缓存中的数据不会过时,并允许开发者根据每个缓存项的实际需求设定不同的过期时间。

适用场景和性能优化建议

MemoryCache适用于存储经常访问但更新不频繁的数据,如用户会话信息、常用配置等。通过合理设置缓存的过期时间和使用LRU算法,可以优化缓存的利用率和性能。此外,定期清理不常用的缓存项和监控内存使用情况也是保持系统性能的重要措施‌

使用方法

‌添加内存缓存服务‌:在Startup.cs文件中,通过依赖注入将内存缓存服务添加到应用程序中。例如:

csharpCopy Code

services.AddMemoryCache();

2.注入和使用内存缓存‌:在控制器或服务中注入IMemoryCache接口,并使用它来存储和检索缓存数据。例如:

csharpCopy Code

publicclassMyController : Controller

{

privatereadonly IMemoryCache _memoryCache;

publicMyController(IMemoryCache memoryCache)

{

_memoryCache = memoryCache;

}

public IActionResult Index()

{

string cachedData = null;

if (!_memoryCache.TryGetValue("myKey", out cachedData))

{

cachedData = "This is the data to cache";

_memoryCache.Set("myKey", cachedData, new MemoryCacheEntryOptions() { SlidingExpiration = TimeSpan.FromMinutes(5) });

}

return View(cachedData);

}

}

3.3 特性

在.NET 中,特性(Attributes)是一种用于在运行时提供有关代码元素(如类、方法、属性等)的附加信息的机制。下面为你介绍其原理和常见应用场景:

特性原理

元数据:.NET 中的特性本质上是一种元数据,它们被嵌入到程序集的二进制代码中。这些元数据可以在运行时通过反射机制来访问,使得程序能够根据这些附加信息来做出不同的行为。

特性类:特性是通过定义特性类来实现的,这些特性类继承自 System.Attribute 基类。当在代码中使用特性时,实际上是在创建特性类的实例,并将其应用到相应的代码元素上。

特性的常见应用场景

验证:可以使用特性来标记方法或属性,以指示它们需要进行特定类型的验证。例如,在 ASP.NET Core 应用程序中,可以使用 [Required] 特性标记模型类的属性,表示该属性在接收用户输入时是必填的。

授权:在身份验证和授权场景中,特性用于限制对特定资源或操作的访问。例如,在 ASP.NET Core 中,[Authorize] 特性可以应用于控制器或控制器方法,以要求用户进行身份验证和授权才能访问相应的端点。

数据映射:在数据访问层,特性可用于将类的属性与数据库表的列进行映射。例如,Entity Framework Core 中使用 [Column] 特性来指定实体类属性与数据库表列之间的对应关系。

日志记录:通过自定义特性,可以标记需要进行日志记录的方法或类。在运行时,利用反射检查这些特性,并在方法执行前后记录相关的日志信息,以便于跟踪和调试。

Web 服务契约:在创建 Web 服务时,特性用于定义服务契约,如 [WebMethod] 特性用于标记 Web 服务中的方法,使其可以通过网络被远程调用。

3.4 有效载荷

在 JSON Web Token(JWT)中,有效载荷(Payload)里的声明(Claim)是非常重要的组成部分,以下为你详细介绍:

定义与本质:

声明是关于实体(通常指用户)和其他数据的陈述,是一组键值对。JWT 的有效载荷由一系列这样的声明组成,这些声明被编码成 JSON 对象,是 JWT 的第二部分(第一部分是头部,第三部分是签名),并且经过 Base64Url 编码。

常见的声明类型:

注册声明:这是一组预定义的声明,虽然不是强制的,但被广泛使用。常见的注册声明包括:

iss(签发者):标识令牌的签发者,例如某个服务器或应用程序。如 iss: "example.com",表示该 JWT 是由 example.com 签发的。

exp(过期时间):令牌的过期时间,是一个 Unix 时间戳。当令牌的当前时间超过这个值时,令牌就被认为是无效的。例如 exp: 1672531200,表示到对应的时间点该令牌过期。

sub(主题):令牌所面向的主体,通常是用户的唯一标识。如 sub: "123456",表示该 JWT 是关于用户 123456 的。

aud(受众):令牌的预期接收者,可以是一个或多个实体。例如 aud: ["client1", "client2"],表示该 JWT 可被 client1 和 client2 使用。

公共声明:这些声明由使用 JWT 的各方协商定义,并且不是注册声明的一部分。比如自定义的用户角色声明,如 role: "admin",表示用户具有管理员角色。

私有声明:这些声明是在使用 JWT 的各方之间保持私密的声明,通常用于在不影响其他方的情况下传递特定信息。例如应用程序内部使用的一些标识或配置信息。

作用与用途:

身份验证:通过有效载荷中的声明,服务器可以验证请求的用户身份。例如根据 sub 声明确定用户是谁,结合其他声明如 role 等进一步确认用户的身份信息。

授权:服务器可以根据声明来判断用户是否有权限访问特定的资源或执行特定的操作。例如,当用户请求访问某个 API 端点时,服务器检查 role 声明,如果用户具有相应的角色权限,则允许访问。

传递信息:可以在不同的系统或服务之间传递一些基本的用户信息或上下文信息,而无需多次查询数据库或其他数据源。比如传递用户的邮箱地址(email 声明)、手机号码(phone 声明)等信息。

3.5 API请求流程

1.创建 axios 实例:使用 axios.create 创建一个 service 实例,并设置基础 URL 和请求超时时间。

2.请求拦截器:在请求发送之前,检查本地存储中是否存在 accessToken,如果存在则将其添加到请求头的 Authorization 字段中。

3.响应拦截器:对于成功的响应,直接返回响应数据。

4.对于失败的响应,如果是 401 未授权错误且原始请求未重试过:

尝试从本地存储获取 refreshToken。

5.如果存在 refreshToken,发送刷新令牌的请求获取新的 accessToken,更新本地存储和 axios 实例的请求头,并重新发送原始请求。

6.如果刷新令牌失败或没有 refreshToken,清除本地存储的 accessToken 和 refreshToken,并跳转到登录页面,同时显示错误提示信息。

7.导出实例:将配置好的 axios 实例导出,以便在其他模块中使用来发送 API 请求。

8.原理:

分离配置和请求逻辑

创建 axios 实例并设置 baseURL 和 timeout 等配置,将通用的配置与具体的请求逻辑分离,使得代码更易于维护和修改。如果需要更改基础 URL 或调整超时时间,只需在一处进行修改,而不必在每个请求处都进行更改。

统一处理请求头

请求拦截器中统一处理 accessToken 的设置。当需要发送带有身份验证信息的请求时,只需在拦截器中统一添加 Authorization 头,而不必在每个请求方法中重复编写添加 accessToken 的代码。这样可以确保所有请求都能正确携带身份验证信息,同时也方便对 accessToken 的管理和更新。

处理身份验证和授权问题

响应拦截器中对 401 未授权错误的处理,实现了自动刷新 accessToken 的功能。当 accessToken 过期时,通过 refreshToken 来获取新的 accessToken,然后自动重试原始请求,使得用户在访问受保护资源时,即使 accessToken 过期也能继续操作,而无需用户手动重新登录,提高了用户体验。

对于 403 禁止访问错误的处理,通过提示用户没有权限操作,明确告知用户问题所在,方便用户与管理员沟通解决权限问题。

错误处理和用户引导

在响应拦截器中,对于各种错误情况进行了统一的处理。除了处理身份验证和授权相关的错误外,还可以在拦截器中对其他常见的错误进行处理,如网络错误、服务器错误等,并根据不同的错误类型给出相应的提示信息,引导用户进行正确的操作,例如提示用户检查网络连接、重新登录等,使得应用的错误处理更加统一和友好。

通过这样的流程设置,可以提高代码的复用性、可维护性和可扩展性,同时为用户提供更好的体验,确保应用在处理 API 请求时的稳定性和可靠性。

4.代码说明

4.1 accesstoken的生成

  1. 获取用户信息(用户实体类UserModel)
  2. 通过UserToClaim方法,传入UserModel,获取有效载荷

3.设置有效时间

4.实例化JwtSecurityToken对象,并对 issuer、audience、claims、expires、signingCredentials元素进行赋值。

5.读取设置的SecurityKey信息,并通过Encoding.UTF8.GetBytes转为UTF8的数组。

6.再反射转换为SymmetricSecurityKey对象key

7.再通过SigningCredentials进行HmacSha256的加密处理,形成SigningCredentials的签名对象。

8.通过JwtSecurityTokenHandler的WriteToken,将实例化的JwtSecurityToken对象,转换为字符串令牌。

​​​​​​​4.2 refreshToken的生成

1.获取新的guid。

2.基于guid,通过Claim方法来获取有效载荷。

3.设置有效时间

4.实例化JwtSecurityToken对象,并对 issuer、audience、claims、expires、signingCredentials元素进行赋值。

5.读取设置的SecurityKey信息,并通过Encoding.UTF8.GetBytes转为UTF8的数组。

6.再反射转换为SymmetricSecurityKey对象key

7.再通过SigningCredentials进行HmacSha256的加密处理,形成SigningCredentials的签名对象。

8.通过JwtSecurityTokenHandler的WriteToken,将实例化的JwtSecurityToken对象,转换为字符串令牌。

9.存入MemoryAcache,将refreshToken存储,并设置存储时长。(注意:accessToken是在调用认证的时候,再接口认证的时候进行判断的,通过时间点)《超时有两种判断,一个是在Memoryacache,一个是在认证的时候的时间点之间的时长间隔》

​​​​​​​4.3 刷新AccessToken

当accesstoken过期的情况下,则需要采用refeshtokenguid来获取最新的accesstoken,保持访问的状态。

以上的请求主要存在于 JavaScript 的基于 Axios 的 HTTP 服务封装,主要用于处理 API 请求、请求拦截、响应拦截以及使用刷新令牌(refreshToken)刷新访问令牌(accessToken)。

​​​​​​​4.4 身份验证和授权的注册

主要时进行实例化、参数设置和事件的绑定

​​​​​​​4.5 依赖注入

builder.RegisterRefreshTokenAuthorization();

var app = builder.Build();

​​​​​​​4.6 接口应用

接口应用实例,采用【特性的应用】

菜单获取

/// <summary>

/// 获取菜单列表

/// </summary>

/// <returns></returns>

HttpGet()

Authorize(AuthenticationSchemes = JwtBearerDefaults.AuthenticationScheme, Policy = "MenuPolicy")

public async Task<JsonResult> GetMenuTreeListAsync()

{

//在这里要解析到当前请求的用户是

//鉴权授权

//这里能够拿到Userid ,说明token必然已经验证通过了

string? strUserId = HttpContext.User?.FindFirst(ClaimTypes.Sid)?.Value;

if (string.IsNullOrWhiteSpace(strUserId))

{

return await Task.FromResult(new JsonResult(new ApiDataResult<int>()

{

Message = "没有token权限",

Success = false,

OValue = 401

}));

}

//strUserId = "2";

var menusTreeList = _IMenuService.GetMenusTreeList(Convert.ToInt32(strUserId));

var result = new JsonResult(new ApiDataResult<List<MenuTreeDto>>()

{

Data = menusTreeList,

Success = true,

Message = "获取菜单列表"

});

return await Task.FromResult(result);

}

​​​​​​​4.7 前端请求的JS

  • 使用 service.interceptors.response.use 方法添加一个响应拦截器。

  • 从响应数据 response.data 中解构出 data、success、oValue 和 message 等字段。

  • 如果 success 为 false 且 oValue 为 401(表示未授权),则从 mainStore 中获取 refreshToken,调用 axionRefreshToken 函数刷新 accessToken。

  • 如果刷新成功(reponse.success 为 true),更新 mainStore 中的 accessToken,并将新的 accessToken 设置到 service 的默认请求头中,然后重新发送原始请求(service(response.config))。

  • 如果刷新失败,将用户重定向到根路由(router.push("/")),并清除 mainStore 中的 accessToken、menulist、refreshToken 和 user 等信息。

  • 如果 success 为 false 且 oValue 为 403(表示权限不足),使用 ElMessage(可能是 Element UI 中的消息提示组件)显示一个警告消息,提示用户没有操作权限。

  • 最后返回响应数据 response,让后续的代码可以处理正常的响应。

    import axios from "axios";

    import { apiUrl,authURL } from '../commom/index'

    import mainStore from '../stores/counter'

    import router from '../router/index'

    const service = axios.create({ baseURL: apiUrl() });

    //请求拦截器---在axios 发起请求之前要做的事儿 aop思想~~

    service.interceptors.request.use(config => {

    复制代码
      //可以在这里配置token
    
      var token = mainStore().accessToken;
    
      //这里就是配置 axiox 请求api的时候,带上的token
    
      config.headers.Authorization = 'Bearer ' + token;
    
      return config;

    });

    //响应拦截器--从服务器响应之后--得到的结果,优先处理

    service.interceptors.response.use(async response => {

    复制代码
      //1.判断是否有权限问题
    
      let { data, success, oValue, message } = response.data;
    
      if (success == false && oValue == 401) {
    
          //获取refreshtoken
    
          var refreshToken = mainStore().refreshToken;
    
          const reponse = (await axionRefreshToken(refreshToken)).data;
    
          if (reponse.success) {
    
              mainStore().$patch({
    
                  accessToken: reponse.data
    
              })
    
              service.defaults.headers.common['Authorization'] = `Bearer ${data}`;
    
              return service(response.config);
    
          }
    
          else {
    
              router.push("/");
    
              mainStore().$patch({
    
                  accessToken: null,
    
                  menulist: [],
    
                  refreshToken: null,
    
                  user: null
    
              })
    
          }
    
      } else if (success == false && oValue == 403) {
    
          ElMessage({
    
              message: "对不起,您不具备操作此功能的权限,请联系管理员",
    
              type: 'warning',
    
          })
    
      }
    
      return response;

    });

    //使用refreshtoken 去刷新 accesstoken

    const axionRefreshToken = async (refreshtoken) => {

    复制代码
      const axionInstance = axios.create({
    
          baseURL: authURL(),
    
      })
    
      axionInstance.defaults.headers.common['Authorization'] = `Bearer ${refreshtoken}`;
    
      return axionInstance.get("Account");

    }export default service;

​​​​​​​4.8 前端登录的代码

登录后,会将token都存储mainStore,在请求前拦截和响应的时候,可以调出来使用。

复制代码
<template>

   <div class="login">

      <div class="login-content">

         <!-- 表单 -->

         <div class="login-form login-item">

            <p class="login-title">朝夕敏捷通用后台</p>

            <el-form :model="temp" :rules="rules" ref="ruleForm" label-width="70px" class="demo-ruleForm">

               <!-- 用户名 -->

               <el-form-item label="用户名" prop="name">

                  <el-input v-model="temp.name"></el-input>

               </el-form-item>

               <el-form-item label="密码" prop="password">

                  <el-input type="password" v-model="temp.password"></el-input>

               </el-form-item>

               <el-form-item>

                  <el-button @click="submitForm(ruleForm)">

                     登录</el-button>

               </el-form-item>

            </el-form>

         </div>

      </div>

   </div>

</template>

<script setup>

import { ref, reactive } from 'vue'

import { useRouter } from 'vue-router' //导入路由  

import { authURL } from '../../commom/index'

import axios from "axios";

import { ElMessage } from 'element-plus'

import mainStore from '../../stores/counter'

import jwt_decode from "jwt-decode";

const myaxios = axios.create({

   baseURL: authURL(),

})

const router = useRouter();

const temp = reactive({

   name: 'zhaoxi-admin',

   password: '123456'

})

const rules = reactive({

   name: [

      {

         required: true,

         message: '请输入用户名',

         trigger: 'blur',

      },

      {

         min: 3,

         max: 18,

         message: '长度为3-18位',

         trigger: 'blur',

      },

   ],

   password: [

      {

         required: true,

         message: '请输入密码',

         trigger: 'blur',

      },

      // {

      //    min: 3,

      //    max: 18,

      //    message: '长度为3-18位',

      //    trigger: 'blur',

      // },

   ],

})

const ruleForm = ref(null);

function submitForm(formEl) {

   formEl.validate(async valid => {

      if (valid) {

         console.log('ok');

         // 登录成功 开始加载当前用户所拥有的菜单

         let url = `Account`;

         let reponse = await myaxios.post(url, temp);

         let { data, success, message } = reponse.data;

         let user = jwt_decode(data.accesstoken)

         if (success) {

            mainStore().$patch({

               accessToken: data.accesstoken,

               refreshToken: data.refreshToken,

               user: user

            })

            router.push("/index")

         }

         else {

            ElMessage({

               message: message,

               type: 'warning',

            })

         }

      } else {

         console.log('error submit!!')

         return false

      }

   })

}

</script>
相关推荐
liyx061840 分钟前
C#关键字:in、out、ref、in T、out T、[In]、[Out]
c#
地球驾驶员2 小时前
NX二次开发C#---搭建NX开发环境(NX1926+VS2019)
开发语言·c#
Var_al16 小时前
Unity 设置弹窗Tips位置
游戏·unity·c#
浅陌sss17 小时前
C#容器源码分析 --- Stack<T>
c#
专注VB编程开发20年17 小时前
C#.NET模拟用户点击按钮button1.PerformClick自动化测试
开发语言·自动化测试·c#·vb.net
一个程序员(●—●)19 小时前
C#调用Lua方法1+C#调用Lua方法2,3
开发语言·c#·lua
佟格湾1 天前
聊透多线程编程-线程池-7.C# 三个Timer类
开发语言·后端·c#·多线程编程·多线程
浅陌sss1 天前
C#容器源码分析 --- Queue<T>
c#
FAREWELL000751 天前
C#核心学习(十五)面向对象--关联知识点(1)命名空间
学习·c#·命名空间