Api授权认证

2019-11-26 16:35 来源:未知

直接上重点~这里是github的地址。这里是github的地址。这里是github的地址。

 

这是介绍的地址: 

 

此次开源的是mac 1.0客户端的源码。额···也是那个不准备维护的、并且也不再维护的那个版本。之后将会对2.0的客户端进行维护。

 

先声明:这是我第一次用swift写osx系统软件,代码或者结构有什么问题也请大家不吝赐教,谢谢~~~

 

前言

这篇文章拖太久了,因为最近实在太忙了,加上这篇文章也非常长,所以花了不少时间,给大家说句抱歉。好,进入正题。目前的项目基本都是前后端分离了,前端分Web,Ios,Android。。。,后端也基本是Java,.NET的天下,后端渲染页面的时代已经一去不复返,当然这是时代的进步。前端调用后端服务目前大多数基于JSON的HTTP服务,那么就引入的我们今天的内容。客户端访问服务的时候怎么保证安全呢?很多同学都听说过OAuth2.0,都知道这个是用来做第三方登录的,实际上它也可以用来做Api的认证授权。不懂OAuth的同学可以先去看看阮一峰的OAuth的讲解,如果你看不懂的话,那就对了,笔者当初也看了很久,结合实际项目才明白。这章我会结合具体的例子帮助大家理解。同时也也会结合前几章的内容做一个整合,让大家对微服务架构以及API授权有一个更清晰的认识。

项目结构

图片 1

CNBlogsForMac从上到下目录解析:

- LGWebImage  自己写的,没用到,无用的东西,可以无视。之前是想用它来做Web图片引用的,后来找到了代替的方法。

- Entity   实体类:包括“新闻列表项”和“博客列表项”这两个实例。

- News    新闻:里面包括新闻列表的Controller,新闻列表的Cell和新闻详细页面的Controller。

- Blogs    博客:里面包括博客李冰的Controller,博客列表的Cell和博客详细页面的Controller。

- BaseControllerAndDeletage     基础的AppDelegate和一个BaseViewController。

- WebApi     网络请求接口:其中包括认证接口,博客接口和新闻接口。

 

业务场景

Api的认证授权,在微服务体系里面它也是一个服务,我们叫做认证授权中心。同时我们再提供一个用户中心和订单中心,构建我们的业务场景。我们模拟一个用户(客户端)是怎么一步一步获取我们的订单数据的,同时也结合前几张的内容搭建一个相对完整的微服务架构的demo。

想到什么写什么

这代码太简单了,我也不知道要怎么说,基本一看就懂了,想到什么写什么吧···

界面布局使用了storyboard。

WebApi->BaseWebApi.swift 中,主要做的是向服务器申请access_token的过程。这里有些问题,因为access_token不仅仅可以使用一次,而是可以使用多次,但是代码中每次请求都会获取新的access_token,这造成了一定的浪费,原本打算做个缓存的,后来也没搞。

NSTableView是一个非常坑的控件= =!至少我认为这家伙水好深···下载了apple官网的demo来看,然后查了很多很多资料,最后写成这样子了···写这个一直不会写,不知道怎没写···这里也算一个demo给大家留着看看吧~

BlogDetailViewController 和 NewsDetailViewController 中,均有一个 transferredString 方法,这是为了处理一个比较奇葩的问题。具体是哪个的原因我也没找到,但是我猜测:这是由于服务器返回的字符串,已经是转义后的字符串了,例如 "aaa\nhahah\taa" 这样,当我接收到以后,作为字符串,它又被转义了一次,这个字符串就变成了"aaa\nhahah\taa",然后我就转不回去了···只能替换掉了···后面还引用了一些js文件,这是为了页面展示排版用的,没啥大问题。

 

有任何问题欢迎留言。

程序清单列表

  • 服务中心
  • API网关
  • 认证授权中心
  • 用户中心
  • 订单中心

    用户中心和认证授权中心有耦合的情况,访问认证授权的时候要去验证用户的账号密码是否合法

下图是一个简单的架构草图
图片 2
服务中心和API网关大家看之前的文章来搭建,也可以直接看github上的源代码,没有什么变化。

认证授权中心

