一切的根源就是因为 HTTP 是一个无状态的协议。
HTTP 是一个无状态的协议
什么是无状态呢?就是说这一次请求和上一次请求是没有任何关系的,互不认识的,没有关联的。
看过电影《夏洛特烦恼》的小伙伴肯定记得夏洛和大爷的一段对话:
夏洛:大爷,楼上322住的是马冬梅家吗? 大爷:马冬什么? 夏洛:马冬梅。 大爷:什么冬梅啊? 夏洛:马冬梅啊。 大爷:马什么梅啊?
夏洛:……行,大爷,您先凉快吧。
「打岔」大爷记不住马冬梅,就如同 HTTP 协议记不住上一次请求。说得通俗一点:HTTP 的记忆力不太好,需要借助『助听器』。
记忆力不好的优点就是一个字「快」;缺点也很明显,需要借靠 cookie、session 、token等机制将客户端多次请求关联起来。
想象一下如果没有 cookie、session、token 这样的机制,我们在网站上每次点击都需要重新输入密码认证,这样槽糕的体验你还愿意继续用吗?
在讲解cookie、session 、token前我们先简单讲解两个概念:认证、授权。
什么是认证?
认证(Authentication)简单来讲就是验证当前者的身份,证明你是你自己。
最常见的认证方式是通过用户名和密码,除此之外还有:
通过用户手机:手机短信、手机二维码扫描、手势密码
用户的电子邮箱
身份证号码
基于时间序列和用户相关的一次性口令
用户的生物学特征:指纹、语音、眼睛虹膜等
为了确认用户的身份,防止请求伪造,在安全要求高的场合,经常会使用组合认证(也叫多因素认证),也就是同时使用多个认证方式对用户的身份进行校验。
什么是授权?
授权(Authorization)简单来讲就是授予第三方访问用户资源的权限。
在互联网应用开发中,主要通过下面几种方式实现授权:
通过 session 机制,一个访问会话保持着用户的授权信息
通过 cookie 机制,一个网站的 cookie 保持着用户的授权信息
颁发授权令牌(token),一个合法有效的令牌中保持着用户的授权信息
什么是 cookie?
cookie 的特点
cookie 最开始被设计出来是为了弥补HTTP在状态管理上的不足。
cookie 存储在客户端:cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,它会在浏览器下次向同一服务器再发起请求时被携带并发送到服务器上。
cookie 是不可跨域的:每个 cookie 都会绑定单一的域名(包括子域),无法在别的域名下获取使用。
设置 cookie
服务器向客户端发送 Cookie 是通过 HTTP 响应报文实现的,在 Set-Cookie 中设置需要向客户端发送的cookie,cookie格式如下:
Set-Cookie: value[; expires=date][; domain=domain][; path=path][; secure]
cookie 工作流程
(1)浏览器向服务器发送请求;
(2)服务器响应请求,向浏览器设置 cookie;
(3)浏览器将 cookie 存在本地,下一次请求带上该 cookie;
(4)服务器响应请求。
cookie 常见属性
(1)name=value
键值对,设置 Cookie 的名称及相对应的值。
(2)domain
指定 cookie 所属域名,默认是当前域名。如果 cookie 的 domain 设置为 taobao.com,那么 item.taobao.com, order.taobao.com 都是可以共享 cookie 的, 但是访问 tmall.com 就不能共享 cookie 了,这就涉及跨域访问的问题,跨域问题如何解决,这里不展开,有兴趣可以自行搜索。
(3)path
指定 cookie 在哪个路径(路由)下生效,默认是 ‘/’。如果设置为 /abc,则只有 /abc 下的路由可以访问到该 cookie,如:/abc/read。
(4)expires
指定 cookie 的过期时间(GMT时间格式),到达该时间点后该 cookie 就会自动失效。
(5)max-age
HTTP 1.1中定义的,优先级高于 expires 字段。
max-age 表示 cookie 有效期,单位秒。如果为正数,则该 cookie 在 max-age 秒后失效;如果为负数,该 cookie 为临时 cookie ,关闭浏览器即失效,浏览器也不会以任何形式保存该 cookie ;如果为 0,表示删除该 cookie 。默认为 -1。
(6)HttpOnly
如果给某个 cookie 设置了 httpOnly 属性,则无法通过 JS 脚本读写该 cookie 的信息。
(7)secure
该 cookie 是否仅被使用安全协议传输,默认为false。当 secure 值为 true 时,cookie 在 HTTP 中是无效的。
什么是 session?
session 翻译过来就是『会话』。用户打开一个浏览器, 点击多个超链接, 访问服务器多个web资源, 然后关闭浏览器, 整个过程称之为一个会话。
(1)session 的特点
session 是另一种记录服务器和客户端会话状态的机制;
session 存储在服务器端,一般是文件中,也可以存在数据库或缓存中。
session 一般基于 cookie 实现。session 中包含敏感信息存储在服务器端,通常将 sessionId 存储在客户端的 cookie 中,客户端每次请求携带 sessionId 即可识别用户。
(2)session 工作流程
(1)用户第一次请求,提交用户名密码等信息进行登录认证,服务器根据用户提交的信息进行鉴权,鉴权成功后创建 session 对象,并将 sessionId 塞入 cookie 中,浏览器收到响应信息将 cookie 存入本地;
(2)用户第二次请求,以查看订单信息为例,浏览器自动将当前域名下的 cookie 信息发送给服务端,服务端解析 cookie,获取到 sessionId 后再查找对应的 session 对象,如果 session 对象存在说明用户已经登录,继续下一步操作。
从上面的流程可知,sessionId 是 cookie 和 session 中间的一道桥梁。
需要注意:如果客户端禁用了 cookie,还可以通过 url 重写等方法传递 sessionId。
cookie 和 session 的区别
存储方式:cookie 数据存放在客户的浏览器上,session 数据放在服务器上;
安全性:cookie 是本地存储,不是很安全,别人可以分析存放在本地的 cookie 并进行欺骗;
存储大小:很多浏览器限制单个 cookie 保存的数据不能超过4K,一个站点最多保存20个cookie,session 没有类似的限制;
生存周期:cookie 可设置为长时间保持,Session 一般失效时间较短,一般客户端关闭 session 就会失效。
什么是 token ?
(1)token 的组成
token 是验证用户身份的凭证,我们通常叫它:令牌。
最简单的token组成: uid(用户唯一的身份标识)、time(当前时间的时间戳)、sign(签名,以哈希算法压缩成一定长的十六进制字符串)
(2)token 特点
无状态、可扩展
支持移动端设备
支持跨程序调用
安全
(3)token 工作流程
第一步:客户端使用用户名密码或者扫码等形式请求登录;
第二步:服务端收到请求后进行鉴权,鉴权成功后服务端会生成一个 token 并发送给客户端,客户端收到 token 以后,会把它存储起来,比如放在 cookie 里或者 localStorage 里;
第三步:客户端下一次向服务端请求资源的时候需要带着存储的 token;
第四步:服务端收到请求,然后去验证客户端请求里面带着的 token ,如果验证成功,就向客户端返回请求的数据。
需要注意:
客户端请求时可以将 token 放到 HTTP 的 Header 里;
基于 token 的用户认证是一种服务端无状态的认证方式,服务端不用存放 token 数据。
用解析 token 的计算时间换取 session 的存储空间,从而减轻服务器的压力,减少频繁的查询数据库
什么是 JWT?
(1)JWT 是标准 化的 token
从本质上讲 JWT 也是一种 token,只不过 JWT 是被大家广泛接受的标准。
JWT 即:Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准(RFC 7519)。
JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息。
(2)JWT 的组成
JWT 共有三部分组成:
第一部分我们称它为头部(header)
第二部分我们称其为载荷(payload, 类似于飞机上承载的物品)
第三部分是签证(signature)
在https://jwt.io/网站上可以解析一个已知的 JWT:
header
jwt的头部承载两部分信息:声明类型,这里是jwt;声明加密的算法,通常直接使用 HMAC SHA256;
playload
载荷就是存放有效信息的地方。主要包含三个部分:标准中注册的声明;公共的声明;私有的声明。
signature
jwt 的第三部分是一个签名信息,这个签证信息由三部分组成:header (base64后的);payload (base64后的);secret。
(3)JWT 的特点
不在 jwt 的 payload 部分存放敏感信息,因为该部分是客户端可解密的部分。
保护好 secret 私钥,该私钥非常重要。
如果可以,请使用 https 协议。
总结
在分布式微服务技术日趋流行的今天,大型网站的设计都尽量避免使用 session 实现 HTTP 状态化。
session简单粗暴,在服务端维护会话信息,在客户端保存session id,服务端能够轻易地把会话控制在自己的手中,但服务集群化产生了session共享的负担;
jwt(token)只在客户端保存会话信息,服务端通过密钥校验会话,(相比session)拿时间换空间,卸下了服务端集群共享会话信息的负担,同时也加大了服务端控制会话的难度。
补充:jwt的框架:JJWT
JJWT是一个提供端到端的JWT创建和验证的Java库。永远免费和开源(Apache License,版本2.0),JJWT很容易使用和理解。它被设计成一个以建筑为中心的流畅界面,隐藏了它的大部分复杂性。
JJWT的目标是最容易使用和理解用于在JVM上创建和验证JSON Web令牌(JWTs)的库。
JJWT是基于JWT、JWS、JWE、JWK和JWA RFC规范的Java实现。
JJWT还添加了一些不属于规范的便利扩展,比如JWT压缩和索赔强制。
规范兼容:
创建和解析明文压缩JWTs
创建、解析和验证所有标准JWS算法的数字签名紧凑JWTs(又称JWSs):
HS256: HMAC using SHA-256
HS384: HMAC using SHA-384
HS512: HMAC using SHA-512
RS256: RSASSA-PKCS-v1_5 using SHA-256
RS384: RSASSA-PKCS-v1_5 using SHA-384
RS512: RSASSA-PKCS-v1_5 using SHA-512
PS256: RSASSA-PSS using SHA-256 and MGF1 with SHA-256
PS384: RSASSA-PSS using SHA-384 and MGF1 with SHA-384
PS512: RSASSA-PSS using SHA-512 and MGF1 with SHA-512
ES256: ECDSA using P-256 and SHA-256
ES384: ECDSA using P-384 and SHA-384
ES512: ECDSA using P-521 and SHA-512
这里以github上的demo演示,理解原理,集成到自己项目中即可。
应用采用 spring boot + angular + jwt
Maven 引进 : pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.nibado.example</groupId>
<artifactId>jwt-angular-spring</artifactId>
<version>0.0.2-SNAPSHOT</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<commons.io.version>2.4</commons.io.version>
<jjwt.version>0.6.0</jjwt.version>
<junit.version>4.12</junit.version>
<spring.boot.version>1.5.3.RELEASE</spring.boot.version>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>${commons.io.version}</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>${jjwt.version}</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>${junit.version}</version>
</dependency>
</dependencies>
</project>
WebApplication.java
package com.nibado.example.jwtangspr;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@ComponentScan
@Configuration
public class WebApplication {
//过滤器
@Bean
public FilterRegistrationBean jwtFilter() {
final FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new JwtFilter());
registrationBean.addUrlPatterns("/api/*");
return registrationBean;
}
public static void main(final String[] args) throws Exception {
SpringApplication.run(WebApplication.class, args);
}
}
JwtFilter.java
package com.nibado.example.jwtangspr;
import java.io.IOException;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.filter.GenericFilterBean;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureException;
public class JwtFilter extends GenericFilterBean {
@Override
public void doFilter(final ServletRequest req,
final ServletResponse res,
final FilterChain chain) throws IOException, ServletException {
final HttpServletRequest request = (HttpServletRequest) req;
//客户端将token封装在请求头中,格式为(Bearer后加空格):Authorization:Bearer +token
final String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
throw new ServletException("Missing or invalid Authorization header.");
}
//去除Bearer 后部分
final String token = authHeader.substring(7);
try {
//解密token,拿到里面的对象claims
final Claims claims = Jwts.parser().setSigningKey("secretkey")
.parseClaimsJws(token).getBody();
//将对象传递给下一个请求
request.setAttribute("claims", claims);
}
catch (final SignatureException e) {
throw new ServletException("Invalid token.");
}
chain.doFilter(req, res);
}
}
UserController.java
package com.nibado.example.jwtangspr;
import java.util.Arrays;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import javax.servlet.ServletException;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@RestController
@RequestMapping("/user")
public class UserController {
//这里模拟数据库
private final Map<String, List<String>> userDb = new HashMap<>();
@SuppressWarnings("unused")
private static class UserLogin {
public String name;
public String password;
}
public UserController() {
userDb.put("tom", Arrays.asList("user"));
userDb.put("wen", Arrays.asList("user", "admin"));
}
/*以上是模拟数据库,并往数据库插入tom和sally两条记录*/
@RequestMapping(value = "login", method = RequestMethod.POST)
public LoginResponse login(@RequestBody final UserLogin login)
throws ServletException {
if (login.name == null || !userDb.containsKey(login.name)) {
throw new ServletException("Invalid login");
}
//加密生成token
return new LoginResponse(Jwts.builder().setSubject(login.name)
.claim("roles", userDb.get(login.name)).setIssuedAt(new Date())
.signWith(SignatureAlgorithm.HS256, "secretkey").compact());
}
@SuppressWarnings("unused")
private static class LoginResponse {
public String token;
public LoginResponse(final String token) {
this.token = token;
}
}
}
ApiController.java
package com.nibado.example.jwtangspr;
import io.jsonwebtoken.Claims;
import java.util.List;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/api")
public class ApiController {
@SuppressWarnings("unchecked")
@RequestMapping(value = "role/{role}", method = RequestMethod.GET)
public Boolean login(@PathVariable final String role,
final HttpServletRequest request) throws ServletException {
final Claims claims = (Claims) request.getAttribute("claims");
return ((List<String>) claims.get("roles")).contains(role);
}
}
index.html
<!doctype html>
<html ng-app="myApp">
<head>
<meta charset="utf-8"/>
<meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
<title>JSON Web Token / AngularJS / Spring Boot example</title>
<meta name="description" content="">
<meta name="viewport" content="width=device-width">
<link rel="stylesheet" href="libs/bootstrap/css/bootstrap.css">
<script src="libs/jquery/jquery.js"></script>
<script src="libs/bootstrap/js/bootstrap.js"></script>
<script src="libs/angular/angular.js"></script>
<script src="app.js"></script>
</head>
<body>
<div class="container" ng-controller='MainCtrl'>
<h1>{{greeting}}</h1>
<div ng-show="!loggedIn()">
Please log in (tom and sally are valid names)</br>
<form ng-submit="login()">
Username: <input type="text" ng-model="userName"/><span><input type="submit" value="Login"/>
</form>
</div>
<div class="alert alert-danger" role="alert" ng-show="error.data.message">
<span class="glyphicon glyphicon-exclamation-sign" aria-hidden="true"></span>
<span class="sr-only">Error:</span>
{{error.data.message}}
</div>
<div ng-show="loggedIn()">
<div class="row">
<div class="col-md-6">
<h3><span class="label label-success">Success!</span> Welcome {{userName}}</h3>
<a href ng-click="logout()">(logout)</a>
</div>
<div class="col-md-4">
<div class="row header">
<div class="col-sm-4">{{userName}} is a</div>
</div>
<div class="row">
<div class="col-sm-2">User</div>
<div class="col-sm-2"><span class="glyphicon glyphicon-ok" aria-hidden="true" ng-show="roleUser"></span></div>
</div>
<div class="row">
<div class="col-sm-2">Admin</div>
<div class="col-sm-2"><span class="glyphicon glyphicon-ok" aria-hidden="true" ng-show="roleAdmin"></span></div>
</div>
<div class="row">
<div class="col-sm-2">Foo</div>
<div class="col-sm-2"><span class="glyphicon glyphicon-ok" aria-hidden="true" ng-show="roleFoo"></span></div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
app.js
var appModule = angular.module('myApp', []);
appModule.controller('MainCtrl', ['mainService','$scope','$http',
function(mainService, $scope, $http) {
$scope.greeting = 'Welcome to the JSON Web Token / AngularJR / Spring example!';
$scope.token = null;
$scope.error = null;
$scope.roleUser = false;
$scope.roleAdmin = false;
$scope.roleFoo = false;
$scope.login = function() {
$scope.error = null;
mainService.login($scope.userName).then(function(token) {
$scope.token = token;
$http.defaults.headers.common.Authorization = 'Bearer ' + token;
$scope.checkRoles();
},
function(error){
$scope.error = error
$scope.userName = '';
});
}
$scope.checkRoles = function() {
mainService.hasRole('user').then(function(user) {$scope.roleUser = user});
mainService.hasRole('admin').then(function(admin) {$scope.roleAdmin = admin});
mainService.hasRole('foo').then(function(foo) {$scope.roleFoo = foo});
}
$scope.logout = function() {
$scope.userName = '';
$scope.token = null;
$http.defaults.headers.common.Authorization = '';
}
$scope.loggedIn = function() {
return $scope.token !== null;
}
} ]);
appModule.service('mainService', function($http) {
return {
login : function(username) {
return $http.post('/user/login', {name: username}).then(function(response) {
return response.data.token;
});
},
hasRole : function(role) {
return $http.get('/api/role/' + role).then(function(response){
console.log(response);
return response.data;
});
}
};
});
运行应用
效果