6网页前台登录模块

六网页前台登录模块

1具体功能

1.1注册

  • 前端输入用户名,密码,手机号
  • 点击发送验证码按钮,调用后台接口
    • 使用阿里云短信sdk,发送验证码
    • 将验证码保存到redis中,key为手机号,value为验证码,时间可以为5分钟
    • 五分钟内再次点击前端接口,直接从redis中查询并返回验证码
    • 超过五分钟,再起调用阿里云短信sdk,重新发送
  • 前端点击确认注册,调用后台接口
    • 获取注册的数据
    • 手机号密码非空判断
    • 从redis中根据手机号判断验证码是否正确
    • 判断手机号是否重复
    • 将用户名,手机号,密码保存到数据库(注意密码md5加密)

1.2登录

  • 前端输入用户名,密码,手机号
  • 点击确认登录,调用后台接口
    • 获取登录手机号和密码
    • 手机号非空以及正确性判断
    • 密码非空以及正确性判断
      • 把输入的密码进行加密,再和数据库密码进行比较
      • 加密方式 MD5
    • 根据用户名,手机号生成token字符串,使用jwt工具类
      • 设置头部分,过期时间,主体部分,签名哈希
    • 返回token给前端
  • 前端调用接口后
    • 前端调用登录接口获得返回的token字符串
    • 把返回的token字符串放到cookie里面
    • 根据token值,调用后端接口,返回用户Id,再根据用户Id返回用户信息,前端得到用户信息
    • 将获取到的用户信息,放到cookie里面(注意两次cookie的key不一样)
    • 在首页面显示用户信息
      • 在渲染前从cookie中获得用户信息
      • 从cookie中取到的用户信息是字符串 ,变成json就能正常使用了
    • 退出时清除cookie里面的两个key就行了
  • 前端拦截器
    • 为了每次请求方便用request拦截器来使token自动增加到请求头

2具体知识点

2.1登录方式简介

  • 单一服务器模式

    • 使用session对象实现
      • 登录成功之后,把用户数据放到session里面
      • 判断是否登录,从session获取数据,可以获取到登录
      • session.setAttribute("user",user);
      • session.getAttribute("user");
    • 但是微服务模式下不能用session模式,用的是单点模式
    • 就是在一个模块登录了 其他模块都不用登录了 ,就像百度 有很多产品 我在贴吧里面登录 再去百度文库就不需要登录 这就是单点登录
  • 单点登录三种常见方式

    • a.session广播机制实现

      • 简单来说就是session复制
      • 有一个致命的缺点 就是项目中有几十个模块 就要把session复制好多次 不适合特别多的模块
    • b.使用cookie+redis实现

      • 项目中任何一个模块进行登录,登录之后把数据放到两个地方
        • redis,在key中生成一个唯一随机值(ip,用户id等等),在value中存用户数据
        • cookie,把redis里面生成key值放到cookie里面
      • 访问项目中其他模块,发送请求带着cookie进行发送,获取cookie值,拿着cookie做事情
        • 把cookie获取值,到redis进行查询,根据key进行查询,如果查询数据就是登录
    • c.使用token实现

      • token是按照一定规则生成字符串(规则自己约定),字符串里面包含用户信息
      • 在某个项目登录后 按照规则(官方规则JWT)生成字符串,把字符串返回;可以把字符串通过cookie返回也可以把字符串通过地址栏返回;
      • 再去访问其他模块时,地址栏都带着生成的字符串,在访问模块里面获取地址栏字符串,根据字符串获取用户信息;如果可以获取到就是登陆
  • 细节

    • session有默认的30分钟过期时间
    • 第二种和第三种方法也能做到过期
    • 第二种就是设置redis过期时间 第三种token生成时候也能设计过期时间

