OAuth2.0 Owin 授权问题
http://www.cnblogs.com/dudu/p/4569857.html
OAuth2.0
一、什么是OAuth
OAuth是一个关于授权(Authorization)的开放网络标准,目前的版本是2.2版。
注意是Authorization(授权),而不是Authentication(认证)。
用来做Authentication(认证)的标准叫做openid connect。
二、名词定义
理解OAuth中的专业术语能够帮助你理解其流程模式,OAuth中常用的名词术语有4个,为了便于理解这些术语,我们先假设一个很常见的授权场景:
你访问了一个日志网站(third party application),你(client)觉得这个网站很不错,准备以后就要在这个网站上写日志了,所以你准备把QQ空间(Resource owner)里面的日志都导入进来。此日志网站想要导入你在QQ空间中的日志需要知道你的QQ用户名和密码才行,为了安全期间你不会把你的QQ用户名和密码直接输入在日志网站中,所以日志网站帮你导航到了QQ认证界面(Authorization Server),当你输入完用户名和密码后,QQ认证服务器返回给日志网站一个token, 该日志网站凭借此token来访问你在QQ空间中的日志。
- third party application 第三方的应用,想要的到Resource owner的授权
- client 代表用户
- Resource owner 资源拥有者,在这里代表QQ
- Authorization server 认证服务,这里代表QQ认证服务,Resource owner和Authorization server可以是不同的服务器,也可以是同一个服务器。
三、OAuth2.0 中的四种模式
OAuth定义了四种模式,覆盖了所有的授权应用场景:
- 授权码模式(authorization code)
- 简化模式(implicit)
- 密码模式(resource owner password credentials)
- 客户端模式(client credentials)
前面我们假设的场景可以用前两种模式来实现,不同之处在于:
当日志网站(third party application)有服务端,使用模式1;
当日志网站(third party application)没有服务端,例如纯的js+html页面需要采用模式2;
本文主描述利用OAuth2.0实现自己的WebApi认证服务,前两种模式使用场景不符合我们的需求。
四、选择合适的OAuth模式打造自己的webApi认证服务
场景:你自己实现了一套webApi,想供自己的客户端调用,又想做认证。
这种场景下你应该选择模式3或者4,特别是当你的的客户端是js+html应该选择3,当你的客户端是移动端(ios应用之类)可以选择3,也可以选择4。
密码模式(resource owner password credentials)的流程:
这种模式的流程非常简单:
- 用户向客户端(third party application)提供用户名和密码。
- 客户端将用户名和密码发给认证服务器(Authorization server),向后者请求令牌(token)。
- 认证服务器确认无误后,向客户端提供访问令牌。
- 客户端持令牌(token)访问资源。
此时third party application代表我们自己的客户端,Authorization server和Resource owner代表我们自己的webApi服务。我们在日志网站的场景中提到:用户不能直接为日志网站(third party application)提供QQ(resource owner)的用户名和密码。而此时third party application、authorization server、resource owner都是一家人,Resource owner对third party application足够信任,所以我们才能采取这种模式来实现。
五、使用owin来实现密码模式
owin集成了OAuth2.0的实现,所以在webapi中使用owin来打造authorization无疑是最简单最方便的方案。
- 新建webApi项目
- 安装Nuget package:
Microsoft.AspNet.WebApi.Owin
Microsoft.Owin.Host.SystemWeb
- 增加owin的入口类:Startup.cs
在项目中新建一个类,命名为Startup.cs,这个类将作为owin的启动入口,添加下面的代码
using Microsoft.AspNet.Identity.EntityFramework;
using Microsoft.Owin;
using Microsoft.Owin.Security;
using Microsoft.Owin.Security.Infrastructure;
using Microsoft.Owin.Security.OAuth;
using Owin;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security.Claims;
using System.Threading.Tasks;
using System.Web;
using System.Web.Http;
using ADT.TuDou.API.Auth; [assembly: OwinStartup(typeof(ADT.TuDou.API.Startup))]
namespace ADT.TuDou.API
{
public class Startup
{
//ConfigureOAuth(IAppBuilder app)方法开启了OAuth服务。简单说一下OAuthAuthorizationServerOptions中各参数的含义:
//AllowInsecureHttp:允许客户端使用http协议请求;
//TokenEndpointPath:token请求的地址,即http://localhost:端口号/token;
//AccessTokenExpireTimeSpan :token过期时间; //注册用户 http://localhost:33590/api/account/register)
//content-type: application/json
//{"UserName":"jay","Password":"xsj1989","ConfirmPassword":"xsj1989"} //获取token方法
//获取token的地址:http://localhost:33590/token
//提交方式:POST
//参数:grant_type=password&username=jay&password=xsj1989
//内容类型:content-type: application/x-www-form-urlencoded
//返回数据:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"} //使用token访问受保护的接口
//http://localhost:33590/api/orders
//User-Agent: Fiddler
//Host: localhost:33590
//Content-Length: 51
//content-type: application/x-www-form-urlencoded
//Authorization: Bearer UcaIhVTpveBVlAVEhwp1iELcd2jMhHLzRdKKcTmja1Ii5520PQ6fr56kt1mN_7O92WBrkg0AkR45i1BvPmiuIAtVCk-aJsvKd7w0uaBJIGnQBycjR3WyW-plFtlGrErtEbxTOjLe4QpLgn6ofTB61wK4MS7RR91skVEhIUt4NPY0gYKn_EqE1ihPoOMuYAIciQUmQH9aKDyo3tYFjDrhtRGS6SfSBoWFdRaIPEOtQvFG4KMnbCO1XymYAsDS0vDnUZ_BgcQAYC_PbYfNRCTGfAkwDc4hidiotwde0---nPpUt2YXbFfI8oWQ48Jgi_Fk //1.刷新Token,再次请求:http://localhost:33590/token
//返回数据:
//{"access_token":"vZPcwG6szzTaybqPWsS7ESlxMKZa7kYDHTwwbXzyEes8-9kwPTCUaf3a8vbq8qp99l265jwzqXMDzMmWa89kmCPHMI-82OYgZVhw86qtbYUGTvFEwHEyysmGD9MAH524CBbaDsCJSl1sg-VaBIC8wgl1pAHZTh56__iHj3ASUhTvphT68GpU6TyhVhnIuBTonkVoE7vjpNjIdzvwGFshHGrJKS84iZYTKlM9Kv7AItrDsRon-QwfgQOmCZ2ceCvUH8sGI5O2CYJVYhH8p7TRGLgBa4p0LYnbMC54xFyO8ZmjXr7vpG0LVTsyd0q-c6pd","token_type":"bearer","expires_in":1799,"refresh_token":"d5fb57fcc7084d128ad70e5e65643045","as:client_id":"","userName":"jay",".issued":"Fri, 18 Mar 2016 02:28:21 GMT",".expires":"Fri, 18 Mar 2016 02:58:21 GMT"}
//包含数据:"refresh_token":"d5fb57fcc7084d128ad70e5e65643045"
//当token过期后,凭借上次得到的refresh_token重新获取token //2.再次请求:http://localhost:33590/token
//参数:grant_type=refresh_token&refresh_token=d5fb57fcc7084d128ad70e5e65643045
//报错:invalid_grant 暂时没解决 //Provider :提供具体的认证策略;
public void Configuration(IAppBuilder app)
{
var config = new HttpConfiguration();
WebApiConfig.Register(config); ConfigureOAuth(app); //这一行代码必须放在ConfiureOAuth(app)之后
app.UseWebApi(config);
} public void ConfigureOAuth(IAppBuilder app)
{
OAuthAuthorizationServerOptions OAuthServerOptions = new OAuthAuthorizationServerOptions()
{
AllowInsecureHttp = true,
TokenEndpointPath = new PathString("/token"),
AccessTokenExpireTimeSpan = TimeSpan.FromMinutes(),
Provider = new SimpleAuthorizationServerProvider(), //refresh token provider
RefreshTokenProvider = new SimpleRefreshTokenProvider()
}; // Token Generation
app.UseOAuthAuthorizationServer(OAuthServerOptions);
app.UseOAuthBearerAuthentication(new OAuthBearerAuthenticationOptions()); //ConfigureOAuth(IAppBuilder app)方法开启了OAuth服务。简单说一下OAuthAuthorizationServerOptions中各参数的含义:
//AllowInsecureHttp:允许客户端使用http协议请求;
//TokenEndpointPath:token请求的地址,即http://localhost:端口号/token;
//AccessTokenExpireTimeSpan :token过期时间;
//Provider :提供具体的认证策略;
}
} public class SimpleAuthorizationServerProvider : OAuthAuthorizationServerProvider
{
public override Task ValidateClientAuthentication(OAuthValidateClientAuthenticationContext context)
{
context.Validated();
return Task.FromResult<object>(null);
} public override async Task GrantResourceOwnerCredentials(OAuthGrantResourceOwnerCredentialsContext context)
{
using (AuthRepository _repo = new AuthRepository())
{
IdentityUser user = await _repo.FindUser(context.UserName, context.Password); if (user == null)
{
context.SetError("invalid_grant", "The user name or password is incorrect.");
return;
}
} var identity = new ClaimsIdentity(context.Options.AuthenticationType);
identity.AddClaim(new Claim(ClaimTypes.Name, context.UserName));
identity.AddClaim(new Claim(ClaimTypes.Role, "user"));
identity.AddClaim(new Claim("sub", context.UserName)); var props = new AuthenticationProperties(new Dictionary<string, string>
{
{
"as:client_id", context.ClientId ?? string.Empty
},
{
"userName", context.UserName
}
}); var ticket = new AuthenticationTicket(identity, props);
context.Validated(ticket);
} public override Task TokenEndpoint(OAuthTokenEndpointContext context)
{
foreach (KeyValuePair<string, string> property in context.Properties.Dictionary)
{
context.AdditionalResponseParameters.Add(property.Key, property.Value);
} return Task.FromResult<object>(null);
}
} public class SimpleRefreshTokenProvider : IAuthenticationTokenProvider
{
public async Task CreateAsync(AuthenticationTokenCreateContext context)
{
var refreshTokenId = Guid.NewGuid().ToString("n"); using (AuthRepository _repo = new AuthRepository())
{
var token = new RefreshToken()
{
Id = refreshTokenId.GetHashCode(),
Subject = context.Ticket.Identity.Name,
IssuedUtc = DateTime.UtcNow,
ExpiresUtc = DateTime.UtcNow.AddMinutes()
}; context.Ticket.Properties.IssuedUtc = token.IssuedUtc;
context.Ticket.Properties.ExpiresUtc = token.ExpiresUtc; token.ProtectedTicket = context.SerializeTicket(); var result = await _repo.AddRefreshToken(token); if (result)
{
context.SetToken(refreshTokenId);
} }
} public async Task ReceiveAsync(AuthenticationTokenReceiveContext context)
{ int hashedTokenId = context.Token.GetHashCode(); using (AuthRepository _repo = new AuthRepository())
{
var refreshToken = await _repo.FindRefreshToken(hashedTokenId); if (refreshToken != null)
{
//Get protectedTicket from refreshToken class
context.DeserializeTicket(refreshToken.ProtectedTicket);
var result = await _repo.RemoveRefreshToken(hashedTokenId);
}
}
} public void Create(AuthenticationTokenCreateContext context)
{
throw new NotImplementedException();
} public void Receive(AuthenticationTokenReceiveContext context)
{
throw new NotImplementedException();
} } public class RefreshToken
{
public int Id { get; set; }
public string Subject { get; set; }
public DateTime IssuedUtc { get; set; }
public DateTime ExpiresUtc { get; set; }
public string ProtectedTicket { get; set; }
};
}
另外修改WebApiConfig.Register(HttpConfiguration config)方法:
public static void Register(HttpConfiguration config)
{
config.MapHttpAttributeRoutes(); //config.Routes.MapHttpRoute(
// name: "DefaultApi",
// routeTemplate: "api/{controller}/{id}",
// defaults: new { id = RouteParameter.Optional }
//);
config.Routes.MapHttpRoute(
name: "DefaultApi",
routeTemplate: "{controller}/{action}/{id}",
defaults: new { id = RouteParameter.Optional }
); //DO:将会使用CamelCase命名法序列化webApi的返回结果
var jsonFormatter = config.Formatters.OfType<JsonMediaTypeFormatter>().First();
jsonFormatter.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
}
最后两句话将会使用CamelCase命名法序列化webApi的返回结果。
3.使用ASP.NET Identity 实现一个简单的用户认证功能,以便我们生成用户名和密码
安装nuget package:
Microsoft.AspNet.Identity.Owin
Microsoft.AspNet.Identity.EntityFramework
4.新建一个Auth的文件夹,并添加类:
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Linq;
using System.Web; namespace ADT.TuDou.API.Auth
{
public class AuthContext : IdentityDbContext<IdentityUser>
{
public AuthContext()
: base("AuthContext")
{ }
public DbSet<RefreshToken> RefreshTokens { get; set; }
}
}
using Microsoft.AspNet.Identity;
using Microsoft.AspNet.Identity.EntityFramework;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Web;
using ADT.TuDou.API.Entities; namespace ADT.TuDou.API.Auth
{
public class AuthRepository : IDisposable
{
private AuthContext _ctx; private UserManager<IdentityUser> _userManager; public AuthRepository()
{
_ctx = new AuthContext();
_userManager = new UserManager<IdentityUser>(new UserStore<IdentityUser>(_ctx));
} //注册用户
public async Task<IdentityResult> RegisterUser(UserModel userModel)
{
IdentityUser user = new IdentityUser
{
UserName = userModel.UserName
}; var result = await _userManager.CreateAsync(user, userModel.Password); return result;
} //查询用户
public async Task<IdentityUser> FindUser(string userName, string password)
{
IdentityUser user = await _userManager.FindAsync(userName, password); return user;
} public async Task<bool> AddRefreshToken(RefreshToken token)
{ var existingToken = _ctx.RefreshTokens.SingleOrDefault(r => r.Subject == token.Subject); if (existingToken != null)
{
var result = await RemoveRefreshToken(existingToken);
} _ctx.RefreshTokens.Add(token); return await _ctx.SaveChangesAsync() > ;
} public async Task<bool> RemoveRefreshToken(int refreshTokenId)
{
var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); if (refreshToken != null)
{
_ctx.RefreshTokens.Remove(refreshToken);
return await _ctx.SaveChangesAsync() > ;
} return false;
} public async Task<bool> RemoveRefreshToken(RefreshToken refreshToken)
{
_ctx.RefreshTokens.Remove(refreshToken);
return await _ctx.SaveChangesAsync() > ;
} public async Task<RefreshToken> FindRefreshToken(int refreshTokenId)
{
var refreshToken = await _ctx.RefreshTokens.FindAsync(refreshTokenId); return refreshToken;
} public void Dispose()
{
_ctx.Dispose();
_userManager.Dispose(); }
}
}
同时在web.config中添加connectionString:
<connectionStrings>
<add name="AuthContext" connectionString="Data Source=.;Initial Catalog=OAuthPractice;Integrated Security=SSPI;" providerName="System.Data.SqlClient" />
</connectionStrings>
5.增加一个Entities文件夹并添加UserModel类:
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web; namespace ADT.TuDou.API.Entities
{
public class UserModel
{
[Required]
[Display(Name = "UserModel name")]
public string UserName { get; set; } [Required]
[StringLength(100, ErrorMessage = "The {0} must be at least {2} characters long.", MinimumLength = 6)]
[DataType(DataType.Password)]
[Display(Name = "Password")]
public string Password { get; set; } [DataType(DataType.Password)]
[Display(Name = "Confirm password")]
[Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
public string ConfirmPassword { get; set; }
}
}
6、增加TestController:ApiController
/// <summary>
/// 依据用户ID,获得头像
/// </summary>
/// <returns></returns>
[AllowAnonymous]
[AcceptVerbs("GET")]
public HttpResponseMessage GetHeadImg(int UserID)
{
//.........
}
方法上加了AllowAnonymous标签,意味着调用这个api无需任何授权
方法上加了Authorize标签,则此api在没有授权的情况下将返回401 Unauthorize。
7、类 SimpleAuthorizationServerProvider 说明:
ValidateClientAuthentication方法用来对third party application 认证,具体的做法是为third party application颁发appKey和appSecrect,在本例中我们省略了颁发appKey和appSecrect的环节,我们认为所有的third party application都是合法的,context.Validated(); 表示所有允许此third party application请求。
GrantResourceOwnerCredentials方法则是resource owner password credentials模式的重点,由于客户端发送了用户的用户名和密码,所以我们在这里验证用户名和密码是否正确,后面的代码采用了ClaimsIdentity认证方式,其实我们可以把他当作一个NameValueCollection看待。最后context.Validated(ticket); 表明认证通过。
只有这两个方法同时认证通过才会颁发token。
TokenEndpoint方法将会把Context中的属性加入到token中。
8、向服务器请求token
resource owner password credentials模式需要body包含3个参数:
grant_type-必须为password
username-用户名
password-用户密码
//获取token方法
//获取token的地址:http://localhost:33590/token
//提交方式:POST
//参数:grant_type=password&username=jay&password=xsj1989
//内容类型:content-type: application/x-www-form-urlencoded
//返回数据:{"access_token":"JznxS2sYbU9fItS-ihnHB6kPnqzFd-C8uZGargqv2TF6mrNhvZYLY4OG1VclTh2PYkqLgeGzZQrnicf633coKEiJsHQCVZQTMHwli1uNQ3fJF2t0ab3CIO7Kj8y2ZvCS5ypOLAOuKkpkP1oAgCJHMkVOMZRbPfj1tqijPSt1EKcRPfzZMcOo0-OxYmbqjBrkHoB-18ZApYy4kyG6g7cHX-kh3Fq4TEAFeShfk5lOn7NKJxUJf9RWs---tCWwcqWVI-XwA3am0G8KW95-OEDq6d1gr2qHxeK020bhbvQ-OWSiR8MEq617wi-jWqdngdl_","token_type":"bearer","expires_in":1799,"as:client_id":"","userName":"jay",".issued":"Thu, 17 Mar 2016 06:19:04 GMT",".expires":"Thu, 17 Mar 2016 06:49:04 GMT"}
实践中的问题:
异常:安全透明方法“System.Web.Http.GlobalConfiguration.get_Configuration()”尝试访问安全关键类型“System.Web.Http.HttpConfiguration”失败
解决:Nuget 安装 Microsoft.AspNet.WebApi -IncludePrerelease 包,更新为最新的包。
参考:
http://www.cnblogs.com/richieyang/p/4918819.html?utm_source=tuicool&utm_medium=referral#undefined
http://oauth.net/code/
http://www.cnblogs.com/n-pei/archive/2012/05/29/2524673.html
https://github.com/feiyit/MvcApiSecurity
OAuth2.0 Owin 授权问题的更多相关文章
- Owin中间件搭建OAuth2.0认证授权服务体会
继两篇转载的Owin搭建OAuth 2.0的文章,使用Owin中间件搭建OAuth2.0认证授权服务器和理解OAuth 2.0之后,我想把最近整理的资料做一下总结. 前两篇主要是介绍概念和一个基本的D ...
- 使用Owin中间件搭建OAuth2.0认证授权服务器
前言 这里主要总结下本人最近半个月关于搭建OAuth2.0服务器工作的经验.至于为何需要OAuth2.0.为何是Owin.什么是Owin等问题,不再赘述.我假定读者是使用Asp.Net,并需要搭建OA ...
- 微信OAuth2.0网页授权
1.OAuth2.0网页授权 关于网页授权的两种scope的区别说明 1.以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页 ...
- 微信公众平台开发(71)OAuth2.0网页授权
微信公众平台开发 OAuth2.0网页授权认证 网页授权获取用户基本信息 作者:方倍工作室 微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使 ...
- Force.com微信开发系列(七)OAuth2.0网页授权
OAuth是一个开放协议,允许用户让第三方应用以安全且标准的方式获取该用户在某一网站上存储的私密资源(如用户个人信息.照片.视频.联系人列表),而无须将用户名和密码提供给第三方应用.本文将详细介绍OA ...
- 解决微信OAuth2.0网页授权回调域名只能设置一个的问题
https://github.com/HADB/GetWeixinCode GetWeixinCode 解决微信OAuth2.0网页授权回调域名只能设置一个的问题 使用方法 部署get-weixin- ...
- ***微信公众平台开发: 获取用户基本信息+OAuth2.0网页授权
本文介绍如何获得微信公众平台关注用户的基本信息,包括昵称.头像.性别.国家.省份.城市.语言.本文的方法将囊括订阅号和服务号以及自定义菜单各种场景,无论是否有高级接口权限,都有办法来获得用户基本信息, ...
- 微信公众平台开发-OAuth2.0网页授权(含源码)
微信公众平台开发-OAuth2.0网页授权接口.网页授权接口详解(含源码)作者: 孟祥磊-<微信公众平台开发实例教程> 在微信开发的高级应用中,几乎都会使用到该接口,因为通过该接口,可以获 ...
- C#-MVC开发微信应用(2)--OAuth2.0网页授权
微信公众平台最近新推出微信认证,认证后可以获得高级接口权限,其中一个是OAuth2.0网页授权,很多朋友在使用这个的时候失败了或者无法理解其内容,希望我出个教程详细讲解一下,于是便有了这篇文章. 一. ...
随机推荐
- Lunce编程模型
问题的场景: 解决方案:都是来自于科技论文 ============================================================================== ...
- python之路之装饰器
一 装饰器进化之路1) import time def index(): start_time=time.time() time.sleep() print('welcome to index wor ...
- 属性,类方法@classmethod
# 属性的初识# class Person:## def __init__(self,name,hight,weight):# self.name = name# self.__hight = hig ...
- 修改oracle数据库默认时间格式
原创作品,允许转载,转载时请务必以超链接形式标明文章 原始出处 .作者信息和本声明.否则将追究法律责任.http://ccchencheng.blog.51cto.com/2419062/929695 ...
- 微信小程序教程系列
微信小程序教程系列 来源: https://blog.csdn.net/michael_ouyang/article/details/56846185 相关连接:http://blog.c ...
- delphi RAD XE 安装路径 重装备份
重装的时候,不要删除c盘C:\ProgramData下的guid目录.以便完整卸载旧版本. 控件安装的生成的目标文件路径 C:\Users\Public\Documents\Embarcadero\S ...
- 反射(hasattr , getattr, setattr) 输入的字符串用来运行程序
当用户输入字符串时,不能够用来运行程序 1.使用 hasattr 找出输入的字符串是否在程序内 2.使用 getattr 返回找出字符串对应的函数的内存地址或者变量 3. 使用setattr 添加新的 ...
- box2d 易错
1.动态刚体与一个与静态刚体重叠的小的感应刚体在contactBegin时,有些时候无法侦测到 2.bodyA.GetContactList()应用非动态刚体,会找不到另一非动态刚体的接触,非动态刚体 ...
- redmine邮件配置
网上找了半天,有很多答案,最后自己测试找出一个解决办法. 1.找到安装位置 D:\Bitnami\redmine-2.5.2-2\apps\redmine\htdocs\config下的文件confi ...
- 漫谈四种神经网络序列解码模型【附示例代码】 glimpse attention
漫谈四种神经网络序列解码模型[附示例代码] http://jacoxu.com/encoder_decoder/ [视觉注意力的循环神经网络模型]http://blog.csdn.net/leo_xu ...