从Client应用11选5源码下载场景介绍IdentityServer4

从Client应用11选5源码下QQ2952777280【话仙源码论坛】 hxforum.com 场景介绍IdentityServer4(五)
本节将在第四节基础上介绍如何实现IdentityServer4从数据库获取User进行验证,并对Claim进行权限设置。

一、新建Web API资源服务,命名为ResourceAPI
(1)新建API项目,用来进行user的身份验证服务。

(2)配置端口为5001

安装Microsoft.EntityFrameworkCore包

安装Microsoft.EntityFrameworkCore.SqlServer包

安装Microsoft.EntityFrameworkCore.Tools包

(3)我们在项目添加一个 Entities文件夹。

新建一个User类,存放用户基本信息,其中Claims为一对多的关系。

其中UserId的值是唯一的。

复制代码
public class User
{
[Key]
[MaxLength(32)]
public string UserId { get; set; }

    [MaxLength(32)]
    public string UserName { get; set; }

    [MaxLength(50)]
    public string Password { get; set; }

    public bool IsActive { get; set; }//是否可用

    public virtual ICollection<Claims> Claims { get; set; }

}
复制代码
新建Claims类

扫描二维码关注公众号,回复: 4667938 查看本文章

复制代码
public class Claims
{
[MaxLength(32)]
public int ClaimsId { get; set; }

    [MaxLength(32)]
    public string Type { get; set; }

    [MaxLength(32)]
    public string Value { get; set; }

    public virtual User User { get; set; }

}

复制代码
继续新建 UserContext.cs

复制代码
public class UserContext:DbContext
{

    public UserContext(DbContextOptions<UserContext> options)
        : base(options)
    {
    }
    public DbSet<User> Users { get; set; }
    public DbSet<Claims> UserClaims { get; set; }

}
复制代码
(4)修改startup.cs中的ConfigureServices方法,添加SQL Server配置。

复制代码
public void ConfigureServices(IServiceCollection services)
{
var connection = “Data Source=localhost;Initial Catalog=UserAuth;User ID=sa;Password=Pwd”;
services.AddDbContext(options => options.UseSqlServer(connection));
// Add framework services.
services.AddMvc();
}
复制代码
完成后在程序包管理器控制台运行:Add-Migration InitUserAuth

生成迁移文件。

(5)添加Models文件夹,定义User的model类和Claims的model类。

在Models文件夹中新建User类:

复制代码
public class User
{
public string UserId { get; set; }

    public string UserName { get; set; }

    public string Password { get; set; }

    public bool IsActive { get; set; }

    public ICollection<Claims> Claims { get; set; } = new HashSet<Claims>();

}
复制代码
新建Claims类:

复制代码
public class Claims
{
public Claims(string type,string value)
{
Type = type;
Value = value;
}
public string Type { get; set; }
public string Value { get; set; }
}
复制代码
做Model和Entity之前的映射。

添加类UserMappers:

复制代码
public static class UserMappers
{
static UserMappers()
{
Mapper = new MapperConfiguration(cfg => cfg.AddProfile())
.CreateMapper();
}
internal static IMapper Mapper { get; }

    /// <summary>
    /// Maps an entity to a model.
    /// </summary>
    /// <param name="entity">The entity.</param>
    /// <returns></returns>
    public static Models.User ToModel(this User entity)
    {
        return Mapper.Map<Models.User>(entity);
    }

    /// <summary>
    /// Maps a model to an entity.
    /// </summary>
    /// <param name="model">The model.</param>
    /// <returns></returns>
    public static User ToEntity(this Models.User model)
    {
        return Mapper.Map<User>(model);
    }
}

复制代码
类UserContextProfile:

复制代码
public class UserContextProfile: Profile
{
public UserContextProfile()
{
//entity to model
CreateMap<User, Models.User>(MemberList.Destination)
.ForMember(x => x.Claims, opt => opt.MapFrom(src => src.Claims.Select(x => new Models.Claims(x.Type, x.Value))));

        //model to entity
        CreateMap<Models.User, User>(MemberList.Source)
            .ForMember(x => x.Claims,
                opt => opt.MapFrom(src => src.Claims.Select(x => new Claims { Type = x.Type, Value = x.Value })));
    }
}

复制代码
(6)在startup.cs中添加初始化数据库的方法InitDataBase方法,对User和Claim做级联插入。

