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 {
// 有token
if (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);
//转json
String 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.authorization
if(token){
// 不为空放本地
window.localStorage.setItem("authorization", token)
}
let refreshtoken = response.headers.refreshtoken
console.log(refreshtoken)
if(refreshtoken){
window.localStorage.setItem("refreshtoken", refreshtoken)
}
return response
}
)
3.2.5 前端在请求拦截将两个token设置到请求头
http.interceptors.request.use(
config =>{
// 从浏览器本地得到token
let token = window.localStorage.getItem("authorization")
let refreshtoken = window.localStorage.getItem("refreshtoken")
config.headers.authorization = token
config.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){
//过期 重新生成token
token = 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;
}