微服务中架构下的鉴权


主要内容:

  1. 为什么使用token?
  2. 什么是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

客户端 服务端 后端拦截器 第一次登陆,发送账号和密码 UUID作为token,token作为key,登录实体作为value,封装到redis中 返回token 登录之后,客户端将token放到请求头中 之后的请求都会携带token 后端拦截器会检测 请求中的toke n,根据这个to ken看能不能从 redis中取出 登录实体 客户端 服务端 后端拦截器

注册:

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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值