session一致性问题
在集群或者分布式系统中,用户登录后的,由于服务端是集群环境或者分布式环境,如何保证用户每次与服务器交互都是使用原来的session或者实现单点登录,这里就涉及session一一致性的问题。
解决session一致性问题可以从两种思路实现,一种是session服务端存储,一种是session客户端存储。
session服务端存储
服务端存储session,需要保证用户请求过来,能够使用到同一个session进行交互,有以下的实现方式
- session sticky,这种方式通过对同一个用户请求进行定向,用户每次请求,都访问同一台服务器,这样就能完成每次访问都是同一个服务器。
- session复制,这种方式就是在服务器集群中对session进行复制存储,每个服务端都存储同一份session,这样也能保证用户每次访问都能使用同一份session。
- session统一存储,使用第三方中间件存储session,每次用户访问时,去中间件进行session查找对应的session,保证每次请求使用同一个session。
session客户端存储
session客户端存储的话,实现思路是服务端不保存任何session信息,session信息附加在客户端存储,每次请求把session信息传递到服务端,服务端需要做的就是验证客户端的session是否是正确有效的。jwt的实现思路就是其中的一种实现方式,也是这篇文章介绍的点。
jwt简介
jwt中的概念
首先,根据jwt官网中的信息,先了解一下jwt中的一些常见概念。
token:返回给客户端用来登录验证的凭证,token有效表示用户已经登录,token中的信息有效可信。token的生成公式如下所示。token=base64(header).base64(payload).sinatrue
header:组成token中的一部分,主要用于定义签名sinatrue的生成算法,例如HS256,以及token类型JWT。例如下面
{
"alg": "HS256",
"typ": "JWT"
}
payload:同样也是生成token的一部分,payload中的信息,有部分key是jwt中已经声明使用含义的,例如exp表示token有效期等等。iss (issuer), exp (expiration time), sub (subject), aud (audience)。除了这些已经声明使用含义的信息,还可以附加我们自定义的key的信息到payload中,作为token的一部分,验证token后,我们加入大token中的信息可以使用,效果类似于存储在session中,例如把登录用户名放入token,验证token后,可以根据token中的用户名信息判断当前登录的用户。例如下面
{
"sub": "1234567890",
"name": "John Doe",
"admin": true,
"exp": 1544872606128
}
sinatrue:token的签名,也是token的组成部分,用来验证token中的header和payload信息是否被修I改过并且是否已经过期等,如果token没被修改并且没过期,表示token可用,用户当前依然属于登录状态。key是服务端定义的一个密钥用来保证在header和payload都被知晓的情况下,key作为混淆值,保证token不被伪造。
根据header定义的签名短发,签名格式如下:
sinatrue=SignatureAlgorithm.HS256( base64(header) + "." + base64(payload) , base64(key))
token验证
验证token的过程其实跟重新生成一个token的过程是一样的
根据传递过来的header、payload以及服务端存储的密钥,重新生成一个token对比,如果token相同,那么标志token有效,没被修改。同时如果token没被修改,还可以进一步同bease64解密payload,然后根据payload中的exp有效期信息,判断token是否已经过期。
jwt基本工作流程
下面看一张jwt的基本工作流程图:
根据上图以及了解的基本概念,应该就能了解token的生成、验证和使用原理。验证完token之后,我们就可以使用token中的payload中的信息。
jwt实践
下面看一下jwt的api是如何使用的。
引入pom依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
新建了以下一个jwt生成token和验证token的工具类。
public class JwtTokenUtils {
/**
* token默认过期时间
*/
public static final Integer DEFAULT_TOKEN_EXPIRE_TIME = 1000 * 60 * 30;
public static final String DEFAULT_TOKEN_KEY = "123456";
public static void main(String[] args) {
//获取secretkey
System.out.println(generatorSecretkey("123456"));
//用户名加入到token中,并生成一个新的token
String tolen = generateTokenBy("1admin");
System.out.println(tolen);
//验证token并且获取token中的payload信息(json结构)
System.out.println(pharseToken(tolen));
}
private static String generatorSecretkey(String key){
return BaseEncoding.base64().encode(key.getBytes());
}
//seperator--------------------------------------------------------------------------
/**
* key-val格式获取payload中信息
* @param token
* @param secretkey
* @return Claims 一个继承map的结构对象
*/
public static Claims pharseToken(String token, String secretkey){
Jws<Claims> claimsJwt=Jwts.parser()
.setSigningKey(secretkey)
.parseClaimsJws(token);
return claimsJwt.getBody();
}
public static Claims pharseToken(String token){
Jws<Claims> claimsJwt=Jwts.parser()
.setSigningKey(generatorSecretkey(DEFAULT_TOKEN_KEY))
.parseClaimsJws(token);
return claimsJwt.getBody();
}
/**
* 生成token信息
* @param header like
* {
* "alg": "HS256",
* "typ": "JWT"
* }
* @param payload like
* {
* "sub": "1234567890",
* "name": "John Doe",
* "admin": true,
* "exp": ${timestamp}
* }
*
* @param secretkey 加盐
* @return token=base64(header).base64(payload).sinatrue ,
* sinatrue=SignatureAlgorithm.HS256( base64(header) + "." + base64(payload) , base64(key))
* secretKey=base64(key)
* JWT规范是以上实现
*/
public static String generateTokenBy(Map<String, Object> header, Map<String, Object> payload, String secretkey){
return Jwts.builder()
.setHeader(header)
.setPayload(JsonUtils.beanToJson(payload))
.signWith(SignatureAlgorithm.HS256, secretkey).compact();
}
public static String generateTokenBy(String userName, long exp, String secretkey){
Map<String, Object> defaultHeader = Maps.newHashMap();
defaultHeader.put("alg", "HS256");
defaultHeader.put("typ", "JWT");
Map<String, Object> payload = Maps.newHashMap();
payload.put("userName", userName);
payload.put("exp", exp);
return generateTokenBy(defaultHeader, payload, secretkey);
}
public static String generateTokenBy(String userName, String secretkey){
return generateTokenBy(userName, getDefaultTokenExpireTime(), secretkey);
}
public static String generateTokenBy(String userName){
return generateTokenBy(userName, generatorSecretkey(DEFAULT_TOKEN_KEY));
}
public static Long getDefaultTokenExpireTime(){
Date curDate = new Date();
return curDate.getTime() + DEFAULT_TOKEN_EXPIRE_TIME;
}
}
有了以上的工具之后,我们就可以实现登录时生成token,请求时验证token来保证session一致性。
在登录方法做这样的处理。这样token就会存储在客户端cookie中,方便下次请求时验证。
@PostMapping("/login")
@ResponseBody
public ResponseData doLogin(String username, String password,
HttpServletResponse response) {
ResponseData data = new ResponseData();
if (hasUser(username, password)) {
//如果验证成功,生成token
String token = JwtTokenUtils.generateTokenBy(username);
//response设置cookie
response.addHeader("Set-Cookie", "access_token=" + token + ";Path=/;HttpOnly");
data.setCode(SUCCESS);
return data;
}
data.setCode(FAIL);
return data;
}
验证token。验证时也好办,只需要在http请求时,拦截请求,获取request中的cookie信息,并且获取token,然后调用验证方法。可以通过继承HandlerInterceptorAdapter,拦截器方式实现验证逻辑。下面是一个简单实现
public class TokenHandlerInterceptorAdapter extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token= null;
Cookie[] cookies = request.getCookies();
if (ArrayUtils.isNotEmpty(cookies)) {
for (Cookie cookie : cookies) {
if ("access_token".equals(cookie.getName())) {
token= cookie.getValue();
}
}
}
if(StringUtils.isEmpty(token)){
response.sendRedirect(LOGIN_PAGE);
return false;
}
try{
Claims claims=JwtTokenUtils.pharseToken(token);
String userName= (String) claims.get("userName");
request.setAttribute("userName", userName);
return super.preHandle(request, response, handler);
}catch (ExpiredJwtException e){
//token'过期了
response.sendRedirect(LOGIN_PAGE);
}catch (SignatureException e1){
//token验签不通过
response.sendRedirect(LOGIN_PAGE);
}catch (Exception e){
}finally {
}
return false;
}
}