在线教育项目用户登录和注册(三)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
简介: 在线教育项目用户登录和注册(三)

4. 用户注册

4.0 整体流程

  • 示意图

image.png

4.1 图片验证码

package com.czxy.zx.user.controller;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;
/**
 * Created by liangtong.
 */
@Controller
@RequestMapping("/verifycode")
public class VerifyCodeController {
    @Resource
    private StringRedisTemplate stringRedisTemplate;
    //   /verifycode/jack
    @GetMapping("/{username}")
    public void verifyCode(@PathVariable("username") String username , HttpServletResponse response ) throws IOException {
        //字体只显示大写,去掉了1,0,i,o几个容易混淆的字符
        String VERIFY_CODES = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
        int IMG_WIDTH = 72;
        int IMG_HEIGTH = 27;
        Random random = new Random();
        //创建图片
        BufferedImage image = new BufferedImage(IMG_WIDTH, IMG_HEIGTH, BufferedImage.TYPE_INT_RGB);
        //画板
        Graphics g = image.getGraphics();
        //填充背景
        g.setColor(Color.WHITE);
        g.fillRect(1,1,IMG_WIDTH-2,IMG_HEIGTH-2);
        g.setFont(new Font("楷体",Font.BOLD,25));
        StringBuilder sb = new StringBuilder();
        //写字
        for(int i = 1 ; i <= 4 ; i ++){
            //随机颜色
            g.setColor(new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)));
            int len = random.nextInt(VERIFY_CODES.length());
            String str = VERIFY_CODES.substring(len,len+1);
            sb.append(str);
            g.drawString(str, IMG_WIDTH / 6 * i , 22 );
        }
        System.out.println("验证码:" + sb.toString());
        // 将验证码保存redis中
        stringRedisTemplate.opsForValue().set("register" + username , sb.toString() );
        // 生成随机干扰线
        for (int i = 0; i < 30; i++) {
            //随机颜色
            g.setColor(new Color(random.nextInt(255),random.nextInt(255),random.nextInt(255)));
            int x = random.nextInt(IMG_WIDTH - 1);
            int y = random.nextInt(IMG_HEIGTH - 1);
            int x1 = random.nextInt(12) + 1;
            int y1 = random.nextInt(6) + 1;
            g.drawLine(x, y, x - x1, y - y1);
        }
        //响应到浏览器
        ImageIO.write(image,"jpeg", response.getOutputStream());
    }
}

4.2 后端实现

4.2.1 完善EduUser

package com.czxy.zx.domain;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import io.swagger.annotations.ApiModel;
import lombok.Data;
import java.util.Date;
/**
 *
 */
@Data
//@TableName("edu_user")
@ApiModel(value = "EduUser对象",description = "用户")
public class EduUser {
    @TableId(value="id" , type = IdType.AUTO)
    private Integer id;
    //用户名
    private String username;
    //用户密码
    private String password;
    //电话
    private String phone;
    //邮箱
    private String email;
    //角色,多个值使用逗号分隔,例如:admin,editor
    private String roles;
    //创建时间
    private Date created;
    //状态:0 未激活、1已激活
    private String status;
    @TableField(exist = false)
    private String verifycode;
    @TableField(exist = false)
    private String repassword;
}

4.2.2 service实现

  • service 接口
package com.czxy.zx.user.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.czxy.zx.domain.EduUser;
/**
 * @author 桐叔
 * @email liangtong@itcast.cn
 */
public interface EduUserService extends IService<EduUser> {
    boolean register(EduUser eduUser);
}

service实现类

package com.czxy.zx.user.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.czxy.zx.domain.EduUser;
import com.czxy.zx.user.mapper.EduUserMapper;
import com.czxy.zx.user.service.EduUserService;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Date;
/**
 * @author 桐叔
 * @email liangtong@itcast.cn
 */