复制代码
public void InitDataBase(IApplicationBuilder app)
{

        using (var serviceScope = app.ApplicationServices.GetService<IServiceScopeFactory>().CreateScope())
        {
            serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>().Database.Migrate();

            var context = serviceScope.ServiceProvider.GetRequiredService<Entities.UserContext>();
            context.Database.Migrate();
            if (!context.Users.Any())
            {
                User user = new User()
                {
                    UserId = "1",
                    UserName = "zhubingjian",
                    Password = "123",
                    IsActive = true,
                    Claims = new List<Claims>
                    {
                        new Claims("role","admin")
                    }
                };
                context.Users.Add(user.ToEntity());
                context.SaveChanges();
            }
        }
    }

复制代码
(7)在startup.cs中添加InitDataBase方法的引用。

复制代码
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
InitDataBase(app);
app.UseMvc();
}
复制代码
运行程序,这时候数据生成数据库UserAuth,表Users中有一条UserName=zhubingjian,Password=123的数据。

二、实现获取User接口,进行身份验证
(1)先对API进行保护,在Startup.cs的ConfigureServices方法中添加:

复制代码
//protect API
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();

        services.AddAuthentication("Bearer")
            .AddIdentityServerAuthentication(options =>
            {
                options.Authority = "http://localhost:5000";
                options.RequireHttpsMetadata = false;

                options.ApiName = "api1";
            });

复制代码
并在Configure中,将UseAuthentication身份验证中间件添加到管道中,以便在每次调用主机时自动执行身份验证。

app.UseAuthentication();

(2)接着,实现获取User的接口。

在ValuesController控制中,添加如下代码:

复制代码
UserContext context;
public ValuesController(UserContext _context)
{
context = _context;
}

//只接受role为AuthServer授权服务的请求
[Authorize(Roles = “AuthServer”)]
[HttpGet("{userName}/{password}")]
public IActionResult AuthUser(string userName, string password)
{
var res = context.Users.Where(p => p.UserName == userName && p.Password == password)
.Include(p=>p.Claims)
.FirstOrDefault();
return Ok(res.ToModel());
}
复制代码

好了,资源服务器获取User的接口完成了。

(3)接着回到AuthServer项目,把User改成从数据库进行验证。

找到AccountController控制器,把从内存验证User部分修改成从数据库验证。

主要修改Login方法,代码给出了简要注释:

复制代码
public async Task Login(LoginInputModel model, string button)
{
// check if we are in the context of an authorization request
AuthorizationRequest context = await _interaction.GetAuthorizationContextAsync(model.ReturnUrl);

        // the user clicked the "cancel" button
        if (button != "login")
        {
            if (context != null)
            {
                // if the user cancels, send a result back into IdentityServer as if they 
                // denied the consent (even if this client does not require consent).
                // this will send back an access denied OIDC error response to the client.
                await _interaction.GrantConsentAsync(context, ConsentResponse.Denied);

                // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                if (await _clientStore.IsPkceClientAsync(context.ClientId))
                {
                    // if the client is PKCE then we assume it's native, so this change in how to
                    // return the response is for better UX for the end user.
                    return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                }

                return Redirect(model.ReturnUrl);
            }
            else
            {
                // since we don't have a valid context, then we just go back to the home page
                return Redirect("~/");
            }
        }

        if (ModelState.IsValid)
        {
            //从数据库获取User并进行验证
            var client = _httpClientFactory.CreateClient();
            //已过时
            DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
            TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
            var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

            //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            //{
            //    Address = "http://localhost:5000",
            //    ClientId = "AuthServer",
            //    ClientSecret = "secret",
            //    Scope = "api1"
            //});
            //if (tokenResponse.IsError) throw new Exception(tokenResponse.Error);
            client.SetBearerToken(tokenResponse.AccessToken);

            try
            {
                var response = await client.GetAsync("http://localhost:5001/api/values/" + model.Username + "/" + model.Password);
                if (!response.IsSuccessStatusCode)
                {
                    throw new Exception("Resource server is not working!");
                }
                else
                {
                    var content = await response.Content.ReadAsStringAsync();
                    User user = JsonConvert.DeserializeObject<User>(content);
                    if (user != null)
                    {
                        await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.UserId, user.UserName));

                        // only set explicit expiration here if user chooses "remember me". 
                        // otherwise we rely upon expiration configured in cookie middleware.
                        AuthenticationProperties props = null;
                        if (AccountOptions.AllowRememberLogin && model.RememberLogin)
                        {
                            props = new AuthenticationProperties
                            {
                                IsPersistent = true,
                                ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)
                            };
                        };

                        //             context.Result = new GrantValidationResult(
                        //user.SubjectId ?? throw new ArgumentException("Subject ID not set", nameof(user.SubjectId)),
                        //OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime,
                        //user.Claims);

                        // issue authentication cookie with subject ID and username
                        await HttpContext.SignInAsync(user.UserId, user.UserName, props);

                        if (context != null)
                        {
                            if (await _clientStore.IsPkceClientAsync(context.ClientId))
                            {
                                // if the client is PKCE then we assume it's native, so this change in how to
                                // return the response is for better UX for the end user.
                                return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });
                            }

                            // we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null
                            return Redirect(model.ReturnUrl);
                        }

                        // request for a local page
                        if (Url.IsLocalUrl(model.ReturnUrl))
                        {
                            return Redirect(model.ReturnUrl);
                        }
                        else if (string.IsNullOrEmpty(model.ReturnUrl))
                        {
                            return Redirect("~/");
                        }
                        else
                        {
                            // user might have clicked on a malicious link - should be logged
                            throw new Exception("invalid return URL");
                        }
                    }

                    await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"));
                    ModelState.AddModelError("", AccountOptions.InvalidCredentialsErrorMessage);
                }
            }
            catch (Exception ex)
            {
                await _events.RaiseAsync(new UserLoginFailureEvent("Resource server", "is not working!"));
                ModelState.AddModelError("", "Resource server is not working");
            }

        }

        // something went wrong, show form with error
        var vm = await BuildLoginViewModelAsync(model);
        return View(vm);
    }