2.2JWT简介

  • jwt介绍

    • token是按照一定规则生成字符串,包括用户信息,规则是怎么样的,不一定
    • 一般采用通用的规则,官方规则JWT
    • JWT就是给我们规定好了规则,使用jwt规则可以生成字符串,包括用户信息
  • jwt组成

    • 第一部分:jwt头信息(公共部分)

    • 第二部分:有效载荷,包含主体信息(用户信息)(私有部分)

    • 第三部分:签名哈希,防伪标签(签名部分)

  • jwt头

    • jwt头部分是一个描述jwt元数据的json对象,通常如下所示

    • {
          "alg":"HS256",
          “typ":"JWT"
      }
      
    • 在上面的代码中,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256)

    • typ属性表示令牌的类型,jwt令牌统一写为JWT

    • 最后使用Base64 URL算法将上述json对象转换为字符串保存

  • 有效载荷

    • 有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择

    • 	iss:发行人
      	exp:到期时间
      	sub:主题
      	aud:用户
      	nbf:在此之前不可用
      	iat:发布时间
      	jti:JWT ID用于标识该JWT
      
    • 除以上默认字段外,我们还可以自定义私有字段,如下例:

    • {
      "sub": "1234567890",
      "name": "Helen",
      "admin": true
      }
      
    • JSON对象也使用Base64 URL算法转换为字符串保存

  • 签名哈希

    • 签名哈希部分是对上面两部分数据签名,通过指定的算法生成哈希,以确保数据不会被篡改

    • 首先,需要指定一个密码(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用标头中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名

    • HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(claims), secret)
      
    • 在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用"."分隔,就构成整个JWT对象

  • Base64URL算法

    • 如前所述,JWT头和有效载荷序列化的算法都用到了Base64URL。该算法和常见Base64算法类似,稍有差别
    • 作为令牌的JWT可以放在URL中(例如api.example/?token=xxx)。 Base64中用的三个字符
      是"+","/"和"=",由于在URL中有特殊含义,因此Base64URL中对他们做了替换:"="去掉,"+"用"-"替换,"/"用"_"替换,这就是Base64URL算法
    • base64编码,并不是加密,只是把明文信息变成了不可见的字符串。但是其实只要用一些工具就可以把base64编码解成明文,所以不要在JWT中放入涉及私密的信息。

2.3JWT使用

  • 引入依赖

  • 使用JWT工具类

    • 第一部分:常量(token过期时间,秘钥)

      •     //常量
            public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
            public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO"; //秘钥 这边随便写就行 实际肯定是按照公司规则生成
        
    • 第二部分:根据Id生成token字符串的方法

      • 	//生成token字符串的方法
        //一般就是用户id 和name来生成token
        public static String getJwtToken(String id, String nickname){
          
            String JwtToken = Jwts.builder()
                    //头部分
                    .setHeaderParam("typ", "JWT")
                    .setHeaderParam("alg", "HS256")
          
                    //设置过期时间
                    .setSubject("guli-user")
                    .setIssuedAt(new Date())
                    .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
          
                    //设置token主体部分 ,存储用户信息
                    .claim("id", id)
                    .claim("nickname", nickname)
          
                    //签名哈希
                    .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                    .compact();
          
            return JwtToken;
        }
        
    • 第三部分: 判断token是否存在与有效

      • 	// 判断token是否存在与有效
           
            public static boolean checkToken(String jwtToken) {
                if(StringUtils.isEmpty(jwtToken)) return false;
                try {
                    Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
                } catch (Exception e) {
                    e.printStackTrace();
                    return false;
                }
                return true;
            }
        
    • 第四部分:根据token字符串获取会员id

      • public static String getMemberIdByJwtToken(HttpServletRequest request) {
               String jwtToken = request.getHeader("token");
               if(StringUtils.isEmpty(jwtToken)) return "";
               Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
               Claims claims = claimsJws.getBody();
               return (String)claims.get("id");
           }
        