@Service
@Transactional
public class EduUserServiceImpl extends ServiceImpl<EduUserMapper, EduUser> implements EduUserService {
    @Override
    public boolean register(EduUser eduUser) {
        //1 校验,用户名存在不允许注册
        QueryWrapper<EduUser> queryWrapper = new QueryWrapper();
        queryWrapper.eq("username", eduUser.getUsername());
        EduUser findUser = this.baseMapper.selectOne(queryWrapper);
        if(findUser != null) {
            throw new EduException("用户名已存在");
        }
        //2 自动生成数据
        eduUser.setCreated(new Date());    //创建时间
        eduUser.setStatus("0");            //登录状态
        //3 保存
        int insert = this.baseMapper.insert(eduUser);
        //4 提示
        return insert == 1;
    }
}

4.3.3 controller

/**
     * 注册功能
     * @param eduUser
     * @return
     */
    @PostMapping("/register")
    public BaseResult register(@RequestBody EduUser eduUser) {
        //1.1 校验验证码
        String redisVerifyCode = stringRedisTemplate.opsForValue().get("login" + eduUser.getUsername());
        stringRedisTemplate.delete("login" + eduUser.getUsername());
        if(redisVerifyCode == null) {
            return BaseResult.error("验证码无效");
        }
        if(! redisVerifyCode.equalsIgnoreCase(eduUser.getVerifycode())) {
            return BaseResult.error("验证码错误");
        }
        //1.2 密码校验
        if(eduUser.getPassword() == null) {
            return BaseResult.error("密码不能为空");
        }
        if(! eduUser.getPassword().equals(eduUser.getRepassword())) {
            return BaseResult.error("密码和确认密码不一致");
        }
        //2 注册
        boolean result = eduUserService.register(eduUser);
        //3 处理结果
        if(result) {
            // 成功
            // 3.1 生成UUID
            String uuid = UUID.randomUUID().toString().replace("-","");
            // 3.2 生成激活路由
            String url = "http://localhost:8080/active/"+eduUser.getUsername()+"/" + uuid;    //访问前端
            // 3.3 发送激活邮件
            String text = eduUser.getUsername() + "您好:<br/>您使用本网站的激活程序,请<a href='"+url+"'>点击激活</a>";
            // 3.4 发送邮件
            UserEmail userEmail = new UserEmail();
            userEmail.setUsername(eduUser.getUsername());
            userEmail.setEmail(eduUser.getEmail());
            userEmail.setText(text);
            String userEmailStr = JSONObject.toJSONString(userEmail);
            rabbitTemplate.convertAndSend(RabbitEmailConfig.QUEUE_NAME , userEmailStr);
            // 3.5 保存激活状态码
            stringRedisTemplate.opsForValue().set("active" + eduUser.getUsername() , uuid , 5 , TimeUnit.MINUTES);
            return BaseResult.ok("注册成功");
        }
        return BaseResult.ok("注册失败");
    }

4.3 前端实现

4.3.1 显示页面

  • 创建页面 @/views/edu/user/register.vue

image.png

<template>
  <div>
    注册
  </div>
</template>
<script>
export default {
}
</script>
<style>
</style>
  • 编写路由

image.png

  {
    path: '/register',
    component: () => import('@/views/edu/user/register'),
    hidden: true      //登录成功后,左侧菜单中不显示
  }
  • 修改登录页面

image.png

<el-button class="thirdparty-button" type="primary" style="right:80px;" @click="showDialog=true">
   三方登录
</el-button>
<el-button class="thirdparty-button" type="primary" @click="$router.push('/register')">
    注册
 </el-button>

将注册连接添加到白名单

image.png

4.3.2 前端 api

image.png

export function register(user) {
  // 真实数据
  return axios.post('/user-service/user/register',user);
}

4.3.3 注册页面

0e430b9529f14490addd1f88a5b02443.png