复制代码
可以看到,在IdentityServer4更新后,旧版获取tokenResponse的方法已过时,但我按官网文档的说明,使用新方法(注释的代码),获取不到信息,还望大家指点。

官网链接:https://identitymodel.readthedocs.io/en/latest/client/token.html

所以这里还是按老方法来获取tokenResponse。

(4)到这步后,可以把Startup中ConfigureServices方法里面的AddTestUsers去掉了。

运行程序,已经可以从数据进行User验证了。

点击进入About页面时候,出现没有权限提示,我们会发现从数据库获取的User中的Claims不起作用了。

三、使用数据数据自定义Claim
为了让获取的Claims起作用,我们来实现IresourceOwnerPasswordValidator接口和IprofileService接口。

(1)在AuthServer中添加类ResourceOwnerPasswordValidator,继承IresourceOwnerPasswordValidator接口。

复制代码
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
private readonly IHttpClientFactory _httpClientFactory;
public ResourceOwnerPasswordValidator(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var client = _httpClientFactory.CreateClient();
//已过时
DiscoveryResponse disco = await DiscoveryClient.GetAsync(“http://localhost:5000”);
TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, “AuthServer”, “secret”);
var tokenResponse = await tokenClient.RequestClientCredentialsAsync(“api1”);

            //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
            //{
            //    Address = "http://localhost:5000",
            //    ClientId = "AuthServer",
            //    ClientSecret = "secret",
            //    Scope = "api1"
            //});
            //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
            client.SetBearerToken(tokenResponse.AccessToken);

            var response = await client.GetAsync("http://localhost:5001/api/values/" + context.UserName + "/" + context.Password);
            if (!response.IsSuccessStatusCode)
            {
                throw new Exception("Resource server is not working!");
            }
            else
            {
                var content = await response.Content.ReadAsStringAsync();
                User user = JsonConvert.DeserializeObject<User>(content);
                //get your user model from db (by username - in my case its email)
                //var user = await _userRepository.FindAsync(context.UserName);
                if (user != null)
                {
                    //check if password match - remember to hash password if stored as hash in db
                    if (user.Password == context.Password)
                    {
                        //set the result
                        context.Result = new GrantValidationResult(
                            subject: user.UserId.ToString(),
                            authenticationMethod: "custom",
                            claims: GetUserClaims(user));

                        return;
                    }
                    context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "Incorrect password");
                    return;
                }
                context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "User does not exist.");
                return;
            }
        }
        catch (Exception ex)
        {

        }

    }
    public static Claim[] GetUserClaims(User user)
    {
        List<Claim> claims = new List<Claim>();
        Claim claim;
        foreach (var itemClaim in user.Claims)
        {
            claim = new Claim(itemClaim.Type, itemClaim.Value);
            claims.Add(claim);
        }
        return claims.ToArray();
    }

}
复制代码
(2)ProfileService类实现IprofileService接口:

复制代码
public class ProfileService : IProfileService
{
private readonly IHttpClientFactory _httpClientFactory;
public ProfileService(IHttpClientFactory httpClientFactory)
{
_httpClientFactory = httpClientFactory;
}
////services
//private readonly IUserRepository _userRepository;

    //public ProfileService(IUserRepository userRepository)
    //{
    //    _userRepository = userRepository;
    //}

