聊聊 cookies,token
总结:小故事:很久很久以前,http 是无状态的协议,就是说一次请求你根本不知道是哪个客户端的,为了解决无状态搞出了 会话 这个名词,就是说要记住哪个客户端发的请求。
于是乎 浏览器 cookies 诞生了,浏览器每次请求都会自动带上 cookies 这里面存了一下信息,第一次访问 服务端时 服务端会产生一个 唯一 id 然后告诉浏览器,浏览器每次请求时 就会在同域名 下的信息 带过来,这样服务端就知道 是哪个在访问了,这就叫做会 话。session 就是折磨个 东东 ,存的就是 会话信息,比如你登入了用户信息
移动端流行起来,由于app 端 基本都是api 发请求,cookie是浏览器 自动带的,当然请求也可以带上,但是还是自定义token 比较多(登入生成token 然后每次请求带上)好处就是每个服务端都可以自己解析 可以跨平台 应用,回话保持就需要 自己实现了
token 常用方式 有 1.jwt (直接base编码 可以解码) 2.自定义token md5(用户名+时间戳)缓存 存 这个 用户的信息。3.对外的 OAuth 2.0 最安全的搞法,一般对外提供接口的基本都是这套 搞法,如第 3方登入
上述的东东 其实就是为了 保持回话。
我也就 简单 介绍一下下 token 中 JWT/Oauth2.0
JWT介绍
JSON Web Token(JWT)是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。来自 JWT RFC 7519 标准化的摘要说明:JSON Web Token 是一种紧凑的,URL 安全的方式,表示要在双方之间传输的声明。JWT 一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该 Token 也可直接被用于认证,也可被加密。
JWT 认证流程
客户端调用登录接口(或者获取 token 接口),传入用户名密码。
服务端请求身份认证中心,确认用户名密码正确。
服务端创建 JWT,返回给客户端。
客户端拿到 JWT,进行存储(可以存储在缓存中,也可以存储在数据库中,如果是浏览器,可以存储在 Cookie 中)在后续请求中,在 HTTP 请求头中加上 JWT。
服务端校验 JWT,校验通过后,返回相关资源和数据。
JWT 结构
JWT 是由三段信息构成的,第一段为头部(Header),第二段为载荷(Payload),第三段为签名(Signature)。每一段内容都是一个 JSON 对象,将每一段 JSON 对象采用 BASE64 编码,将编码后的内容用. 链接一起就构成了 JWT 字符串。如下:
header.payload.signature
1. 头部(Header)
头部用于描述关于该 JWT 的最基本的信息,例如其类型以及签名所用的算法等。这也可以被表示成一个 JSON 对象。
在头部指明了签名算法是 HS256 算法。
2. 载荷(payload)
载荷就是存放有效信息的地方。有效信息包含三个部分:
标准中注册的声明
公共的声明
私有的声明
标准中注册的声明(建议但不强制使用):
iss:JWT 签发者
sub:JWT 所面向的用户
aud:接收 JWT 的一方
exp:JWT 的过期时间,这个过期时间必须要大于签发时间
nbf:定义在什么时间之前,该 JWT 都是不可用的
iat:JWT 的签发时间
jti:JWT 的唯一身份标识,主要用来作为一次性 token, 从而回避重放攻击。
公共的声明 :
公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息. 但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明 :
私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为 base64 是对称解密的,意味着该部分信息可以归类为明文信息。
示例如下:
3. 签名(signature)
创建签名需要使用 Base64 编码后的 header 和 payload 以及一个秘钥。将 base64 加密后的 header 和 base64 加密后的 payload 使用. 连接组成的字符串,通过 header 中声明的加密方式进行加盐 secret 组合加密,然后就构成了 jwt 的第三部分。
比如:HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
JWT 的优点:
跨语言,JSON 的格式保证了跨语言的支撑
基于 Token,无状态
占用字节小,便于传输
关于 Token 注销:
Token 的注销,由于 Token 不存储在服务端,由客户端存储,当用户注销时,Token 的有效时间还没有到,还是有效的。所以如何在用户注销登录时让 Token 注销是一个要关注的点。一般有如下几种方式:
Token 存储在 Cookie 中,这样客户端注销时,自然可以清空掉
注销时,将 Token 存放到分布式缓存中,每次校验 Token 时区检查下该 Token 是否已注销。不过这样也就失去了快速校验 Token 的优点。
多采用短期令牌,比如令牌有效期是 20 分钟,这样可以一定程度上降低注销后 Token 可用性的风险。
四、OAuth 2.0 介绍
OAuth 的官网介绍:An open protocol to allow secure API authorization in a simple and standard method from desktop and web applications。OAuth 是一种开放的协议,为桌面程序或者基于 BS 的 web 应用提供了一种简单的,标准的方式去访问需要用户授权的 API 服务。OAUTH 认证授权具有以下特点:
简单:不管是 OAuth 服务提供者还是应用开发者,都很容易于理解与使用;
安全:没有涉及到用户密钥等信息,更安全更灵活;
开放:任何服务提供商都可以实现 OAuth,任何软件开发商都可以使用 OAuth;
OAuth 2.0 是 OAuth 协议的下一版本,但不向后兼容 OAuth 1.0,即完全废止了 OAuth 1.0。OAuth 2.0 关注客户端开发者的简易性。要么通过组织在资源拥有者和 HTTP 服务商之间的被批准的交互动作代表用户,要么允许第三方应用代表用户获得访问的权限。同时为 Web 应用,桌面应用和手机,和起居室设备提供专门的认证流程。2012 年 10 月,OAuth 2.0 协议正式发布为 RFC 6749。
授权流程
OAuth 2.0 的流程如下:
(A)用户打开客户端以后,客户端要求用户给予授权。(B)用户同意给予客户端授权。(C)客户端使用上一步获得的授权,向认证服务器申请令牌。(D)认证服务器对客户端进行认证以后,确认无误,同意发放令牌。(E)客户端使用令牌,向资源服务器申请获取资源。(F)资源服务器确认令牌无误,同意向客户端开放资源。
四大角色
由授权流程图中可以看到 OAuth 2.0 有四个角色:客户端、资源拥有者、资源服务器、授权服务器。
客户端:客户端是代表资源所有者对资源服务器发出访问受保护资源请求的应用程序。
资源拥有者:资源拥有者是对资源具有授权能力的人。
资源服务器:资源所在的服务器。
授权服务器:为客户端应用程序提供不同的 Token,可以和资源服务器在统一服务器上,也可以独立出去。
客户端的授权模式
客户端必须得到用户的授权(Authorization Grant),才能获得令牌(access token)。OAuth 2.0 定义了四种授权方式:authorizationcode、implicit、resource owner password credentials、client credentials。
1. 授权码模式(authorization code)
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。流程如下:
用户访问客户端,后者将前者导向认证服务器。
用户选择是否给予客户端授权。
假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向 URI"(redirection URI),同时附上一个授权码。
客户端收到授权码,附上早先的"重定向 URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
认证服务器核对了授权码和重定向 URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
2. 简化模式(implicit)
简化模式(Implicit Grant Type)不通过第三方应用程序的服务器,直接在浏览器中向认证服务器申请令牌,跳过了"授权码"这个步骤,因此得名。所有步骤在浏览器中完成,令牌对访问者是可见的,且客户端不需要认证。流程如下:
客户端将用户导向认证服务器。
用户决定是否给于客户端授权。
假设用户给予授权,认证服务器将用户导向客户端指定的"重定向 URI",并在 URI 的 Hash 部分包含了访问令牌。
浏览器向资源服务器发出请求,其中不包括上一步收到的 Hash 值。
资源服务器返回一个网页,其中包含的代码可以获取 Hash 值中的令牌。
浏览器执行上一步获得的脚本,提取出令牌。
浏览器将令牌发给客户端。
3. 密码模式(Resource Owner Password Credentials)
密码模式中,用户向客户端提供自己的用户名和密码。客户端使用这些信息,向"服务商提供商"索要授权。在这种模式中,用户必须把自己的密码给客户端,但是客户端不得储存密码。这通常用在用户对客户端高度信任的情况下,比如客户端是操作系统的一部分,或者由一个著名公司出品。而认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。流程如下:
用户向客户端提供用户名和密码。
客户端将用户名和密码发给认证服务器,向后者请求令牌。
认证服务器确认无误后,向客户端提供访问令牌。
4. 客户端模式(Client Credentials)
客户端模式(Client Credentials Grant)指客户端以自己的名义,而不是以用户的名义,向"服务提供商"进行认证。严格地说,客户端模式并不属于 OAuth 框架所要解决的问题。
在这种模式中,用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。流程如下:
客户端向认证服务器进行身份认证,并要求一个访问令牌。
认证服务器确认无误后,向客户端提供访问令牌。
五、思考总结
正如 David Borsos 所建议的一种方案,在微服务架构下,我们更倾向于将 Oauth 和 JWT 结合使用,Oauth 一般用于第三方接入的场景,管理对外的权限,所以比较适合和 API 网关结合,针对于外部的访问进行鉴权(当然,底层 Token 标准采用 JWT 也是可以的)。
JWT 更加轻巧,在微服务之间进行访问鉴权已然足够,并且可以避免在流转过程中和身份认证服务打交道。当然,从能力实现角度来说,类似于分布式 Session 在很多场景下也是完全能满足需求,具体怎么去选择鉴权方案,还是要结合实际的需求来。
参考:
https://mp.weixin.qq.com/s/GIL606vsUXbyeQySTomLCA
jwt 小例子:
JWT+Interceptor实现无状态登录和鉴权
无状态登录原理#
先提一下啥是有状态登录#
单台tomcat的情况下:编码的流程如下
- 前端提交表单里用户的输入的账号密码
- 后台接受,查数据库,
- 在数据库中找到用户的信息后,把用户的信息存放到session里面,返回给用户cookie
- 以后用户的请求都会自动携带着cookie去访问后台,后台根据用户的cookie辨识用户的身份
但是有缺点
- 如果有千千万的用户都往session里面存放信息,
- session很难跨服务器,也就是说,用户每次请求,都必须访问同一台tomcat,新的tomcat不认识用户的cookie
无状态登录#
- 对于服务端,不再保存任何用户的信息,对于他们来说,所有的用户地位平等
- 对于用户,他们需要自己携带着信息去访问服务端,携带的信息可以被所有服务端辨认,所以,对于用户,所有的服务地位平等
具体如何实现呢?
- 用户登录,服务端返回给用户的个人信息+token令牌
- 前端为了自己使用方便,将用户的个人信息缓存进浏览器(因为后端只是给了他一个token)
- 用户携带自己的token去访问服务端
- 认证通过,把用户请求的数据返回给客户端
- 以后不论用户请求哪个微服务,都携带着token
- 微服务对token进行解密,判断他是否有效
JWT(Json Web Token)生成规则#
整个登录.授权.鉴权的过程token的安全性至关重要,而JWT就是一门有关于如何生成一个不可仿造的token的规范
他是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权,而且它不是技术,和语言无关,java有对这个规范的实现 叫做 jjwt -- 点击进入jjwt的github项目
由JWT算法得到的token格式如上图,由头部,载荷,签名三部分组成
-
头部
由两部分组成,alg表示使用的加密算法 typ表示使用的token的类型 - 载荷,存放用户相关的信息
// 下面是它已经定义好的载荷部分key,也允许我们自定义载荷部分key
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
- 第三步分的签名是由 头+载荷+盐 三部分加密组成
从图可以看出,头部和载荷被串改后,生成的编码会发生改变,因此保证了token的安全 ,还有,载荷部分可解密,因此不要往里面放入敏感的信息(比如密码 )
只要密钥不泄露,别人就无法伪造任何信息
jwt的交互过程
RSA非对称加密算算法#
由美国麻 省理工 学院三 位学者 Riv est、Sh amir 及Adleman 研 究发 展出 一套 可实 际使用 的公 开金 钥密 码系 统,那 就是
RSA(Rivest-Shamir-Adleman)密码系统
jwt(是一种非对称加密算法) JWT不一定非要搭配RSA算法,但是拥有RSA算法公钥私钥的特性,会使我们的业务逻辑变的简单,做到校验变少
- 对称加密,如AES(Advanced Encryption Standard) 高级加密标准
- 基本原理:将明文分成N个组,然后使用密钥对各个组进行加密,形成各自的密文,最后把所有的分组密文进行合并,形成最终的密文。
- 优势:算法公开、计算量小、加密速度快、加密效率高
- 缺陷:双方都使用同样密钥,安全性得不到保证
- 非对称加密,如RSA
- 基本原理:同时生成两把密钥:私钥和公钥,私钥隐秘保存,公钥可以下发给信任客户端
- 私钥加密,持有私钥或公钥才可以解密
- 公钥加密,持有私钥才可解密
- 优点:安全,难以破解
- 缺点:算法比较耗时
- 不可逆加密,如MD5,SHA
使用JJWT实现JWT#
JJWT(java json web token)
坐标
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
创建jwt令牌#
token最终会颁发给前端,这时候就得去和前端的哥们商量它想要什么信息
// 生成令牌,主要是用它生成载荷
JwtBuilder builder = Jwts.builder()
// 设置头部,使用hs256加密, + key,也就是盐
.signWith(SignatureAlgorithm.HS256,"changwu")
// 添加载荷
.setId("666") // 用户id
.setSubject("张三") // 用户名
.setExpiration(new Date(new Date().getTime()+60*1000)) // 过期时间
.setIssuedAt(new Date())// 登录时间
// 添加自定义的键值对
.claim("role","admin");
System.out.println(builder.compact());
经过它处理的token长这个样子, 三部组成
XXX.YYY.ZZZ
解析token#
能成功解析出结果的前提是两次的盐是一样的才行
Claims map = Jwts.parser().setSigningKey("changwu")
.parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiI2NjYiLCJzdWIiOiLlvKDkuIkiLCJleHAiOjE1NjU2MTg1MjUsImlhdCI6MTU2NTYxODQ2NSwicm9sZSI6ImFkbWluIn0.GDVfLq-ehSnMCRoxVcziXkirjOg34SUUPBK5vAEHu80")
.getBody();
System.out.println("用户id" + map.getId());
System.out.println("用户名" + map.getSubject());
System.out.println("token过期时间" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(map.getExpiration()));
System.out.println("用户登录时间" + new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(map.getIssuedAt()));
System.out.println("用户的角色是:"+map.get("role"));
拦截器#
注意哦,使用的是SpringMvc的拦截器,而不是Servlet的过滤器
拦截器的体系架构#
在拦截器的体系中,我们常用的是上面的来两个
HandlerInterceptor: 是顶级接口如下:
虽然是接口, 但是拥有jdk8的特性,是默认的方法,所以允许我们挑选它的部分方法实现而不会报错
prehandler: 请求到达控制器之间被回调,可以在这里进行设置编码,安全控制,权限校验, 一般全部返回ture,表示放行
postHandler: 控制器处理请求之后生成了ModelAndView,但是未进行渲染,提供了修改ModelAndView的机会
afterCompletion: 返回给用户ModelAndView之后执行, 用于收尾工作
第二个是HandlerInterceptorAdapter如下图
这个适配器方法全是空实现,同样可以满足我们的需求,但是它同时实现了AsyncHandlerInterceptor,拥有了一个新的方法,afterConcrruentHandingStarted(request,response,handler)
这个方法会在Controller方法异步执行时开始执行, 而Interceptor的postHandle方法则是需要等到Controller的异步执行完才能执行
编码实现#
其实到这里改如何做已经清晰明了
用户登录,授权#
授权的很简单
- 用户发送登录请求提交form表单
- 后端根据用户名密码查询用户的信息
- 把用户的信息封装进jwt的载荷部分
- 返回给前端token
用户再次请求,鉴权#
后台会有很多方法需要指定权限的人才能访问, 所谓鉴定权限,其实就是把前端放在请求头中的token信息解析出来,如果解析成功了,说明用户的合法的,否则就提示前端用户没有权限
把token从请求头中解析出来的过程,其实是在大量的重复性工作,所以我们放在拦截器中实现
使用拦截器两步走
第一步,继承HandlerInterAdapter,选择重写它的方法
- 设计的逻辑,这个方法肯定要返回true, 因为后台的方法中肯定存在大量的不需要任何权限就能访问的方法
- 所以这个方法的作用就是,解析出请求头中的用户的权限信息,重新放回到request中,
- 这样每个需要进行权限验证的请求,就不需要再进行解析请求头,而是直接使用当前回调方法的处理结果
@Component
public class RequestInterceptor extends HandlerInterceptorAdapter {
@Autowired
JwtUtil jwtUtil;
// 在请求进入处理器之前回调这个方法
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取请求头
String header = request.getHeader("Authorization");
// 请求头不为空进行解析
if (StringUtils.isNotBlank(header)) {
// 按照我们和前端约定的格式进行处理
if (header.startsWith("Bearer ")){
// 得到令牌
String token = header.substring(7);
// 验证令牌
try{ // 令牌的解析这里一定的try起来,因为它解析错误的令牌时,会报错
// 当然你也可以在自定义的jwtUtil中把异常 try起来,这里就不用写了
Claims claims = jwtUtil.parseJWT(token);
String roles =(String) claims.get("roles");
System.err.println("roles=="+roles);
if (roles!=null&&"admin".equals(roles)){
request.setAttribute("role_admin",token);
}
if (roles!=null&&"user".equals(roles)){
request.setAttribute("role_user",token);
}
}catch (Exception e){
throw new RuntimeException("令牌不存在");
}
}
}
return true;
}
}
这样 控制器中的方法需要进行权限验证时,就免去了解析的麻烦,直接从request中获取即可
第二步,将拦截器注册进容器
@Configuration
public class InterceptorConfig extends WebMvcConfigurationSupport {
@Autowired
RequestInterceptor requestInterceptor;
// 注册拦截器
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptor)
.addPathPatterns("/**")
.excludePathPatterns("/user/login/**");
}
}