2.4阿里云短信(只有注册需要用到,和登录无关)

  • 开通阿里云短信服务

    • 使用用测试api
  • 编写代码实现短信发送

    • 直接复制官方demo

    • @Service
      public class MsmServiceImpl implements MsmService {
      
          /**
           * 使用AK&SK初始化账号Client
           *
           * @param accessKeyId
           * @param accessKeySecret
           * @return Client
           * @throws Exception
           */
          public static com.aliyun.dysmsapi20170525.Client createClient(String accessKeyId, String accessKeySecret) throws Exception {
              Config config = new Config()
                      // 您的AccessKey ID
                      .setAccessKeyId("xxxxx")
                      // 您的AccessKey Secret
                      .setAccessKeySecret("Oxxx");
              // 访问的域名
              config.endpoint = "dysmsapi.aliyuncs.com";
              return new com.aliyun.dysmsapi20170525.Client(config);
          }
      
          //发送短信的方法
          @Override
          public boolean send(Map<String, Object> param, String phone)  {
      
              com.aliyun.dysmsapi20170525.Client client = null;
      
              try {
                  client = MsmServiceImpl.createClient("accessKeyId", "accessKeySecret");
              } catch (Exception e) {
                  e.printStackTrace();
                  return false;
              }
              SendSmsRequest sendSmsRequest = new SendSmsRequest()
                      .setSignName("阿里云短信测试")
                      .setTemplateCode("xxxx909")
                      .setPhoneNumbers("xxxxxx")
                      .setTemplateParam("{\"code\":\"1234\"}");
      
      
              //最终发送
              try {
                  SendSmsResponse sendSmsResponse = client.sendSms(sendSmsRequest);
                  return true;
              } catch (Exception e) {
                  e.printStackTrace();
                  return false;
              }
          }
      }
      
  • redis解决验证码有效时间问题

    • 这样5分钟内再次点击就会到redis去查

    • 如果超时了就再次发送

    •     //发送短信的方法
          @GetMapping("send/{phone}")
          public R sendMsm(@PathVariable String phone) throws Exception {
              //1 从redis获取验证码,如果获取到直接返回
              String code = redisTemplate.opsForValue().get(phone);
              if(!StringUtils.isEmpty(code)) {
                  return R.ok();
              }
              //2 如果redis获取 不到,进行阿里云发送
              //生成随机值,传递阿里云进行发送(由于是测试账号只能1234 这边是模拟正式场景)
              //注意randomutil是老师的工具类
              code = RandomUtil.getFourBitRandom();
              Map<String,Object> param = new HashMap<>();
              param.put("code",code);
              //调用service发送短信的方法(就是上面复制的代码)
              boolean isSend = msmService.send(param,phone);
              if(isSend) {
                  //发送成功,把发送成功验证码放到redis里面
                  //设置有效时间
                  redisTemplate.opsForValue().set(phone,code,5, TimeUnit.MINUTES);
                  return R.ok();
              } else {
                  return R.error().message("短信发送失败");
              }
          }
      

2.5前台登录模块完整

  • 在service下面创建子模块service_ucenter

  • 编写实体类

    • 用户UcenterMember(主要字段)
      • 用户id
      • 用户名
      • 微信openid
      • 手机号
      • 密码
      • 性别
      • 年龄
      • 用户签名
    • RegisterVo
      • 用户名
      • 手机号
      • 密码
      • 验证码
  • 注册流程

    • controller

    • 	//注册
          @PostMapping("register")
          public R registerUser(@RequestBody RegisterVo registerVo) {
              memberService.register(registerVo);
              return R.ok();
          }
      
    • service

    • 注意密码加密使用md5加密密码(有工具类)

    •     //注册的方法
          @Override
          public void register(RegisterVo registerVo) {
              //1获取注册的数据
              String code = registerVo.getCode(); //验证码
              String mobile = registerVo.getMobile(); //手机号
              String nickname = registerVo.getNickname(); //昵称
              String password = registerVo.getPassword(); //密码
      
              //2非空判断
              if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)
                      || StringUtils.isEmpty(code) || StringUtils.isEmpty(nickname)) {
                  throw new GuliException(20001,"注册失败");
              }
              //3判断验证码(注意之前以及获得过阿里云的短信验证码了)
              //获取redis验证码
              String redisCode = redisTemplate.opsForValue().get(mobile);
              if(!code.equals("1234")){//真实业务就是!code.equals(redissCode)
                  if(!code.equals(redisCode)) {
                      throw new GuliException(20001,"注册失败");
                  }
              }
      
      
              //4判断手机号是否重复,表里面存在相同手机号不进行添加
              QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
              wrapper.eq("mobile",mobile);
              Integer count = baseMapper.selectCount(wrapper);
              if(count > 0) {
                  throw new GuliException(20001,"注册失败");
              }
      
              //5数据添加数据库中
              UcenterMember member = new UcenterMember();
              member.setMobile(mobile);
              member.setNickname(nickname);
              member.setPassword(MD5.encrypt(password));//密码需要加密的
              member.setIsDisabled(false);//用户不禁用
              member.setAvatar("xxxxx.jpeg");
              baseMapper.insert(member);
          }
      
  • 登录流程

    • controller

    • //登录
       @PostMapping("login")
       public R loginUser(@RequestBody UcenterMember member) {
           //member对象封装手机号和密码
           //调用service方法实现登录
           //返回token值,使用jwt生成
           String token = memberService.login(member);
           return R.ok().data("token",token);
       }
      
    • service

    • //登录的方法
        @Override
        public String login(UcenterMember member) {
            //1获取登录手机号和密码
            String mobile = member.getMobile();
            String password = member.getPassword();
        
            //2手机号和密码非空以及准确性判断
            //手机号和密码非空判断
            if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
                throw new GuliException(20001,"登录失败");
            }
        
            //判断手机号是否正确
            QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
            wrapper.eq("mobile",mobile);
            UcenterMember mobileMember = baseMapper.selectOne(wrapper);
            //判断查询对象是否为空
            if(mobileMember == null) {//没有这个手机号
                throw new GuliException(20001,"登录失败");
            }
        
            //判断密码
            //因为存储到数据库密码肯定加密的
            //把输入的密码进行加密,再和数据库密码进行比较
            //加密方式 MD5
            if(!MD5.encrypt(password).equals(mobileMember.getPassword())) {
                throw new GuliException(20001,"登录失败");
            }
      
          //3登录成功生成token字符串返回
          //使用jwt工具类
          String jwtToken = JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
          return jwtToken;
      }
      
      
      

