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

本文涉及的产品
云原生内存数据库 Tair,内存型 2GB
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Redis 版,经济版 1GB 1个月
简介: 在线教育项目用户登录和注册(三)

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
相关文章
|
JavaScript Java 数据库
企业微信接入系列-扫码绑定/登录
讲述在企业后台管理平台账号绑定企业微信以及企业微信扫码登录企业管理平台
企业微信接入系列-扫码绑定/登录
|
7月前
|
NoSQL 前端开发 数据库
淘东电商项目(36) -SSO单点登录(退出功能)
淘东电商项目(36) -SSO单点登录(退出功能)
34 0
|
7月前
|
JSON 前端开发 NoSQL
淘东电商项目(27) -门户登出功能
淘东电商项目(27) -门户登出功能
30 0
|
7月前
|
前端开发 NoSQL 数据库
淘东电商项目(26) -门户登录功能
淘东电商项目(26) -门户登录功能
25 0
|
7月前
|
前端开发
淘东电商项目(33) -SSO单点登录(改造SSO认证服务登录界面)
淘东电商项目(33) -SSO单点登录(改造SSO认证服务登录界面)
44 0
|
7月前
|
移动开发 NoSQL Redis
淘东电商项目(35) -SSO单点登录(登录功能完善)
淘东电商项目(35) -SSO单点登录(登录功能完善)
55 0
|
7月前
|
前端开发 NoSQL 数据库
淘东电商项目(25) -门户注册功能
淘东电商项目(25) -门户注册功能
26 0
|
9月前
|
XML 缓存 NoSQL
手把手实现第三方社交登录方式微信登录
手把手实现第三方社交登录方式微信登录
118 0
|
10月前
|
存储 小程序 数据库
云开发(微信-小程序)笔记(十三)---- 注册登陆
云开发(微信-小程序)笔记(十三)---- 注册登陆
90 0
云开发(微信-小程序)笔记(十三)---- 注册登陆
|
10月前
|
SQL 缓存 前端开发
用户登录【项目 商城】
用户登录【项目 商城】
115 0