<template>
  <div class="login-container">
    <el-form ref="loginForm" :model="loginForm" class="login-form" >
      <div class="title-container">
        <h3 class="title">注册表单</h3>
      </div>
      <el-form-item prop="username">
        <span class="svg-container">
          <svg-icon icon-class="user" />
        </span>
        <el-input
          ref="username"
          v-model="loginForm.username"
          placeholder="用户名"
          type="text"
          tabindex="1"
          @blur="reload"
        />
      </el-form-item>
      <el-tooltip v-model="capsTooltip" content="Caps lock is On" placement="right" manual>
        <el-form-item prop="password">
          <span class="svg-container">
            <svg-icon icon-class="password" />
          </span>
          <el-input
            :key="passwordType"
            ref="password"
            v-model="loginForm.password"
            :type="passwordType"
            placeholder="密码"
            tabindex="2"
            autocomplete="on"
            @keyup.native="checkCapslock"
            @blur="capsTooltip = false"
            @keyup.enter.native="handleLogin"
          />
          <span class="show-pwd" @click="showPwd">
            <svg-icon :icon-class="passwordType === 'password' ? 'eye' : 'eye-open'" />
          </span>
        </el-form-item>
      </el-tooltip>
      <el-tooltip v-model="recapsTooltip" content="Caps lock is On" placement="right" manual>
        <el-form-item prop="repassword">
          <span class="svg-container">
            <svg-icon icon-class="password" />
          </span>
          <el-input
            :key="repasswordType"
            ref="repassword"
            v-model="loginForm.repassword"
            :type="repasswordType"
            placeholder="确认密码"
            tabindex="3"
            autocomplete="on"
            @keyup.native="recheckCapslock"
            @blur="recapsTooltip = false"
          />
          <span class="show-pwd" @click="reshowPwd">
            <svg-icon :icon-class="repasswordType === 'password' ? 'eye' : 'eye-open'" />
          </span>
        </el-form-item>
      </el-tooltip>
      <!-- 手机 -->
      <el-form-item prop="phone">
        <span class="svg-container">
          <svg-icon icon-class="wechat" />
        </span>
        <el-input
          ref="phone"
          v-model="loginForm.phone"
          placeholder="请输入手机号"
          name="phone"
          type="text"
          tabindex="4"
        />
      </el-form-item>
      <!-- 邮箱 -->
      <el-form-item prop="email">
        <span class="svg-container">
          <svg-icon icon-class="email" />
        </span>
        <el-input
          ref="email"
          v-model="loginForm.email"
          placeholder="请输入邮箱"
          name="email"
          type="text"
          tabindex="5"
        />
      </el-form-item>
      <!-- 验证码 -->
      <el-form-item prop="verifycode">
        <span class="svg-container">
          <svg-icon icon-class="guide" />
        </span>
        <el-input
          ref="verifycode"
          v-model="loginForm.verifycode"
          placeholder="请输入验证码"
          name="verifycode"
          type="text"
          tabindex="6"
          style="width:70%;"
        />
        <img :src="rerifycodeImg" @click="reload" alt="">
      </el-form-item>
      <el-button type="primary" style="width:100%;margin-bottom:30px;" @click.native.prevent="userRegister">注册</el-button>
    </el-form>
  </div>
