1.CSRF跨站请求伪造
跨站请求伪造(英语:Cross-site request forgery),也被称为 one-click attack 或者 session riding,通常缩写为 CSRF 或者 XSRF, 是一种挟制用户在当前已登录的Web应用程序上执行非本意的操作的攻击方法。
为了防止CSRF攻击,产检的防御措施有:生成随即令牌(token)、设置Cookie的SameSite属性为Strict或Lax 、验证Referer字段、双重身份验证等
2.JWT概念
JWT(JSON Web Token)是一种用于进行身份验证和授权的开放标准(RFC 7519)。它是一种安全的、基于JSON的令牌,用于在客户端和服务器之间传递声明
2.1 JWT组成
header | 声明类型、声明加密的算法,通常直接使用HMAC SHA256或RSA |
payload | 也称为JWT Claims,包含用户的一些非隐私数据(秘钥、签发人、签发时间、有效时间等) |
Signature | 签证信息,由三部分组成:header(base64后的)、payload(base64后的)、secred |
3.登录详细流程
3.1 单token验证
3.1.1 登录成功,生成token并返回
1.生成token
String token = JWTUtil.generateToken(user.getId());
2.设置响应头
response.setHeader("authorization", token);
3.暴露响应头
浏览器不认识自定义的头,如果不暴露浏览器会自动屏蔽
response.setHeader("Access-Control-Expose-Headers","authorization");
3.1.2 前端得到token保存在浏览器本地
sessionstorage:在会话期间有效(浏览器打开期间)
localstorage:只要不主动删除、不卸载浏览器,数据一直有效
let token = res.headers.authorization
window.localStorage.setItem("authorization", token)
3.1.3 通过axios拦截器自动携带token
http.interceptors.request.use(config =>{// 得到token 本地let token = window.localStorage.getItem("authorization")config.headers.authorization = token// 放行请求return config}
)
3.1.4 自定义过滤器验证token
if (token == null || token.length() == 0 || token.equals("null")){// 没登录extracted(servletResponse);// 终止return;
}else {// 有tokenif (JWTUtil.verify(token) == TokenEnum.TOKEN_SUCCESS){// 合法,放行request.getSession().setAttribute("uid", JWTUtil.getuid(token));filterChain.doFilter(servletRequest, servletResponse);}else {// 伪造或者过期,都让登录extracted(servletResponse);// 终止请求return;}
}private static void extracted(ServletResponse servletResponse) throws IOException {ResponseResult<Object> responseResult = new ResponseResult<>(403,"无法访问此界面,请登录",null);//转jsonString json = new ObjectMapper().writeValueAsString(responseResult);//设置响应头servletResponse.setContentType("application/json;charset=utf-8");servletResponse.getWriter().write(json);
}
3.2 双token验证(安全性更高)
3.2.1 登录成功生成两个token
//生成Token令牌
String token = JWTUtil.generateToken(user.getId());
//生成refreshToken
String refreshtoken = UUID.randomUUID().toString();
3.2.2 以refreshtoken作为key,token作为value放入Redis并设置过期时间
redisTemplate.opsForValue().set(refreshtoken,token,JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);
3.2.3 设置响应头、暴露头
//将token放到响应头中返回给前端(流行做法)
response.setHeader("authorization",token);
response.setHeader("refreshtoken",refreshtoken);
//暴露头,浏览器不认识自定义的头,如果不暴露浏览器会自动屏蔽
response.setHeader("Access-Control-Expose-Headers","authorization,refreshtoken");
3.2.4 前端在响应拦截器上得到两个token放到本地
http.interceptors.response.use(response =>{// 判断响应中是否有token信息,如果有则将token放到本地let token = response.headers.authorizationif(token){// 不为空放本地window.localStorage.setItem("authorization", token)}let refreshtoken = response.headers.refreshtokenconsole.log(refreshtoken)if(refreshtoken){window.localStorage.setItem("refreshtoken", refreshtoken)}return response}
)
3.2.5 前端在请求拦截将两个token设置到请求头
http.interceptors.request.use(config =>{// 从浏览器本地得到tokenlet token = window.localStorage.getItem("authorization")let refreshtoken = window.localStorage.getItem("refreshtoken")config.headers.authorization = tokenconfig.headers.refreshtoken = refreshtoken// 放行请求return config}
)
3.2.6 在AuthFilter校验两个token
//需要登录
//获取token
String token = request.getHeader("authorization");
String refreshtoken = request.getHeader("refreshtoken");//校验refreshtoken是否过期
if(refreshtoken==null || refreshtoken.length()==0 || refreshtoken.equals("null")||!redisTemplate.hasKey(refreshtoken)){//非法、过期 去登陆extracted(servletResponse);return;
}
//判断token
if(token==null || token.length()==0 || token.equals("null")){//没登陆extracted(servletResponse);return;
}else{if(JWTUtil.verify(token) == TokenEnum.TOKEN_SUCCESS){//进一步的安全验证if(token.equals(redisTemplate.opsForValue().get(refreshtoken))){//合法,登录成功//过滤器放行:让后面的过滤器 或者 servlet处理这个请求
// request.getSession().setAttribute("uid",JWTUtil.getuid("authorization"));filterChain.doFilter(servletRequest, servletResponse);}else{//伪造extracted(servletResponse);return;}}else if(JWTUtil.verify(token) == TokenEnum.TOKEN_EXPIRE){//过期 重新生成tokentoken = JWTUtil.generateToken(JWTUtil.getuid(token));//判断token与后台记录的是否一致//进一步的安全验证if(token.equals(redisTemplate.opsForValue().get(refreshtoken))){//修改redis的数据redisTemplate.opsForValue().set(refreshtoken,token,JWTUtil.REFRESH_TOKEN_EXPIRE_TIME, TimeUnit.MILLISECONDS);//将新的token返回前端//将token放到响应头中返回给前端(流行做法)HttpServletResponse response = (HttpServletResponse) servletResponse;response.setHeader("authorization",token);response.setHeader("refreshtoken",refreshtoken);//暴露头,浏览器不认识自定义的头,如果不暴露浏览器会自动屏蔽response.setHeader("Access-Control-Expose-Headers","authorization");filterChain.doFilter(servletRequest, servletResponse);}else{//伪造extracted(servletResponse);return;}}else{//伪造extracted(servletResponse);return;}