• 博客园logo
  • 会员
  • 周边
  • 新闻
  • 博问
  • 闪存
  • 众包
  • 赞助商
  • Chat2DB
    • 搜索
      所有博客
    • 搜索
      当前博客
  • 写随笔 我的博客 短消息 简洁模式
    用户头像
    我的博客 我的园子 账号设置 会员中心 简洁模式 ... 退出登录
    注册 登录
夕颜
博客园    首页    新随笔    联系   管理    订阅  订阅
ASP.NET Core IdentityServer4

参考:IdentityServer4实现Token登录以及权限控制_identityserver4服务端怎么在登录时获取token-CSDN博客

 前言:

1. OAuth 2.0是有关如何颁发访问令牌的规范,提供Access Token用于访问服务接口的
2. OpenID Connect是有关如何发行ID令牌的规范,提供Id Token用于用户身份标识(非敏感信息),Id Token是基于JWT格式
3. IdentityServer4服务中心默认提供接口/connect/token获取access token
4. IdentityServer4新版本新增ApiScope配置保护API资源,并使用ApiScope结合策略授权完成了一个简单的权限控制

1.基本步骤

  1. 安装相关 Nuget 包: IdentityServer4 2.5.3
  2. 注册服务
  3. 调用中间件
  4. webApI调用

  注册服务

builder.Services.AddIdentityServer()
    //配置证书
    .AddDeveloperSigningCredential()
    //配置API资源
    .AddInMemoryApiResources(Config.GetApiResources())
    //配置身份资源
    .AddInMemoryIdentityResources(Config.GetIdentityResources())
    //客户端信息配置
    .AddInMemoryClients(Config.GetClients(configuration))
    //用户验证  
    .AddResourceOwnerValidator<ResourcePasswordValidator>()

       //扩展claims
      .AddProfileService<ProfileService>();

    //测试用户
    .AddTestUsers(Config.GetUsers()); 
Config配置:
 public class Config
 {
     public static IEnumerable GetIdentityResources()
     {
         return new List
         {
             new IdentityResources.OpenId(),
             new IdentityResources.Profile()
         };
     }

     public static IEnumerable GetApis()
     {
         return new List
         {
             new ApiResource("OaIdService4",new List(){JwtClaimTypes.Subject})
         };
     }

     public static IEnumerable GetClients()
     {
         var clientList = AppSettings.app<List>("AuthClient") ?? new List();
         return clientList.Select(s => new Client()
         {
             ClientId = s.ClientId,
             AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
             //AccessToken过期时间(秒),默认为86400秒/1天
             AccessTokenLifetime = s.AccessTokenLifetime,

             //RefreshToken生命周期以秒为单位。默认为1296000秒/30天
             SlidingRefreshTokenLifetime = s.RefreshTokenLifetime,

             //刷新令牌时,将刷新RefreshToken的生命周期。RefreshToken的总生命周期不会超过AbsoluteRefreshTokenLifetime。
             RefreshTokenExpiration = TokenExpiration.Sliding,

             //AllowOfflineAccess 允许使用刷新令牌的方式来获取新的令牌
             AllowOfflineAccess = true,
             ClientSecrets =
             {
                 new Secret(s.ClientSecret.Sha256())
             },
             AllowedScopes = {
                 "OaIdService4",
                 StandardScopes.OfflineAccess,//如果要获取refresh_tokens ,必须在scopes中加上OfflineAccess
             }
         });
     }
 }
扩展claims:
  public class ProfileService : IProfileService
  {
      private readonly ILogger logger;

      public ProfileService(ILogger logger)
      {
          this.logger = logger;
      }

      public async Task GetProfileDataAsync(ProfileDataRequestContext context)
      {
          try
          {
              var claims = context.Subject.Claims.ToList();

              context.IssuedClaims = claims.ToList();
          }
          catch (Exception ex)
          {
              logger.LogError(ex.ToString());
          }
      }

      public async Task IsActiveAsync(IsActiveContext context)
      {
          context.IsActive = true;
      }
  }
用户验证:
public class ResourcePasswordValidator : IResourceOwnerPasswordValidator
{
    private readonly ITBSysUserBLL tbSysUserService = Container.Resolve();

    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        var loginQuery = new MethodLoginQuery();
       