</template>
<script>
export default {
  data() {
    return {
      loginForm: {
      },
      capsTooltip: false,
      passwordType: 'password',
      recapsTooltip: false,
      repasswordType: 'password',
      rerifycodeImg: '',
    }
  },
  methods: {
    showPwd() {
      if (this.passwordType === 'password') {
        this.passwordType = ''
      } else {
        this.passwordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.password.focus()
      })
    },
    checkCapslock(e) {
      const { key } = e
      this.capsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
    },
    reshowPwd() {
      if (this.repasswordType === 'password') {
        this.repasswordType = ''
      } else {
        this.repasswordType = 'password'
      }
      this.$nextTick(() => {
        this.$refs.repassword.focus()
      })
    },
    recheckCapslock(e) {
      const { key } = e
      this.recapsTooltip = key && key.length === 1 && (key >= 'A' && key <= 'Z')
    },
    reload() {
      //  "路径?t=" + new Date() ,提供一个t变量,用于唯一标识每一次访问路径
      this.rerifycodeImg = `http://localhost:10010/v2/user-service/verifycode/${this.loginForm.username}?t=` + new Date().getTime()
    },
    userRegister() {
    }
  },
}
</script>
<style lang="scss">
/* 修复input 背景不协调 和光标变色 */
/* Detail see https://github.com/PanJiaChen/vue-element-admin/pull/927 */
$bg:#283443;
$light_gray:#fff;
$cursor: #fff;
@supports (-webkit-mask: none) and (not (cater-color: $cursor)) {
  .login-container .el-input input {
    color: $cursor;
  }
}
/* reset element-ui css */
.login-container {
  .el-input {
    display: inline-block;
    height: 47px;
    width: 85%;
    input {
      background: transparent;
      border: 0px;
      -webkit-appearance: none;
      border-radius: 0px;
      padding: 12px 5px 12px 15px;
      color: $light_gray;
      height: 47px;
      caret-color: $cursor;
      &:-webkit-autofill {
        box-shadow: 0 0 0px 1000px $bg inset !important;
        -webkit-text-fill-color: $cursor !important;
      }
    }
  }
  .el-form-item {
    border: 1px solid rgba(255, 255, 255, 0.1);
    background: rgba(0, 0, 0, 0.1);
    border-radius: 5px;
    color: #454545;
  }
}
</style>
<style lang="scss" scoped>
$bg:#2d3a4b;
$dark_gray:#889aa4;
$light_gray:#eee;
.login-container {
  min-height: 100%;
  width: 100%;
  background-color: $bg;
  overflow: hidden;
  .login-form {
    position: relative;
    width: 520px;
    max-width: 100%;
    padding: 160px 35px 0;
    margin: 0 auto;
    overflow: hidden;
  }
  .tips {
    font-size: 14px;
    color: #fff;
    margin-bottom: 10px;
    span {
      &:first-of-type {
        margin-right: 16px;
      }
    }
  }
  .svg-container {
    padding: 6px 5px 6px 15px;
    color: $dark_gray;
    vertical-align: middle;
    width: 30px;
    display: inline-block;
  }
  .title-container {
    position: relative;
    .title {
      font-size: 26px;
      color: $light_gray;
      margin: 0px auto 40px auto;
      text-align: center;
      font-weight: bold;
    }
  }
  .show-pwd {
    position: absolute;
    right: 10px;
    top: 7px;
    font-size: 16px;
    color: $dark_gray;
    cursor: pointer;
    user-select: none;
  }
  .thirdparty-button {
    position: absolute;
    right: 0;
    bottom: 6px;
  }
  @media only screen and (max-width: 470px) {
    .thirdparty-button {
      display: none;
    }
  }
}
</style>
  • 获得 .env.development 文件中配置内容

image.png

   reload() {
      //this.rerifycodeImg = `http://localhost:10010/v2/user-service/verifycode/${this.loginForm.username}?t=${new Date().getTime()}`
      this.rerifycodeImg = `${process.env.VUE_APP_BASE_API}/user-service/verifycode/${this.loginForm.username}?t=${new Date().getTime()}`
    }

4.3.4 注册功能

image.png

  async userRegister() {
      let {message} = await register(this.loginForm)
      this.$message.success(message)
      //跳转到登录
      this.$router.push('/login')
    }

5 整合JWT

5.0 分析

image.png

5.1 搭建环境

5.1.1 拷贝坐标

<!--JavaBean工具类,用于JavaBean数据封装-->
        <dependency>
            <groupId>commons-beanutils</groupId>
            <artifactId>commons-beanutils</artifactId>
        </dependency>
        <!--jwt工具-->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
        </dependency>
        <!--joda 时间工具类 -->
        <dependency>
            <groupId>joda-time</groupId>
            <artifactId>joda-time</artifactId>
        </dependency>

5.1.2 复制yml配置

sc:
  jwt:
    secret: sc@Login(Auth}*^31)&czxy% # 登录校验的密钥
    pubKeyPath: D:/rsa/rsa.pub # 公钥地址
    priKeyPath: D:/rsa/rsa.pri # 私钥地址
    expire: 360 # 过期时间,单位分钟

