目录
一. session流程
二. 分布式应用的session共享
三. JWT的组成
四. header介绍
五. payload介绍
六. signature介绍
七. base64url算法
八. JWT的使用方式
九. JWT的特点
十. java代码简单实现
一. session流程
1. 用户向服务器发送用户名和密码
2. 服务器验证通过后,在当前会话session里保存信息
3. 服务器向用户返回一个session id,写入到cookie中
4. 用户之后的每次请求都会携带cookie,将session id传给服务器
5. 服务器根据session id 查找以前保存的信息
二. 分布式应用的session共享
实现方式:
tomcat集群session共享。缺点:有广播风暴;用户量比较大的时候,占用资源比较严重。
Redis存储token。服务端使用UUID随机生成64位或者128位token放到Redis中,然后返回给客户端的cookie里。用户每次访问,服务端去Redis那里,去验证用户是否存在。缺点:网络开销较大。
JsonWebToken。
三. JWT的组成
JWT的组成:(头部 + 负载 + 签名)
Header.Payload.Signature
四. header介绍
header用于描述JWT的元数据。
{
"alg": "HS256",
"typ": "JWT"
}
大多数的header如上图。
alg:签名使用的算法。(默认是HMAC SHA256,简写为:HS256)
typ:令牌的类型。统一写JWT。
最后使用Base64URL算法将其转成字符串。
五. payload介绍
payload用于存放实际需要传递的数据。
官方规定的七个字段供选用,如下:
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
也可以有一些自定义的字段。
切勿把密码等敏感信息放到payload里。(payload没有加密处理)
最后使用Base64URL算法将其转成字符串。
六. signature介绍
signature是对上面两个的签名。
需要制定一个secret,然后使用header指定的算法,产生签名。方法如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
七. base64url算法
前面提到的算法是Base64URL,这个算法跟 Base64 算法基本类似,但有一些小的不同。
Base64Url 有三个字符+、/和=,在 URL 里面有特殊含义,所以要被替换掉:=被省略、+替换成-,/替换成_ 。
八. JWT的使用方式
1. 放到请求头里的 Authorization: Bearer<token> 里
2. 放到请求的数据体里
九. JWT的特点
(1)JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次。
(2)JWT 不加密的情况下,不能将秘密数据写入 JWT。
(3)JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数。
(4)JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑。
(5)JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
(6)为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输。
十. java代码简单实现
payload可以加一些自定义字段。这里我传个User对象,把User对象的id,name, img加入进去。放到.claim()里。K-V的形式。
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.UUID;
public class JWT {
private static final String SECRET = "usdfsdJSDG";
private static final String SUBJECT = "TOM";
private static final long EXPIRE_TIME = 1000 * 60 * 60;
private static final long EFFECT_TIME = 0;
private static final String ISSUER = "Selina";
private static final long ISSUE_TIME = 0;
private static final String ID = UUID.randomUUID().toString();
public static String generateJsonWebToken() {
String token = Jwts.builder().setSubject(SUBJECT)
.setId(ID)
.setIssuer(ISSUER)
.setExpiration(new Date(EXPIRE_TIME + System.currentTimeMillis()))
.setNotBefore(new Date(EFFECT_TIME + System.currentTimeMillis()))
.setIssuedAt(new Date(ISSUE_TIME + System.currentTimeMillis()))
.claim("username", "tom")
.claim("password", "123")
.claim("userId", 1)
.signWith(SignatureAlgorithm.HS256, SECRET)
.compact();
return token;
}
public static Claims getClaimsFromToken(String token) {
return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody();
}
public static void main(String[] args) {
String token = generateJsonWebToken();
Claims claims = getClaimsFromToken(token);
Integer userId = claims.get("userId", Integer.class);
String username = claims.get("username", String.class);
System.out.println(userId + "," + username);
}
}
第一个方法: (用于生成JWT字符串)
.set**():用于设置一些官方定义的字段。
.signWith():签名。第一个参数是加密的算法,第二个参数是指定的秘钥。
.compact() :把字符串拼凑起来。
第二个方法:(用于获取自定义字段)
.setSigningKey() : 参数里写的是秘钥。
.parseClaimsJws() :解析jwt字符串。
.getBody():获取自定义字段。
--------------------
谈谈我对session, cookies和jwt的理解
最近在做项目重构,因为核心功能仅以restful风格接口提供,因此对于会话管理这一部分,目前考虑使用jwt(Json Web Token)。本文是我在项目开发过程中对这几种会话管理技术理解的一些总结。不对之处,请指正。
为什么我们需要会话管理
众所周知,HTTP协议是一个无状态的协议,也就是说每个请求都是一个独立的请求,请求与请求之间并无关系。但在实际的应用场景,这种方式并不能满足我们的需求。举个大家都喜欢用的例子,把商品加入购物车,单独考虑这个请求,服务端并不知道这个商品是谁的,应该加入谁的购物车?因此这个请求的上下文环境实际上应该包含用户的相关信息,在每次用户发出请求时把这一小部分额外信息,也做为请求的一部分,这样服务端就可以根据上下文中的信息,针对具体的用户进行操作。所以这几种技术的出现都是对HTTP协议的一个补充。使得我们可以用HTTP协议+状态管理构建一个的面向用户的WEB应用。
Session与Cookies的区别
这里我想先谈谈session与cookies,因为这两个技术是做为开发最为常见的。那么session与cookies的区别是什么?个人认为session与cookies最核心区别在于额外信息由谁来维护。利用cookies来实现会话管理时,用户的相关信息或者其他我们想要保持在每个请求中的信息,都是放在cookies中,而cookies是由客户端来保存,每当客户端发出新请求时,就会稍带上cookies,服务端会根据其中的信息进行操作。当利用session来进行会话管理时,客户端实际上只存了一个由服务端发送的session_id,而由这个session_id,可以在服务端还原出所需要的所有状态信息,从这里可以看出这部分信息是由服务端来维护的。
除此以外,session与cookies都有一些自己的缺点:
- cookies的安全性不好,攻击者可以通过获取本地cookies进行欺骗或者利用cookies进行CSRF攻击。
- 使用cookies时,在多个域名下,会存在跨域问题。
- session在一定的时间里,需要存放在服务端,因此当拥有大量用户时,也会大幅度降低服务端的性能。
- 当有多台机器时,如何共享session也会是一个问题,也就是说,用户第一个访问的时候是服务器A,而第二个请求被转发给了服务器B,那服务器B如何得知其状态。
实际上,session与cookies是有联系的,比如,我们可以把session_id存放在cookies中的。
JWT认证
什么是JWT
JWT是Json Web Token的全称,它是由三部分组成:
- header
- payload
- signature
header中通常来说由token的生成算法和类型组成。如:
{
"alg":"HS256",
"typ":"JWT"
}
payload中则用来保存相关的状态信息。如用户id,role,name等。
{
"id": 10111000,
"role": "admin",
"name": "Leo"
}
signature部分由header,payload,secret_key三部分生成,其生成公式为:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret_key)
再将这三个部分组合成header.payload.signature的形式。
JWT如何工作
首先用户发出登录请求,服务端根据用户的登录请求进行匹配,如果匹配成功,将相关的信息放入payload中,利用上述算法,加上服务端的密钥生成token,这里需要注意的是secret_key很重要,如果这个泄露的话,客户端就可以随意篡改发送的额外信息,它是信息完整性的保证。生成token后服务端将其返回给客户端,客户端可以在下次请求时,将token一起交给服务端,一般来说我们可以将其放在Authorization首部中,这样也就可以避免跨域问题。接下来,服务端根据token进行信息解析,再根据用户信息作出相应的操作。
JWT优点与缺点及对应的解决方案
考虑JWT的实现,上面所述的关于session,cookies的缺点都不复存在了,不易被攻击者利用,安全性提高。利用Authorization首部传输token,无跨域问题。额外信息存储在客户端,服务端占用资源不多,也不存在session共享问题。感觉JWT优势很明显,但其仍然有一些缺点:
-
登录状态信息续签问题。比如设置token的有效期为一个小时,那么一个小时后,如果用户仍然在这个web应用上,这个时候当然不能指望用户再登录一次。目前可用的解决办法是在每次用户发出请求都返回一个新的token,前端再用这个新的token来替代旧的,这样每一次请求都会刷新token的有效期。但是这样,需要频繁的生成token。另外一种方案是判断还有多久这个token会过期,在token快要过期时,返回一个新的token。下面是我在项目里的一个实现。
@staticmethod def verify_auth_token(token): s = Serializer(current_app.config['SECRET_KEY']) try: data = s.loads(token) refresh_token_or_not = True if now() + 600 >= data.get('expire_time') else False except: return None return User.query.get(data['id']), refresh_token_or_not def auth(func): """ According to the token in the cookies(to compatible with the previous api, will abort) or authorization header to determine the login status. if user has logined, the user object will be in global varaibles, so we can access it easily and return normally. however, the cases below will not be allowed to finish the request: 1. no token or authorization content 2. token is in black list 3. token is expired In two cases, the token will be added to the black list. 1. logout by the user 2. the outdate token, which means the token will be expired in ten minutes """ def wrapper(*args, **kwargs): if not getattr(func, 'auth', True): return func(*args, **kwargs) token = request.headers.get('Authorization') or request.cookies.get('session_id') # to process the authorization correctly if not token: return unauthorized('Please login first!') token = token.split(' ')[-1] # logout for user if token in redis_db.get('token_black_list'): return unauthorized("Invalid token") user, refresh = User.verify_auth_token(token) if user: g.user = user try: res = func(*args, **kwargs) except Exception as e: current_app.logger.exception(e) res = internal_error if refresh: res.setdefault('token', user.generate_auth_token()) redis_db.get('token_black_list').append(token) return res else: return unauthorized("Invalid token or token has expired") return wrapper
-
用户主动注销。JWT并不支持用户主动退出登录,当然,可以在客户端删除这个token,但在别处使用的token仍然可以正常访问。为了支持注销,我的解决方案是在注销时将该token加入黑名单。当用户发出请求后,如果该token在黑名单中,则阻止用户的后续操作,返回Invalid token错误。这个地方我再稍微补充一下,其实这里的黑名单操作也比较简单,把已经注销的token存入比如说一个set中,那么在每次进行token验证时,先检查在set中是否已经存在,如果已经存在的话,则视为token已经失效,直接返回未授权。这一部分在上面的授权代码中也可以看到,不过我是放到redis缓存中的。
总结
无论session还是cookies或是jwt。目前情况是jwt仍然无法代替session,cookies也会有人用。它们各自有自己的优势和缺点,不能因为有一些缺点就否认技术的存在,缺点仍然可以采用一些技术手段来弥补,比如通过添加csrf token来阻止来自CSRF的攻击,比如利用redis集群来做session的存储和共享。技术只是工具,选择最适合你的才是最重要的