文章目录
主要内容:
- 为什么使用token?
- 什么是jwt两者有什么区别?
一.cookie?session?token?
什么是cookie和session?
● 当客户端向服务端第一次发送请求时,cookie在服务器生成,然后返回给客户端,并在客户端以key/value的形式保存起来,下一次再请求这个网站的时候会携带该cookie给服务端,cookie可以用来做登录等功能
● 当客户端向服务器第一次发送请求时,在服务端产生session,然后将sessionid返回给客户端,客户端将sessionid封装到cookie中,下一次客户端再访问时,会携带这个sessionid
cookie和session的区别?
● cookie是存储在浏览器上的,即本地硬盘;session是存储在服务端的,是在内存中
● cookie只能存储4K数据,session远大于cookie
● cookie会有csrf的风险,即虽然在同源策略下拿不到你的cookie,但是只要你发送了cookie,危险就存在了;session由于存储在服务端,即使你拿到了sessionid,并携带其发送请求,服务端那边也不会返回session
● cookie支持跨域名访问,前提是你的domain和path要配置正确;而session不支持跨域名访问
● cookie保存在客户端,不占用服务端内存;session由于保存在服务端,每个用户都有不同的session,会造成服务端压力
● cookie中只能存ASCII字符串;session可以存string、map、list等
分布式session的由来?
现在有A、B两台服务器,用户张三在A服务器上登录了,然后发现在B服务器上海要登录,这是因为B服务器上并没有对应的session,这就是session不统一的问题,也是分布式session的由来,这个问题的解决方案有如下几种:
● session复制,将session每台服务器都同步,当一台服务器上的session发生改变时,把session序列化广播到其他服务器
● session集中式管理,即使用单独的服务器来管理session
● 或者用nginx来代理,用用户的IP的hash来分配,这样每个用户只能访问一个服务器了,如果当前服务器挂了,马上启动从服务器并复制session
● 还可以用单独的模块比如redis来统一管理session
token的由来?
session让服务端有状态化,token让服务端无状态化
● 在之前的web的开发阶段,网络上的网页基本上都是静态网页,动态网页也不多,后来随着互联网的高速发展,交互式的网页也越来越多,所以,我需要知道当前是谁在与我交互,由于http的无状态性,我无法知道当前用户是谁,为了区分每个用户,就有了session,每个用户都有自己的session保存在服务器上,但用户基数多了的话,不说存储大小的问题,管理也变得麻烦起来,再加上现在分布式微服务的盛行,你需要管理多台服务器中session,这对于程序员来说又是一道难题
● jwt:这个时候,就有了token的出现,它相当于是一个令牌的意思,具体怎么做呢?首先还是先登录,注意这是第一次登陆,后端取到自己的登录名(也可以是uuid,其实随便都可以),然后用某种算法,比如MD5加盐或者HMAC-SHA256,设置一个盐或者密匙,将其进行加密,将加密前字符串和加密后字符串一起放在token中,然后token返回给前端,在前端请求拦截器将这个请求头放在header中,让所有的请求都会携带这个token,在下次请求的时候,后端会检查请求有没有携带token,没有?退回到登录界面,有?继续检查token,怎么检查呢,就不像之前session那样比对sessionid是否正确了,现在我根本就不需要存储token了,我直接截取token中的加密前字符串和加密后字符串,然后用盐或密匙对加密前字符串加密,看是不是等于加密后字符串就可以知道我的token是不是伪造的了,因为服务端根本就不会返回盐或密匙给前端,那些甚至不知道用的什么算法加密,伪造token一说无从提起,token被盗取倒是可能会发生的,但这也是无可避免的了
● token:老陈在课堂中对于这个问题是怎么做的呢?在用户登录成功后,后端UIID生成token,将token作为key,登录实体作为value保存在redis中,前端会接收这个token,取出用户实体后设置到cookie中,然后设置统一的前端拦截器,在拦截器中将token放到请求头header中,这样,以后的请求就都会携带token了,下次请求时又后端的zuul作统一鉴权(注意/login不会走这个filter),在这个filter中取出redis中的值判断这个token是否正确,正确?就可以直接访问服务资源了;错误?跳到登录界面进行登录
二.axios+token+redis+zuul
注册:
public class RegisterDTO {
private Long mobile;
private String imageCode;
private String smsCode;
private String password;
private Date createTime = new Date();
}
@Override
public void register(RegisterDTO registerDTO) {
//检验数据完整性
if (!StringUtils.hasLength(String.valueOf(registerDTO.getMobile())) || !StringUtils.hasLength(registerDTO.getPassword())) {
throw new RuntimeException("注册数据有问题或不完整");
}
//检验校验码是否正确,去redis中查找这个验证码
String smsCode = registerDTO.getSmsCode();
String smsVerifiyRedisKey = "sms" + registerDTO.getMobile();
AjaxResult ajaxResult = redisFeignClient.get(smsVerifiyRedisKey);
if (!ajaxResult.isSuccess() || ajaxResult.getResultObj() == null) {
throw new RuntimeException("查询数据不存在");
}
SmsSendDTO smsCodeFromRedis = JSON.parseObject(ajaxResult.getResultObj().toString(), SmsSendDTO.class);
if (!smsCodeFromRedis.getVerifiyRandomString().equals(smsCode)) {
throw new RuntimeException("你的验证码是不是输错啦!");
}
Sso sso = new Sso();
//保存手机号
sso.setPhone(String.valueOf(registerDTO.getMobile()));
Sso ssoFromSql = baseMapper.selectByPhone(sso.getPhone());
if (ssoFromSql != null) {
throw new RuntimeException("该用户以及被注册");
}
//保存创建时间
sso.setCreateTime(registerDTO.getCreateTime().getTime());
String finalPassword = MD5.getMD5(registerDTO.getPassword() + SaltConstant.SSO_SALT);
//保存加密后的密码
sso.setPassword(finalPassword);
//保存盐
sso.setSalt(SaltConstant.SSO_SALT);
//保存密码要加密,同时将盐保存到数据库,登录的时候才能根据这个盐来解密
baseMapper.insert(sso);
//保存base表
VipBase vipBase = new VipBase();
vipBase.setSsoId(sso.getId());
vipBase.setCreateTime(registerDTO.getCreateTime().getTime());
vipBase.setRegTime(registerDTO.getCreateTime().getTime());
vipBase.setRegChannel(1);
vipBaseMapper.insert(vipBase);
}
登录:
@Override
public String login(LoginDTO loginDTO) {
//1.判断数据有没有空
if (!StringUtils.hasLength(String.valueOf(loginDTO.getPhone())) || !StringUtils.hasLength(loginDTO.getPassword())) {
throw new RuntimeException("注册数据有问题或不完整");
}
//2.判断当前手机号是否已经存在
Long phone = loginDTO.getPhone();
Sso sso = baseMapper.selectByPhone(String.valueOf(phone));
if (sso == null) {
throw new RuntimeException("无此用户");
}
//3.通过当前手机号查到加密后的密码和盐,进行解密
String password = sso.getPassword();
String salt = sso.getSalt();
String passwordMD5 = MD5.getMD5(loginDTO.getPassword() + salt);
if (password == null) {
throw new RuntimeException("数据库数据有问题");
}
if (!password.equals(passwordMD5)) {
throw new RuntimeException("登录失败,密码不对");
}
//4.创建cookie,将登录信息保存到redis中
String token = UUID.randomUUID().toString();
AjaxResult ajaxResult = redisFeignClient.setExLogin(token, JSON.toJSONString(sso));
if (!ajaxResult.isSuccess()) {
throw new RuntimeException("登录失败");
}
return token;
}
登录后会返回token给前端,并设置到cookie中去:
submitLogin () {
this.$http.post("/user/sso/login", this.formParams).then(res => {
var ajaxResult = res.data;
if (ajaxResult.success) {
alert("登录成功");
var token = ajaxResult.resultObj;
document.cookie = "token=" + token;
//发送ajax请求到后端
this.$http.get("/user/sso/list", this.formParams).then(res => {
console.log(res);
});
window.location.href = "http://localhost:6003/user.home.html";
} else {
alert("登录失败:" + ajaxResult.message);
}
})
},
统一的前端拦截器:
axios.interceptors.request.use(config => {
//1.获取到token : token=d60e2caf-4576-42e5-bc87-0dcfdb9646f2
var token = document.cookie;
//2.把token设置到请求头
//如果已经登录了,每次都把token作为一个请求头传递过程
if (token && token.indexOf("token") > -1) {
//token=d60e2caf-4576-42e5-bc87-0dcfdb9646f2
token = token.split("=")[1];
// 让每个请求携带token--['token']为自定义key 请根据实际情况自行修改
config.headers['token'] = token;
}
return config
}, error => {
// Do something with request error
Promise.reject(error)
});
统一的后端拦截器:
@Component
public class LoginCheckFilter extends ZuulFilter {
@Autowired
private RedisFeignClient redisFeignClient ;
@Override
public String filterType() {
return "pre"; //前置Filter
}
@Override
public int filterOrder() {
return 1; //filter执行顺序
}
//返回的值boolean决定了要不要执行run方法
@Override
public boolean shouldFilter() {
//对于不需要做登录检查的请求:如: /login,/register 。。。就返回false
//1.通过Request对象获取url
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String requestURI = request.getRequestURI();
//2.判断url是否需要做登录检查
return !requestURI.endsWith("/login") && !requestURI.endsWith("/register");
}
//核心业务方法:登录检查
@Override
public Object run() throws ZuulException {
//1.获取到请求头中的Token
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletRequest request = currentContext.getRequest();
String token = request.getHeader("token");
//2.如果没有token,1.不要继续执行了 , 2.返回错误信息 AjaxResult
if(!StringUtils.hasLength(token)){
errorResponse();
return null;
}
//3.如果有token,取Redis中查询登录信息,集成Redis
AjaxResult ajaxResult = redisFeignClient.get(token);
if(!ajaxResult.isSuccess() || ajaxResult.getResultObj() == null){
//4.如果token在Redis中不存在,1.不要继续执行了 , 2.返回错误信息
errorResponse();
}
return null;
}
private void errorResponse(){
RequestContext currentContext = RequestContext.getCurrentContext();
HttpServletResponse response = currentContext.getResponse();
response.setContentType("application/json;charset=utf-8");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
//1.不要继续执行了 ,
currentContext.setSendZuulResponse(false);
// 2.返回错误信息 AjaxResult
AjaxResult ajaxResult = AjaxResult.me().setSuccess(false).setMessage("滚去登录");
try {
response.getWriter().print(JSON.toJSONString(ajaxResult));
} catch (IOException e) {
e.printStackTrace();
}
}
}
三.axios+jwt+shiro
jwt是目前最流行的跨域认证访问方案
jwt和token的关系:
- 都能使服务端无状态化
- 都能作为令牌
- 后者拿到令牌向服务端发起请求时,还会去数据库中(或者是redis)查token是否正确;而前者只需要将加密后的token发送过去,然后通过密匙解密,验证通过就能成功访问资源了,不需要存储token