        var method = context.Request.Raw["method"];
        if (!method.ToIsEmpty())
        {
            loginQuery.Method = EnumHelper.GetEnum(StringToValue.Set.ChangeInt(method));
        }
        var manageName = context.Request.Raw["manage"];
        if (!manageName.ToIsEmpty())
        {
            var n = StringToValue.Set.ChangeInt(manageName, 0);
            if (n > 0)
            {
                if (Enum.IsDefined(typeof(EManageName), n))
                {
                    loginQuery.AuthManage = new EManageName[] { EnumHelper.GetEnum(n) };
                }
                loginQuery.ManageName = EnumHelper.GetEnum(n);
            }
        }
        loginQuery.UserName = context.UserName;
        loginQuery.Password = context.Password;
        var claimList = new List() { new Claim(DevKeyCode.LoginManage, manageName)};
        if (loginQuery.AuthManage?.Length > 0)
        {
            claimList.Add(new Claim(DevKeyCode.AuthManage, loginQuery.AuthManage.Select(s => s.ToEString()).ToArray().ToJoin()));
        }
        var appkey = context.Request.Raw["appkey"];
        if (string.IsNullOrEmpty(appkey))
        {
            appkey = Guid.NewGuid().ToString().Replace("-", "").ToLower();
        }
        claimList.Add(new Claim(DevKeyCode.EncryptKey, appkey));
        var loginResult = tbSysUserService.SSOLogin(loginQuery);
        if (loginResult.Success)
        {
            var userInfo = loginResult.Data as UserAuthInfo;
            context.Result = new GrantValidationResult(
               subject: "userInfo",
               authenticationMethod: OidcConstants.AuthenticationMethods.Password,
               claims: GetUserClaims(userInfo, claimList.ToArray())
            );
          
        }
        else
        {
            context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, Convert.ToInt32(loginResult.Code).ToString());
        }
    }


    private Claim[] GetUserClaims(UserAuthInfo user, params Claim[] claimParam)
    {
        var list = new List
        {
            new Claim(DevKeyCode.SSOLoginUser,JHSC.Helper.Serializations.SerializationHelper.SerializeJson(user))
        };
        if (claimParam?.Length > 0)
        {
            list.AddRange(claimParam.ToArray());
        }
        return list.ToArray();
    }
}

调用中间件

app.UseIdentityServer();

WebApi调用

注入认证服务
引用:IdentityServer4.AccessTokenValidation 2.5.0
public static class AuthenticationIds4Setup { public static void AddAuthenticationIds4Setup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); services.AddAuthentication("Bearer").AddIdentityServerAuthentication(options => { options.RequireHttpsMetadata = false; options.Authority = AppSettings.app("IdentityServer", "Authority"); options.ApiName = AppSettings.app("IdentityServer", "ApiName"); }); } }
获取token
 private async Task LoginIn(LoginQuery query)
 {
     if (string.IsNullOrEmpty(query.UserName) || string.IsNullOrEmpty(query.Password) || string.IsNullOrEmpty(query.LoginManage))
     {
         return ResponseApp(null, ResultEnum.LackParam);
     }
     var appKey = Guid.NewGuid().ToString().Replace("-", "").ToLower();  //密钥
     var httpClient = new HttpClient();
     var parameters = new Dictionary<string, string>
     {
         { "manage", query.LoginManage },
         { "method", Convert.ToInt32(query.Method).ToString() },
         { "client_id", currentAuth.ClientId },
         { "client_secret", currentAuth.ClientSecret },
         { "grant_type", "password" },
         { "username", query.UserName },
         { "password", System.Web.HttpUtility.UrlDecode(query.Password) },
         { "appkey", appKey }
     };
     var response = await httpClient.PostAsync($"{authUrl}/connect/token", new FormUrlEncodedContent(parameters));
     var responseValue = await response.Content.ReadAsStringAsync();
     if (response.StatusCode == System.Net.HttpStatusCode.OK)
     {
         cacheDbService.RemoveModules(query.LoginManage, query.UserName);
         var authResult = SerializationHelper.DeserializeJson(responseValue);
         return ResponseApp(new
         {
             authResult.access_token,
             authResult.token_type,
             authResult.refresh_token,
             authResult.expires_in,
             appKey
         });
     }
     else
     {
         var errorResult = SerializationHelper.DeserializeJson(responseValue);
         ResultEnum errorEnum = ResultEnum.LoginFail;
         if (errorResult?.error == "invalid_grant")
         {
             if (!string.IsNullOrEmpty(errorResult.error_description))
             {
                 var errorNum = StringToValue.Set.ChangeNInt(errorResult.error_description);
                 if (errorNum.HasValue)
                 {
                     errorEnum = EnumHelper.GetEnum(errorNum.Value);
                 }
             }
         }
         return ResponseApp(errorEnum);
     }
 }
刷新token
 [HttpPost]
 public async Task RefreshToken()
 {
     var query = Decrypt(ESetType.AuthKey);
     var httpClient = new HttpClient();
     var parameters = new Dictionary<string, string>();
     parameters.Add("client_id", currentAuth.ClientId);
     parameters.Add("client_secret", currentAuth.ClientSecret);
     parameters.Add("grant_type", "refresh_token");
     parameters.Add("refresh_token", query.Refresh_token);
     var response = await httpClient.PostAsync($"{authUrl}/connect/token", new FormUrlEncodedContent(parameters));
     var responseValue = await response.Content.ReadAsStringAsync();
     dynamic objJson = SerializationHelper.DeserializeJson(responseValue);
     if (response.StatusCode == System.Net.HttpStatusCode.OK)
     {
         var authResult = SerializationHelper.DeserializeJson(responseValue);
         return ResponseApp(authResult,ResultEnum.Success);
     }
     else
     {
         return ResponseApp(ResultEnum.DataEx);
     }
 }

 