    //Get user profile date in terms of claims when calling /connect/userinfo
    public async Task GetProfileDataAsync(ProfileDataRequestContext context)
    {
        try
        {
            //depending on the scope accessing the user data.
                       var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");
                //获取User_Id
                if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                {
                    var client = _httpClientFactory.CreateClient();
                    //已过时
                    DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                    TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                    var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                    //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                    //{
                    //    Address = "http://localhost:5000",
                    //    ClientId = "AuthServer",
                    //    ClientSecret = "secret",
                    //    Scope = "api1"
                    //});
                    //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
                    client.SetBearerToken(tokenResponse.AccessToken);

                    //根据User_Id获取user
                    var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                    //get user from db (find user by user id)
                    //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                    var content = await response.Content.ReadAsStringAsync();
                    User user = JsonConvert.DeserializeObject<User>(content);
                    // issue the claims for the user
                    if (user != null)
                    {
                        //获取user中的Claims
                        var claims = GetUserClaims(user);
                        //context.IssuedClaims = claims.Where(x => context.RequestedClaimTypes.Contains(x.Type)).ToList();
                        context.IssuedClaims = claims.ToList();
                    }
              }
        }
        catch (Exception ex)
        {
            //log your error
        }
    }

    //check if user account is active.
    public async Task IsActiveAsync(IsActiveContext context)
    {
        try
        {
            var userId = context.Subject.Claims.FirstOrDefault(x => x.Type == "sub");

                    if (!string.IsNullOrEmpty(userId?.Value) && long.Parse(userId.Value) > 0)
                    {
                        //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                        var client = _httpClientFactory.CreateClient();
                        //已过时
                        DiscoveryResponse disco = await DiscoveryClient.GetAsync("http://localhost:5000");
                        TokenClient tokenClient = new TokenClient(disco.TokenEndpoint, "AuthServer", "secret");
                        var tokenResponse = await tokenClient.RequestClientCredentialsAsync("api1");

                        //var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
                        //{
                        //    Address = "http://localhost:5000",
                        //    ClientId = "AuthServer",
                        //    ClientSecret = "secret",
                        //    Scope = "api1"
                        //});
                        //if (TokenResponse.IsError) throw new Exception(TokenResponse.Error);
                        client.SetBearerToken(tokenResponse.AccessToken);

                        //根据User_Id获取user
                        var response = await client.GetAsync("http://localhost:5001/api/values/" + long.Parse(userId.Value));
                        //get user from db (find user by user id)
                        //var user = await _userRepository.FindAsync(long.Parse(userId.Value));
                        var content = await response.Content.ReadAsStringAsync();
                        User user = JsonConvert.DeserializeObject<User>(content);
                        if (user != null)
                        {
                            if (user.IsActive)
                            {
                                context.IsActive = user.IsActive;
                            }
                        }                
                    }
        }
        catch (Exception ex)
        {
            //handle error logging
        }
    }
    public static Claim[] GetUserClaims(User user)
    {
        List<Claim> claims = new List<Claim>();
        Claim claim;
        foreach (var itemClaim in user.Claims)
        {
            claim = new Claim(itemClaim.Type, itemClaim.Value);
            claims.Add(claim);
        }
        return claims.ToArray();
    }
}

复制代码
(3)发现代码里面需要在ResourceAPI项目的ValuesController控制器中

添加根据UserId获取User的Claims的接口。

复制代码
Authorize(Roles = “AuthServer”)]
[HttpGet("{userId}")]
public ActionResult Get(string userId)
{
var user = context.Users.Where(p => p.UserId == userId)
.Include(p => p.Claims)
.FirstOrDefault();
return Ok(user.ToModel());
}
复制代码
(4)修改AuthServer中的Config中GetIdentityResources方法,定义从数据获取的Claims为role的信息。

复制代码
public static IEnumerable GetIdentityResources()
{
var customProfile = new IdentityResource(
name: “mvc.profile”,
displayName: “Mvc profile”,
claimTypes: new[] { “role” });
return new List
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
//new IdentityResource(“roles”,“role”,new List{ “role”}),
customProfile
};
}
复制代码
(5)在GetClients中把定义的mvc.profile加到Scope配置

(6)最后记得在Startup的ConfigureServices方法加上

.AddResourceOwnerValidator()

.AddProfileService();

运行后,出现熟悉的About页面(Access Token后面加上去的,源码上有添加方法)

本节介绍的IdentityServer4通过访问接口的形式验证从数据库获取的User信息。当然,也可以写成AuthServer授权服务通过连接数据库进行验证。

另外,授权服务访问资源服务API,用的是ClientCredentials模式(服务与服务之间访问)。

猜你喜欢

转载自blog.csdn.net/weixin_44294348/article/details/85274084