5.1.3 拷贝配合类

  • 拷贝:JwtProperties
package com.czxy.zx.user.config;
import com.czxy.zx.user.utils.RsaUtils;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import javax.annotation.PostConstruct;
import java.io.File;
import java.security.PrivateKey;
import java.security.PublicKey;
/**
 * @author 桐叔
 * @email liangtong@itcast.cn
 */
@Configuration
@ConfigurationProperties(prefix = "sc.jwt")
@Data
public class JwtProperties {
    private String secret;
    private String pubKeyPath;
    private String priKeyPath;
    private Integer expire;
    private PublicKey publicKey;
    private PrivateKey privateKey;
    @PostConstruct      //初始化方法注解
    public void init() {
        try {
            File pubFile = new File(pubKeyPath);
            File priFile = new File(priKeyPath);
            if(!pubFile.exists() || ! priFile.exists()) {
                RsaUtils.generateKey(pubKeyPath,priKeyPath,secret);
            }
            // 获得公钥和私钥对象
            this.publicKey = RsaUtils.getPublicKey(pubKeyPath);
            this.privateKey = RsaUtils.getPrivateKey(priKeyPath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

5.1.4 拷贝工具类

  • 拷贝工具类

image.png

5.1.5 生成秘钥(可选)

  • 如果已经存在,此步省略

image.png

编写测试类,生成公钥和私钥

package com.czxy.zx.utils;
/**
 * @author 桐叔
 * @email liangtong@itcast.cn
 */
public class TestRsa {
    //公钥的位置
    private static final String pubKeyPath = "D:\\rsa\\rsa.pub";
    //私钥的位置
    private static final String priKeyPath = "D:\\rsa\\rsa.pri";
    public static void main(String[] args) throws Exception {
        RsaUtils.generateKey(pubKeyPath,priKeyPath,"1234");
    }
}
相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
Web App开发 移动开发 前端开发
前端企业微信服务商第三方应用开发详情流程
前端企业微信服务商第三方应用开发详情流程
386 0
|
5月前
|
存储 NoSQL 中间件
【Django+Vue3 线上教育平台项目实战】登录功能模块之短信登录与钉钉三方登录
在当今的数字化时代,用户认证是任何在线服务安全性的基石。本文将简明扼要地介绍登录注册流程中的核心概念:HTTP无状态性、Session、Token与JWT,并详细阐述两种实用登录方式—— 手机号登录验证(借助容联云/云通讯服务) 与钉钉第三方登录。我们将探讨这些概念的基本原理,并深入解析两种登录方式的实现流程,旨在帮助开发者提升用户认证的安全性与便捷性。
【Django+Vue3 线上教育平台项目实战】登录功能模块之短信登录与钉钉三方登录
|
小程序 前端开发 安全
微信小程序OA会议系统个人中心授权登入
微信小程序OA会议系统个人中心授权登入
72 0
|
7月前
|
前端开发 NoSQL 数据库
设计 QQ、微信等第三方账号登陆
设计 QQ、微信等第三方账号登陆
69 0
设计 QQ、微信等第三方账号登陆
|
前端开发
HTML+CSS实现小米账号注册界面
HTML+CSS实现小米账号注册界面
|
JSON 前端开发 NoSQL
淘东电商项目(27) -门户登出功能
淘东电商项目(27) -门户登出功能
52 0
|
XML 缓存 NoSQL
手把手实现第三方社交登录方式微信登录
手把手实现第三方社交登录方式微信登录
176 0
|
存储 安全 Java
java实现社交账号登录
java实现社交账号登录
90 0
|
SQL 缓存 前端开发
用户登录【项目 商城】
用户登录【项目 商城】
144 0
|
JSON JavaScript 前端开发
前后端分离项目知识汇总(整合短信登录和微信扫描登录)
前后端分离项目知识汇总(整合短信登录和微信扫描登录)
208 0