一直在说Ids4(IdentityServer4)这个框架,它实际上是一个实现了OAuth+OIDC(OpenId Connect)这两个功能的解决方案。那么OAuth和OIDC又到底是什么东西呢?简单来说OAuth就是帮助我们做授权获取token的,而OIDC就是帮助我们做认证这个token合法性的。一个完整的授权认证系统应该包含这两个功能。那么我们再谈一谈token,Ids4提供2种完全不一样的token加密方式,一种是JWT另一种叫Reference。那么这两种加密方式有何不同呢?JWT就是对这个字符串的一个加密算法,这个字符串包含了用户信息,客户端可以直接解析token,拿到用户信息,不需要和认证服授权务器去交互(程序首次加载的时候交互一次)。Reference更像Session,需要和认证服务器交互,由认证授权服务器去验证是否合法,每一次访问都需要和认证服务器进行交互,并且用户信息也是通过认证成功以后返回的。这两种方式各有优缺点。
JWT是一种加密方式,那么认证服务器不需要对token进行存储,而客户端也不需要找服务端验证,那么对于程序的性能是有很大的提升的,也不用考虑分布式和存储的问题,但是对于生成的token没办法控制,只能通过时效性来过期。
Reference的方式,token需要考虑分布式的存储,而且客户端需要一直和服务端认证,有一定的性能损耗,但是服务端可以对token进行控制,比如登出用户,修改密码都可以作废掉已经生成的token,这个时候再拿这个token是没办法使用的。然而不管是APP还是WEB让用户主动登出操作这是一个非常伪的需求,实际上即使是Reference方式token依然靠时效性来控制。
那么问题来了,当你的上级不懂技术的时候,问你万一我的token泄露了怎么办?你可以这样回答他。如果是在传输过程中的泄露,那么我们可以通过HTTPS的方式加密。程序代码里面用户相关的操作,都应该对传递的UserId参数和token里面解析出来UserId进行比较,如果出现不一致,那么这一定是一个非法请求。例如张三拿着李四的token去修改密码,肯定是修改不成功的。如果是在用户的客户端(WEB,APP)就把token泄露了,那么这个实际上这个客户端已经不止token泄露这么简单了,包括他所有的用户信息都泄露了,这个时候token已经没有了意义。就好比腾讯QQ加密算法做的如何如何牛逼,但是你泄露了你的QQ号和密码...
我们可以在过期时间上尽量短一点,客户端通过刷新token的方式不断获取新的token,而达到用户不用重复的登录,就能一直访问API接口。
至于两种方式的安全性我觉得都一样,微服务中我更倾向JWT这种方式,简单,高效。下面的代码我会模拟这两种模式,至于具体选择哪种方式大家根据实际的业务需求来。

小插曲:和几位技术大牛经过激烈的讨论,大家一致认为服务与服务之间的通信也是需要认证的,这样虽然增加了一定的性能损耗但是却更加的安全。我觉得有句话说的非常好,原则上内部其它系统都是不可信的。所以微服务之间的访问也得认证。

Reference方式的token,Ids4默认采用的内存做存储,也提供了EF for MS SQL 做分布式存储,而我们这里并不采用这种方式,我们采用redis来作为token的存储。

添加nuget引用
<PackageReference Include="Foundatio.Redis" Version="5.1.1478" />
<PackageReference Include="IdentityServer4" Version="2.0.2" />
<PackageReference Include="Pivotal.Discovery.Client" Version="1.1.0" />
Config.cs

配置Client信息,我们创建2个Client,一个采用JWT,一个采用Reference方式

new Client
{
    ClientId = "client.jwt",
    ClientSecrets =
    {
        new Secret("AB2DC090-0125-4FB8-902A-34AFB64B7D9B".Sha256())
    },
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
    AllowOfflineAccess = true,
    AccessTokenLifetime = accessTokenLifetime,
    AllowedScopes =
    {
        "api1"
    },
    AccessTokenType =AccessTokenType.Jwt
}
new Client
{
    ClientId = "client.reference",
    ClientSecrets =
    {
        new Secret("A30E6E57-086C-43BE-AF79-67ADECDA0A5B".Sha256())
    },
    AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
    AllowOfflineAccess = true,
    AccessTokenLifetime = accessTokenLifetime,
    AllowedScopes =
    {
        "api1"
    },
    AccessTokenType =AccessTokenType.Reference 
},
RedisPersistedGrantStore.cs

实现IPersistedGrantStore接口来支持redis

public class RedisPersistedGrantStore : IPersistedGrantStore
{
    private readonly ICacheClient _cacheClient;
    private readonly IConfiguration _configuration;

    public RedisPersistedGrantStore(ICacheClient cacheClient, IConfiguration configuration)
    {
        _cacheClient = cacheClient;
        _configuration = configuration;
    }

    public Task StoreAsync(PersistedGrant grant)
    {
        var accessTokenLifetime = double.Parse(_configuration.GetConnectionString("accessTokenLifetime"));
        var timeSpan = TimeSpan.FromSeconds(accessTokenLifetime);
        _cacheClient?.SetAsync(grant.Key, grant, timeSpan);
        return Task.CompletedTask;
    }

    public Task<PersistedGrant> GetAsync(string key)
    {
        if (_cacheClient.ExistsAsync(key).Result)
        {
            var ss = _cacheClient.GetAsync<PersistedGrant>(key).Result;
            return Task.FromResult<PersistedGrant>(_cacheClient.GetAsync<PersistedGrant>(key).Result.Value);
        }
        return Task.FromResult<PersistedGrant>((PersistedGrant)null);
    }

    public Task<IEnumerable<PersistedGrant>> GetAllAsync(string subjectId)
    {
        var persistedGrants = _cacheClient.GetAllAsync<PersistedGrant>().Result.Values;
        return Task.FromResult<IEnumerable<PersistedGrant>>(persistedGrants
            .Where(x => x.Value.SubjectId == subjectId).Select(x => x.Value));
    }

