一.cookie-session总结,与jwt区别
1:cookie-session技术需要浏览器支持,response返回set-cookie属性后。http请求request 会自动携带cookie
而,jwt和toekn一样 每次ajax都需要手动携带,一般是设置的request的header头中
2.cookie-session技术在web应用方便,对于集群,sessio存在容器(tomcat)并发大了占用内存的说法都有解决方案,
比如spring-redis 把session存在redis中,集群环境每个节点也是连接redis,tomcat不存储session,这样就可以实现集群了。
cookie-session自带刷新功能,而且浏览器会自动携带
传统的web应用还是使用cookie-session技术方便
二.jwt和token技术的区别
JWT是json web token缩写,JWT包含三个部分: Header头部,Payload负载和Signature签名
jwt的思想为去中心化。生成的令牌携带了加密的用户信息 直接后台验证 ,一旦生成后只要没到设置的失效时间就一直有效,这样如果泄露后无法让该令牌立即失效,除非
在加一层拦截验证,这样就失去了使用jwt的意义。
token就是一个用户标识(key),每次传入后台使用token查询redis或者数据库等查询用户信息验证,这样token泄露后可以及时在redis或数据库中剔除该toekn
使用jwt和toekn都没类似cookie-session刷新过期时间的机制(比如session超时时间30分钟,在29分钟时使用session后,超时时间又会重置为30分钟),因为jwt的过期时间是生成jwt令牌时指定的 ,需要自己采取相应方案,防止用户使用一段时间到期了,需要重新登录生成jwt或toekn。
jwt 口令如果快过期了,是没有设置延长过期时间的方式,因为过期时间本身就包含在了jwt口令中,只能重新生成新的jwt返回给用户使用,依此循环。
普通token的方式,因为token只是一个唯一标识,用户信息,过期时间在后端,直接延长就行。
我在开发移动app中就是使用的token技术 ,原生app都有提供获取机器唯一标识 UUID的函数,登录后UUID当做key,存入登录信息,
每次ajax请求携带UUID,登录后每个需要登录才能访问的请求都需要UUID查询redis或者数据库 验证用户。
jwt和token技术适合非web应用环境,比如移动端,或者发布服务给第三方调用,用来做用户验证
总结jwt和token最大的区别就是jwt令牌自带用户信息,过期时间等,token只是一个key,需要这个key去对应的存储中查找用户信息,项目上看情况使用
3.jwt demo
我使用的spring-boot项目写的
1.新建spring-boot项目
2.项目创建好pom引入jwt jar,我这边引入的JJWT最新版本
jwt的类库蛮多,我这边使用的jjwt测试
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
3.创建JwtUtil工具类
JwtUtil.java
package com.wying.utils;
import com.fasterxml.jackson.core.JsonProcessingException;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtUtil {
//JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
// public static final String JWT_ID = UUID.randomUUID().toString();
public static final String JWT_ID = "111111111222222222222222223333333333333";
/**
* 附加码加密密文 我这边用的base64编码的 使用时在解码 也可以直接定义不加密的普通密码 使用时也不用解码了
*/
public static final String JWT_SECRET = "YnNvZnRwZXNoaGhoaGho";
public static final int JWT_TTL = 60*60*1000; //单位毫秒 这里设置一小时
public static ObjectMapper mapper = new ObjectMapper();
/**
* 由附加码 JWT_SECRET 生成加密key
*
* @return
*/
public SecretKey generalKey() {
String stringKey = JWT_SECRET;
// base64 密码 解密
byte[] encodedKey = Base64.decodeBase64(stringKey);
// 根据给定的字节数组使用AES加密算法构造一个密钥
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");
return key;
}
/**
* 创建jwt
* @param id jwtid
* @param issuer jwt签发人
* @param subject jwt 主题 通常存放用户信息
* @param ttlMillis jtw过期时间
* @return
* @throws Exception
*/
public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {
// 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
// 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map<String, Object> claims = new HashMap<>();
claims.put("uid", "1111111111");
// 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
// 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
SecretKey key = generalKey();
// 下面就是在为payload添加各种标准声明和私有声明了
JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
.setClaims(claims) // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setId(id) // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setIssuedAt(now) // iat: jwt的签发时间
.setIssuer(issuer) // issuer:jwt签发人
.setSubject(subject) // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥
// 设置过期时间
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 解密jwt
*
* @param jwt 令牌
* @return
* @throws Exception
*/
public Claims parseJWT(String jwt) throws Exception {
SecretKey key = generalKey(); //签名秘钥,和生成的签名的秘钥一模一样
Claims claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(jwt).getBody(); //设置需要解析的jwt
return claims;
}
public static void main(String[] args) throws Exception {
Map<String,String> userMeesgaeMap=new HashMap<>();
userMeesgaeMap.put("userId","CS001");
userMeesgaeMap.put("userName","张三");
userMeesgaeMap.put("idCard","330110202001010101");
userMeesgaeMap.put("releId","100");
String subject =mapper.writeValueAsString(userMeesgaeMap);
try {
JwtUtil util = new JwtUtil();
String jwt = util.createJWT(JWT_ID, "gaom", subject,JWT_TTL);
System.out.println("util.createJWT 生成的JWT :" + jwt);
System.out.println("==============解密=====================");
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
Claims c = util.parseJWT(jwt);
System.out.println("jwt id:"+c.getId());//jwt id
System.out.println("jwt签发时间:"+sdf.format(c.getIssuedAt()));
System.out.println("jwt过期时间:"+sdf.format(c.getExpiration()));
System.out.println("jwt用户信息:"+c.getSubject());
System.out.println("jwt签发人:"+c.getIssuer());
System.out.println("jwt 额外附加的属性uid :"+c.get("uid", String.class));
} catch (Exception e) {
e.printStackTrace();
}
}
}
4.运行,测试效果
5.jwt应用
业务场景:微信H5中无法下载文件,用户下载体检报告时我是先判断访问来源,如果是微信访问的话提示用户右上角在浏览器打开链接下载,在浏览器访问pdf报告下载链接由于不需要二次登录,直接传递体检编号的话存在遍历风险,传递加密的体检编号,下载时在解密可以做到遍历风险,但是这个加密字符串是一直有效的,用户保存起来可以随时下载,所以想到了jwt,用体检编号生成一个有下载期限的令牌,同时在jwt 主题subject属性中放入体检编号,jwt验证通过后解析subject在得到体检编号,同时实现体检编号加密传输和下载期限设置;
。
4.1 用户下载报告前生成wjt令牌
/**
* 创建报告jwt口令
* @param httpServletRequest
* @param tjTjdjbDto
* @return
*/
@GetMapping(value = "/createRporToken")
public WrapperRusult createRporToken(HttpServletRequest httpServletRequest, TjTjdjbDto tjTjdjbDto) {
log.info("入参tjTjdjbDto:" + tjTjdjbDto);
try {
//验证权限 防止水平越权
Object tjDwdmbObjDto = httpServletRequest.getSession().getAttribute(ConstantUtil.DW_USER_MESSAGE);
if (tjDwdmbObjDto == null) {
return WrapperRusult.fail("查看PDF报告失败 提示:"+(ConstantUtil.SESSION_TIME_OUT_TIPS),"");
}
TjDwdmbDto tjDwdmbDto = (TjDwdmbDto) tjDwdmbObjDto;
/**
* 设置查询条件 只能查询本单位的报告
*/
tjTjdjbDto.setDwbh(tjDwdmbDto.getDwbh());
tjTjdjbDto.setJgid(tjDwdmbDto.getJgid());
List<TjTjdjbDto> tjTjdjbDtoList= companyReportService.selectReportByTjbh(tjTjdjbDto);
if(tjTjdjbDtoList.size()==0){
return WrapperRusult.fail("查看PDF报告失败 提示: tjbh:"+tjTjdjbDto.getTjbh()+" 未检索到报告301","");
}
if(!tjTjdjbDtoList.get(0).getDwbh().equals(tjDwdmbDto.getDwbh())){
return WrapperRusult.fail("查看PDF报告失败 提示: tjbh:"+tjTjdjbDto.getTjbh()+" 不属于当前登录单位: "+tjDwdmbDto.getMc());
}
//生成令牌
JwtUtil util = new JwtUtil();
String jwt = util.createJWT(JWT_ID, "gaom", "{\"tjbh\":\""+tjTjdjbDto.getTjbh()+"\"}",JWT_TTL);
String urlEncoderJwt=java.net.URLEncoder.encode(jwt);
log.info("util.createJWT 生成的JWT :{} url编码后:{}",jwt,urlEncoderJwt);
return WrapperRusult.success(urlEncoderJwt);
} catch (Exception e) {
//返回异常信息 方便前端排查错误
return WrapperRusult.fail(e.getMessage());
}
}
4.2 外部浏览器下载报告先验证jwt
/**
* 下载报告 返回pdf 文件流
* @param httpServletRequest
* @param httpServletResponse
* @param tjTjdjbDto
*/
@GetMapping(value = "/downReport")
public void downReport(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse, TjTjdjbDto tjTjdjbDto) {
ByteArrayInputStream byteArrayInputStream = null;
BufferedInputStream bis = null;
BufferedOutputStream bos = null;
log.info(" 入参tjTjdjbDto:" + tjTjdjbDto);
try {
String tjbhToken= tjTjdjbDto.getTjbhToken();
String urlDecoderTjbhToken=java.net.URLDecoder.decode(tjbhToken);
log.info("tjbhToken:{} ",urlDecoderTjbhToken);
JwtUtil util = new JwtUtil();
/**
* 如果令牌过期会直接抛出异常,不用我们手动判断是否过期 只能能执行下去得到Claims对象 就在有效期
*/
Claims c = util.parseJWT(urlDecoderTjbhToken);
String subject=c.getSubject();
String tjbh= JSONUtil.toBean(subject, TjTjdjbDto.class).getTjbh();
log.info("tjbhToken:{} 解析到 subject:{}, 体检编号:{} ",urlDecoderTjbhToken,subject,tjbh);
tjTjdjbDto.setTjbh(tjbh);
String pdfBase64= companyReportService.getPdfReportBase64(tjTjdjbDto);
byte[] bytes = Base64.getDecoder().decode(pdfBase64);
//创建一个将byte作为缓冲区的ByteArrayInputStream对象
byteArrayInputStream = new ByteArrayInputStream(bytes);
//创建从底层输入流读取的读取数据的缓冲输入流对象
bis = new BufferedInputStream(byteArrayInputStream);
//创建一个缓冲区字节对象
byte[] buffer = new byte[bis.available()];
//读取缓冲区的数据
bis.read(buffer);
//清空response
httpServletResponse.reset();
// httpServletResponse.setHeader("Content-type", "application/pdf");
httpServletResponse.setHeader("Content-Disposition", "attachment;filename=" + new String
(tjbh.getBytes("gb2312"), "iso-8859-1") + ".pdf");
//创建从底层输出流输出页面数据的缓冲流输出对象
bos = new BufferedOutputStream(httpServletResponse.getOutputStream());
bos.write(buffer);
bos.flush();
} catch (Exception e) {
e.printStackTrace();
BaseUtil.outputString(httpServletResponse,"查看PDF报告失败 异常:e"+e.getMessage());
} finally {
try {
if (bos != null) {
bos.close();
}
if (bis != null) {
bis.close();
}
if (byteArrayInputStream != null) {
byteArrayInputStream.close();
}
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
如果jw令牌过期会抛出异常