授权模式

  • 客户端模式(Client Credentials):和用户无关,用于应用程序与 API 资源的直接交互场景。
  • 密码模式(resource owner password credentials):和用户有关,一般用于第三方登录。
  • 简化模式-With OpenID(implicit grant type):仅限 OpenID 认证服务,用于第三方用户登录及获取用户信息,不包含授权。
  • 简化模式-With OpenID & OAuth(JS 客户端调用):包含 OpenID 认证服务和 OAuth 授权,但只针对 JS 调用(URL 参数获取),一般用于前端或无线端。
  • 混合模式-With OpenID & OAuth(Hybrid Flow):推荐使用,包含 OpenID 认证服务和 OAuth 授权,但针对的是后端服务调用。

1).简单模式 (client_credentials)

客户端模式只对客户端进行授权,不涉及到用户信息。如果你的api需要提供到第三方应用,第三方应用自己做用户授权,不需要用到你的用户资源,就可以用客户端模式,只对客户端进行授权访问api资源。这是一种最简单的模式,

只要client请求,我们就将AccessToken发送给它。这种模式是最方便但最不安全的模式。因此这就要求我们对client完全的信任,而client本身也是安全的

 

 

2).用户密码模式(password)

需要客户端提供用户名和密码,密码模式相较于客户端凭证模式。通过User的用户名和密码向Identity Server申请访问令牌。  这种方式需要用户给出自己的用户名/密码,显然风险很大,因此只适用于其他授权方式都无法采用的情况,而且必须是用户高度信任的应用

 

3).隐藏式 (implicit)

https://localhost:6005/connect/authorize?client_id=Implicit&redirect_uri=http://localhost:5000/Home&response_type=token&scope=WebApi

有些 Web 应用是前后端分离的纯前端应用,没有后端。这时就必须将令牌储存在前端。 这种方式没有授权码这个中间步骤,所以称为(授权码)"隐藏式"(implicit)。

隐藏式的认证步骤为:

第一步,A 网站提供一个链接,要求用户跳转到 认证中心,授权用户数据给 A 网站使用。

第二步,用户跳转到 认证中心,登录后同意给予 A 网站授权。这时,认证中心就会跳回redirect_uri参数指定的跳转网址,并且把令牌作为 URL 参数,传给 A 网站。

注意,令牌的位置是 URL 锚点(fragment),而不是查询字符串(querystring),这是因为 OAuth 2.0 允许跳转网址是 HTTP 协议,因此存在"中间人攻击"的风险,而浏览器跳转时,锚点不会发到服务器,就减少了泄漏令牌的风险。

 4).授权码模式(code)

https://localhost:6005/connect/authorize?client_id=GrantCode&redirect_uri=http://localhost:5000/Home&response_type=code&scope=WebApi

授权码(authorization code)方式,指的是第三方应用先申请一个授权码,然后再用该码获取令牌。 这种方式是最常用的流程,安全性也最高,它适用于那些有后端的 Web 应用。授权码通过前端传送,令牌则是储存在后端,而且所有与资源服务器的通信都在后端完成。这样的前后端分离,可以避免令牌泄漏。

 

1.在浏览器中访问OAuth2 服务器的认证接口:
      http://localhost:8020/oauth/authorize?response_type=code&client_id=test&redirect_uri=http://localhost:8080

  • response_type=code : 代表期望的请求响应类型为authorization code
  • client_id=test: client_id为你需要使用的客户端id
  • redirect_uri=http://localhost:8080 : redirect_uri是成功获取token之后,重定向的地址

2.访问认证接口成功之后,浏览器会跳转到OAuth2配置的登录页或者默认的security登录,正确输入用户名/密码之后。浏览器将会在重定向的地址上返回一个code。如下:
      http://localhost:8080?code=W3ixVa

  • code=W3ixVa : code就是OAuth2服务器返回的

3.然后使用获取到的code范围OAuth2认证服务器取到access_token,如下:
      http://localhost:8020/oauth/token?grant_type=authorization_code&code=W3ixVa&client_id=test&client_secret=secret&redirect_uri=http://localhost:8080

  • grant_type=authorization_code : grant_type为认证类型,当前为授权码模式
  • code=W3ixVa : code为上面获取到的code
  • client_id=test : client_id 与上面获取code的client_id需要一致
  • client_secret=secret : 为client_id对应的客户端的密钥
  • redirect_uri=http://localhost:8080 : : redirect_uri是成功获取token之后,重定向的地址

 

 

 

posted on 2024-12-16 10:34  夕颜~~  阅读(392)  评论(0)    收藏  举报
刷新页面返回顶部
博客园  ©  2004-2025
浙公网安备 33010602011771号 浙ICP备2021040463号-3