    public Task RemoveAsync(string key)
    {
        _cacheClient?.RemoveAsync(key);
        return Task.CompletedTask;
    }

    public Task RemoveAllAsync(string subjectId, string clientId)
    {
        _cacheClient.RemoveAllAsync();
        return Task.CompletedTask;
    }

    public Task RemoveAllAsync(string subjectId, string clientId, string type)
    {
        var persistedGrants = _cacheClient.GetAllAsync<PersistedGrant>().Result.Values
            .Where(x => x.Value.SubjectId == subjectId && x.Value.ClientId == clientId &&
                        x.Value.Type == type).Select(x => x.Value);
        foreach (var item in persistedGrants)
        {
            _cacheClient?.RemoveAsync(item.Key);
        }
        return Task.CompletedTask;
    }
}
ResourceOwnerPasswordValidator.cs

实现IResourceOwnerPasswordValidator接口实现自定义的用户验证逻辑

public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
    private readonly DiscoveryHttpClientHandler _handler;
    private const string UserApplicationName = "user";

    public ResourceOwnerPasswordValidator(IDiscoveryClient client)
    {
        _handler = new DiscoveryHttpClientHandler(client);
    }

    public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
    {
        //调用用户中心的验证用户名密码接口
        var client = new HttpClient(_handler);
        var url = $"http://{UserApplicationName}/search?name={context.UserName}&password={context.Password}";
        var result = await client.GetAsync(url);
        if (result.IsSuccessStatusCode)
        {
            var user = await result.Content.ReadAsObjectAsync<dynamic>();
            var claims = new List<Claim>() { new Claim("role", user.role.ToString()) };
            context.Result = new GrantValidationResult(user.id.ToString(), OidcConstants.AuthenticationMethods.Password, claims);
        }
        else
        {
            context.Result = new GrantValidationResult(null);
        }
    }
}

var claims = new List<Claim>() { new Claim("key", "value") };
这里可以传递自定义的用户信息,在客户端通过User.Claims.FirstOrDefault(x => x.Type == "key")来获取

这里需要注意一下,因为这里走的是http所以,授权服务中心和用户中心存在耦合,我个人建议如果走JWT的方式,用户中心和认证授权中心可以合并成一个服务,如果采用Reference的方式,建议还是拆分。

Startup.cs
public void ConfigureServices(IServiceCollection services)
{
    services.AddDiscoveryClient(Configuration);
    var redisconnectionString = Configuration.GetConnectionString("RedisConnectionString");
    var config = new Config(Configuration);
    services.AddMvc();
    services.AddIdentityServer({
                    x.IssuerUri = "http://identity";
                    x.PublicOrigin = "http://identity";
                })
        .AddDeveloperSigningCredential()
        .AddInMemoryPersistedGrants()
        .AddInMemoryApiResources(config.GetApiResources())
        .AddInMemoryClients(config.GetClients());
    services.AddSingleton(ConnectionMultiplexer.Connect(redisconnectionString));
    services.AddTransient<ICacheClient, RedisCacheClient>();//注入redis
    services.AddSingleton<IPersistedGrantStore, RedisPersistedGrantStore>();
    services.AddTransient<IResourceOwnerPasswordValidator, ResourceOwnerPasswordValidator>();
}
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }

    app.UseMvc();
    app.UseDiscoveryClient();
    app.UseIdentityServer();

}

因为是采用服务发现的方式,所以我们这里要修改IssuerUri和PublicOrigin。不要让发现服务暴露自己的具体URL地址,否则这里就负载不均衡了。

appsettings.json
"ConnectionStrings": {
        "RedisConnectionString": "localhost",
        "AccessTokenLifetime": 3600 //token过期时间 单位秒
    },
    "spring": {
        "application": {
            "name": "identity"
        }
    },
    "eureka": {
        "client": {
            "serviceUrl": "http://localhost:5000/eureka/"
        },
        "instance": {
          "port": 8010
        }
    }

用户中心

用户中心主要实现2个接口,一个给授权中心验证用户使用,还有一个是给客户端登录的时候返回token使用

nuget引用
<PackageReference Include="IdentityModel" Version="2.14.0" />
<PackageReference Include="Pivotal.Discovery.Client" Version="1.1.0" />
appsettings.json
{
  "spring": {
    "application": {
      "name": "user"
    }
  },
  "eureka": {
    "client": {
      "serviceUrl": "http://localhost:5000/eureka/"
    },
    "instance": {
      "port": 8040,
      "hostName": "localhost"
    }
  },
  "IdentityServer": {
    //jwt
    "ClientId": "client.jwt",
    "ClientSecrets": "AB2DC090-0125-4FB8-902A-34AFB64B7D9B"
    //reference
    //"ClientId": "client.reference",
    //"ClientSecrets": "A30E6E57-086C-43BE-AF79-67ADECDA0A5B"
  }
}  
TAG标签:
版权声明:本文由金沙澳门官网4166发布于文物考古,转载请注明出处:Api授权认证