2.6注册和登录成功之后首页面显示数据全过程

  • 第一步:前端调用登录接口返回token字符串

  • 第二步:把第一步返回token字符串放到cookie里面(前端)

    • //第一个参数cookie名称,第二个参数值,第三个参数作用范围
                  cookie.set('fao_token',response.data.data.token,{domain: 'localhost'})
      
  • 第三步:根据token值,调用后端接口,返回用户Id,再根据用户Id返回用户信息用于前台显示

    • //后端接口
      //根据token获取用户信息
      @GetMapping("getMemberInfo")
      public R getMemberInfo(HttpServletRequest request) {
          //调用jwt工具类的方法。根据request对象获取头信息,返回用户id
          String memberId = JwtUtils.getMemberIdByJwtToken(request);
          //查询数据库根据用户id获取用户信息
          UcenterMember member = memberService.getById(memberId);
          return R.ok().data("userInfo",member);
      }
      
  • 第四步:获取返回用户信息,放到cookie里面

    •     //第四步获取返回用户信息,放到cookie里面
              cookie.set('fao_ucenter',this.loginInfo,{domain: 'localhost'})
      
  • 第五步:在首页面显示用户信息

    • 从cookie中取到的用户信息是字符串 我们要变成json

    • created() {
         this.showInfo();
       },
       methods: {
         //创建方法,从cookie获取用户信息
         showInfo() {
           //从cookie获取用户信息
           var userStr = cookie.get("guli_ucenter");
           // 把字符串转换json对象(js对象)
           if (userStr) {
             console.log(userStr);
             this.loginInfo = JSON.parse(userStr);
           }
         }
       }
      
    • 注意 后端都是字符串 前端才是json 后端是没有这个概念的

  • 第六步:退出

    • 就是清除cookie

    • //退出
         logout() {
           //清空cookie值
           cookie.set("guli_token", "", { domain: "localhost" });
           cookie.set("guli_ucenter", "", { domain: "localhost" });
           //回到首页面
           window.location.href = "/";
         }
      

2.7前端请求拦截器

  • 简介

    • 在前后端交互时,经常会使用到token
    • 上面首次请求后台把token返回给前端,已经把这个token可以保存到cookie里面了
    • 之后每次请求后端都需要带上这个token 进行鉴权验证,太麻烦
    • 这时候就可以用拦截器来使token自动增加
  • 创建前端拦截器,判断cookie里面是否有token字符串

    • // http request 拦截器
      service.interceptors.request.use(
        config => {
          //debugger
          //判断cookie里面是否有名称为fao_token数据
          if (cookie.get('fao_token')) {
            //如果有,把获取cookie值自动放到header(请求头)里面
            config.headers['token'] = cookie.get('fao_token');
          }
          return config
        },
        err => {
          return Promise.reject(err);
        })
      
posted @ 2022-07-14 15:53  fao99  阅读(595)  评论(0)    收藏  举报