前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >SpringBoot项目整合Vue做一个完整的用户注册功能

SpringBoot项目整合Vue做一个完整的用户注册功能

作者头像
用户3587585
发布2022-09-21 07:16:02
1.2K0
发布2022-09-21 07:16:02
举报
文章被收录于专栏:阿福谈Web编程阿福谈Web编程

引言

用户注册功能是每一个系统的入口门面功能,很多人可能会以为很简单,不就是一个简单的CRUD吗?其实不然,要把前后端功能都做出来,页面跳转也没问题,还真不简单。这次笔者做这么一个看似简单的用户注册功能就花了足足两天多时间,中间调试和解决Bug也花了好长时间。这次我就把自己做出的完整功能的实现过程作了一个提炼分享到我的公众号上来。希望有需要了解如何实现用户注册完整过程的读者朋友能够仔细看一看。

说明:本文前后端代码的实现分别在本人之前二次开发的开源项目vue-element-adminvueblog两个项目的基础上进行

实现用户注册流程

用户注册完整流程

用户注册信息及校验

后台接口设计

上传头像接口

接口url

http://localhost:8081/blog/upload/user/avatar

请求类型

POST

接口入参

参数名称

参数类型

是否必传

备注

file

MultipartFile

多媒体图片文件

接口出参

参数名称

参数类型

示例值

备注

status

Integer

200

状态码

msg

String

success

响应信息

data

String

https://vueblog2022.oss-cn-shenzhen.aliyuncs.com/avatar/63be8be25fee4c0f8df679238435d8d2.png

图片下载地址

用户注册接口

接口url

http:localhost//:8081/blog/user/reg

请求类型

POST

接口入参

参数名称

参数类型

是否必填

备注

username

String

用户账号

nickname

String

用户昵称

password

String

用户登录密码

userface

String

用户头像链接地址

phoneNum

Long

用户手机号码

email

String

用户邮箱地址

接口出参

参数名称

参数类型

示例值

备注

status

Integer

200

响应码

msg

String

注册成功

响应消息

data

Integer

0

注册成功标识

后端代码实现

用户头像上传接口编码实现

文件上传,这里选用了阿里云的对象存储,需要先开通阿里云对象存储服务,关于如何开通阿里云短信服务并将阿里云对象存储服务集成到SpringBoot项目中,请参考我之前发布的文章SpringBoot项目集成阿里云对象存储服务实现文件上传

服务层编码

新建OssClientService类继承阿里云对象存储服务SDK完成图片上传功能

代码语言:javascript
复制

@Service
public class OssClientService {

    @Resource
    private OssProperties ossProperties;

    private static final Logger logger =  LoggerFactory.getLogger(OssClientService.class);

    public String uploadFile(MultipartFile file){
        // 创建OSSClient实例。
        OSS ossClient = new OSSClientBuilder().build(ossProperties.getEndPoint(), ossProperties.getAccessKey(),
                ossProperties.getSecretKey());
        String uuid = UUID.randomUUID().toString().replaceAll("-", "");
        String objectName = "avatar/" + uuid + ".png";
        String imageUrl = null;
        try {
            InputStream inputStream =  file.getInputStream();  
            ossClient.putObject(ossProperties.getBucketName(), objectName, inputStream);
            imageUrl = "https://" + ossProperties.getBucketName() + "." + ossProperties.getEndPoint() + "/" + objectName;
        } catch (OSSException oe) {
            logger.error("Caught an OSSException, which means your request made it to OSS, but was rejected with an error response for some reason.");
            logger.error("Error Message:" + oe.getErrorMessage());
            logger.error("Error Code:" + oe.getErrorCode());
            logger.error("RequestId: " + oe.getRequestId());
            logger.error("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            logger.error("Caught an ClientException, which means the client encountered a serious internal problem " +
                    "while trying to communicate with OSS,such as not being able to access the network");
            logger.error("Error Message:" + ce.getErrorMessage());
        } catch (FileNotFoundException fe) {
            logger.error("file not found exception");
            logger.error("Error Message:" + fe.getMessage(), fe);
        } catch (IOException exception){
            logger.error("file get input stream error, caused by " + exception.getMessage(), exception);
        }
        finally {
            if (ossClient!=null) {
                ossClient.shutdown();
            }
        }
        return imageUrl;
    }
}

注意:升级到3.9.1版本后的aliyun-sdk-oss需要在每次上传文件时新建一个OSS实例, 上传完文件之后再调用shutdown方法关闭这个实例

控制器层编码

新建UploadFileController类完成从前端接收附件参数,并调用OssClientService服务实现图片上传

代码语言:javascript
复制
@RestController
@RequestMapping("/upload")
public class UploadFileController {

    @Resource
    private OssClientService ossClientService;

    @PostMapping("/user/avatar")
    @ApiOperation(value = "userAvatar", notes = "用户上传头像接口",
    produces = "application/octet-stream", consumes = "application/json")
    public RespBean uploadUserAvatar(HttpServletRequest request){
        MultipartHttpServletRequest multipartRequest = (MultipartHttpServletRequest) request;
        // 获取上传文件对象
        MultipartFile file = multipartRequest.getFile("file");
        RespBean respBean = new RespBean();
        String downloadUrl = ossClientService.uploadFile(file);
        if (!StringUtils.isEmpty(downloadUrl)) {
            respBean.setStatus(200);
            respBean.setMsg("success");
            respBean.setData(downloadUrl);
        } else {
            respBean.setStatus(500);
            respBean.setMsg("upload file failed");
        }
        return respBean;
    }
}

用户注册接口编码实现

数据库访问层编码

UserMapper接口类中新增注册用户抽象方法

代码语言:javascript
复制
int registerUser(UserDTO user);

然后在UserMapper.xml文件中完成用户数据入库sql编写

代码语言:javascript
复制
<insert id="registerUser" useGeneratedKeys="true" keyProperty="id" parameterType="org.sang.pojo.dto.UserDTO">
        INSERT INTO user(username, nickname, password, phoneNum,email, userface, regTime,enabled)
        values(#{username,jdbcType=VARCHAR},#{nickname,jdbcType=VARCHAR},
        #{password,jdbcType=VARCHAR}, #{phoneNum,jdbcType=BIGINT}, #{email,jdbcType=VARCHAR},
        #{userface,jdbcType=VARCHAR},now(),1)
    </insert>

服务层编码

CustomUserDetailsService接口类中添加注册用户抽象方法

代码语言:javascript
复制
int registerUser(UserDTO user);

然后在 CustomUserDetailsService接口类的实现类UserService类中完成用户注册逻辑

代码语言:javascript
复制
    @Override
    public int registerUser(UserDTO user) {
        // 判断用户是否重复注册
        UserDTO userDTO  = userMapper.loadUserByUsername(user.getUsername());
        if (userDTO != null) {
            return 1;
        }
        //插入用户, 插入之前先对密码进行加密
        user.setPassword(passwordEncoder.encode(user.getPassword()));
        user.setEnabled(1);//用户可用
        int result = userMapper.registerUser(user);
        //配置用户的角色,默认都是普通用户
        List<Integer> roleIds = Arrays.asList(2);
        int i = rolesMapper.setUserRoles(roleIds, user.getId());
        boolean b = i == roleIds.size() && result == 1;
        if (b) {
            // 注册成功
            return 0;
        } else {
            // 注册失败
            return 2;
        }
    }

控制器层编码

LoginRegController类中完成用户登录接口从前端接收参数到调用UserService服务类完成用户注册业务

代码语言:javascript
复制
    @PostMapping("/user/reg")
    @ApiOperation(value = "reg", notes = "用户注册", produces = "application/json",
            consumes = "application/json", response = RespBean.class)
    public RespBean reg(@RequestBody UserDTO user) {
        int result = userService.registerUser(user);
        if (result == 0) {
            //成功
            return new RespBean(ResponseStateConstant.SERVER_SUCCESS, "注册成功!");
        } else if (result == 1) {
            return new RespBean(ResponseStateConstant.DUPLICATE_ERROR, "用户名重复,注册失败!");
        } else {
            //失败
            return new RespBean(ResponseStateConstant.SERVER_ERROR, "注册失败!");
        }
    }

由于以上两个接口都是需要放开权限控制的,因此完成以上两个接口的编码后还需要在security配置类WebSecurityConfig类中支持匿名访问

只需要在configure(HttpSecurity http)方法中添加如下几行代码即可

代码语言:javascript
复制
http.authorizeRequests()
                .antMatchers("/user/reg").anonymous()
                .antMatchers("/upload/user/avatar").anonymous()

完成后端编码后可以启动Mysql服务和redis服务,然后运行BlogserverApplication类中的Main方法成功后就可以通过postman工具测试接口了

前端代码实现

完成用户注册界面vue组件编码

src/views目录下新建register文件夹,然后在register目录下新建index.vue文件

完成用户注册组件编码

这里的文件上传选择了element-ui组件库中的upload组件

代码语言:javascript
复制
  <template>
    <div class="register-container">
        <el-form :model="registerModel" :rules="rules" ref="registerForm" label-width="100px" class="register-form">
            <el-form-item label="用户账号" prop="userAccount" required>
                <el-input 
                  v-model="registerModel.userAccount"
                  placeholder="请输入用户名"/>
            </el-form-item>
            <el-form-item label="用户昵称" prop="nickName" required>
                <el-input 
                  v-model="registerModel.nickName"
                  type="text"
                  placeholder="请输入用户昵称"/>
            </el-form-item>
            <el-form-item label="登录密码" prop="password" required>
                <el-input 
                  v-model="registerModel.password" 
                  type="password"
                  placeholder="请输入密码"
                  suffix-icon="el-icon-lock"/>
            </el-form-item>
            <el-form-item label="确认密码" prop="password2" required>
                <el-input 
                  v-model="registerModel.password2"
                  type="password"
                  :show-password="false"  
                  placeholder="请再次输入密码"
                  suffix-icon="el-icon-lock" />
            </el-form-item>
            <el-form-item label="头像">
                <el-upload class="avatar-uploader"
                    :show-file-list="false"
                    accept="image"
                    :action="uploadAvatarUrl"
                    :on-preview="previewAvatar" 
                    :before-upload="beforeAvartarUpload"
                    :on-success="handleSuccessAvatar"
                >   
                    <img v-if="avatarUrl" :src="avatarUrl" class="avatar" />
                    <div v-else class="upload-btn" >
                        <el-button>点击上传头像</el-button>
                        <div slot="tip" class="el-upload__tip">只能上传jpg/png文件,且不超过10M</div>
                    </div>
                </el-upload>
            </el-form-item>
            <el-form-item label="手机号" prop="phoneNum" required>
                <el-input type="tel" 
                v-model="registerModel.phoneNum"
                placeholder="请输入手机号" 
                />
            </el-form-item>
            <el-form-item label="邮箱" prop="email">
                <el-input type="email" 
                v-model="registerModel.email"
                placeholder="请输入你的邮箱" />
            </el-form-item>
            <el-form-item class="btn-area">
               <el-button class="submit-btn" type="primary" :loading="onLoading"  @click="handleRegister('registerForm')">提交</el-button>
               <el-button class="reset-btn" type="info" @click="resetForm('registerForm')">重置</el-button> 
            </el-form-item>
        </el-form>
    </div>
</template>

<script>
import { Message } from 'element-ui'
import { isNumber, validatePhoneNum, validatePassword, validEmail } from '@/utils/validate'
export default {
    name: 'register',
    data(){
        // 密码校验器
        const passwordValidator = (rule,value, callback) =>{
            console.log(rule)
            if(!validatePassword(value)){
                callback('密码强度不满足要求,密码必须同时包含字母、数字和特殊字符,请重新输入')
            } else {
                callback()
            }
        }
        // 二次密码校验器
        const password2Validator = (rule, value, callback) => {
            console.log(rule)
            const password = this.registerModel.password
            if(password!=value){
                callback(new Error('两次输入的密码不一致'))
            } else {
                callback()
            }
        }
        // 手机号码校验器
       const  phoneNumValidator = (rule, value, callback)=> {
             console.log(rule)
            if(!(value.length==11 && isNumber(value))){
                callback(new Error('手机号码必须是11位数字'))
            } else if(!validatePhoneNum(parseInt(value))){
                callback(new Error('手机号码不合法'))
            } else {
                callback()
            }
       }
       // 邮件地址校验器
       const emailValidator = (rule, value, callback) => {
          console.log(rule)
          if(value!='' && !validEmail(value)){
             callback(new Error('邮箱地址不合法'))
          } else {
            callback()
          }
       }
        // 区分本地开发环境和生产环境
       let uploadAvatarUrl = ''
       if(window.location.host='localhost'){
           uploadAvatarUrl = 'http://localhost:8081/blog/upload/user/avatar'
       } else {
          uploadAvatarUrl = 'http://www.javahsf.club:8081/blog/upload/user/avatar'
       }
        return {
            uploadAvatarUrl: uploadAvatarUrl,
            registerModel: {
                userAccount: '',
                nickName: '',
                password: '',
                password2: '',
                avatarSize: 32,
                phoneNum: '',
                email: ''
            },
            onLoading: false,
            avatarUrl: '',
            // 表单校验规则
            rules: {
                userAccount: [
                    { required: true, message: '请输入用户账号', trigger: 'blur' },
                    { min: 2, max: 64, message: '2-64个字符', trigger: 'blur' }
                ],
                nickName: [
                    { required: true, message: '请输入昵称',  trigger: 'blur' },
                    { min: 2, max: 64, message: '长度控制在2-64个字符',trigger: 'blur' }
                ],
                password: [
                    { required: true, message: '请输入密码', trigger: 'blur' },
                    { min: 6, max: 18, message: '长度控制在6-18个字符', trigger: 'blur' },
                    { validator: passwordValidator, trigger: 'blur' }
                ],
                password2: [
                    { required: true, message: '请再次输入密码', trigger: 'blur' },
                    { min: 6, max: 18, message: '长度控制在6-18个字符', trigger: 'blur' },
                    { validator: password2Validator, trigger: 'blur' }
                ],
                phoneNum: [
                    { required: true, message: '请输入手机号',  trigger: 'blur'},
                    { validator: phoneNumValidator, trigger: 'blur' }
                ],
                email: [
                    { min: 0, max: 64, message: '长度控制在64个字符'},
                    { validator: emailValidator, trigger: 'blur' }
                ]

            },
            redirect: undefined
        }
    },
    watch: {
        $route: {
            handler: function(route) {
                const query = route.query
                if (query) {
                this.redirect = query.redirect
                this.otherQuery = this.getOtherQuery(query)
                }
            },
            immediate: true
        }
   },
    methods: {   
        // 图片上传之前校验图片格式和附件大小
        beforeAvartarUpload(file) {
           console.log(file)
           if(!(file.type=='image/jpeg' ||file.type=='image/png')){
              Message.error('头像图片必须是jpg或png格式')  
           }else if(file.size/(1024*1024)>10){
              Message.error('图片大小不能超过10M')
           }
        },
        // 上传图片预览
        previewAvatar(file){
            console.log(file)
        },
        // 图片上传成功回调
        handleSuccessAvatar(response){
           console.log(response.data)
           this.avatarUrl = response.data
        },
        // 提交注册
        handleRegister(formName){
            this.$refs[formName].validate((valid=>{
                if(valid){ // 表单校验通过
                    const params = {
                        username: this.registerModel.userAccount,
                        nickname: this.registerModel.nickName,
                        password: this.registerModel.password,
                        phoneNum: this.registerModel.phoneNum,
                        email: this.registerModel.email,
                        userface: this.avatarUrl
                   }
                    this.onLoading = true
                    this.$store.dispatch('user/register', params).then(res=>{
                        this.onLoading = true
                        if(res.status===200){
                            Message.success('恭喜注册成功,现在就可以登录系统了!')
                            // 跳转到登录界面
                            this.$router.push({ path: '/login', query: this.otherQuery })
                        } else {
                            Message.error(res.msg)
                        }
                    })
                }else{  // 表单校验不通过,拒绝提交注册
                    this.onLoading = true
                    Message.error('用户注册信息校验不通过,请重新填写注册信息')
                    return false
                }
            }))
        },
        // 表单重置
        resetForm(formName) {
          this.$refs[formName].resetFields()
        },
        getOtherQuery(query) {
            return Object.keys(query).reduce((acc, cur) => {
                if (cur !== 'redirect') {
                acc[cur] = query[cur]
                }
                return acc
            }, {})
        }
    }
}
</script>
<!--页面样式-->
<style lang="scss" scoped>
    .register-container{
        margin-top: 100px;
        margin-left: 10%;
        .el-input{
            width: 60%;
        }
        .avatar-uploader .avatar{
            width: 240px;
            height: 240px;
        }
        .el-button.submit-btn{
            width: 10%;
            height: 40px;
            margin-left: 150px;
            margin-right: 25px;
        }
        .el-button.reset-btn{
            width: 10%;
            height: 40px;
        }
    }
</style>

增加校验方法

src/utils/validate.js中增加校验密码和手机号码的方法

代码语言:javascript
复制
  export function validatePhoneNum(phoneNum) {
  const reg = /^(13[0-9]|14[01456879]|15[0-35-9]|16[2567]|17[0-8]|18[0-9]|19[0-35-9])\d{8}$/
  return reg.test(phoneNum)
}

export function validatePassword(password) {
  // 强密码:字母+数字+特殊字符
  const reg = /^(?![a-zA-z]+$)(?!\d+$)(?![!@#$%^&*]+$)(?![a-zA-z\d]+$)(?![a-zA-z!@#$%^&*]+$)(?![\d!@#$%^&*]+$)[a-zA-Z\d!@#$%^&*]+$/
  return reg.test(password)
}

以上校验均使用正则表达式校验

导出用户注册接口方法

src/api/user.js文件中新增用户注册接口方法

代码语言:javascript
复制
  export function register(data) {
  return request({
    url: '/user/reg',
    method: 'post',
    data
  })
}

vuex全局存储中添加用户注册action

src/store/modules/user.js 文件中的actions对象中增加用户注册行为方法

代码语言:javascript
复制
 const actions = {
  // user register
  register({ commit }, registerInfo) {
    return new Promise((resolve, reject) => {
      register(registerInfo).then(response => {
        if (response.status === 200 && response.data.status === 200) {
          const resInfo = { status: response.status, msg: '注册成功' }
          resolve(resInfo)
        } else {
          const resInfo = { status: response.status, msg: response.data.msg }
          resolve(resInfo)
        }
      }).catch(error => {
        console.error(error)
        reject(error)
      })
    })
  },
    // ......省略其他已有方法
} 

因为用户注册完之后需要跳转到登录界面,直接在注册页面调用后台用户注册接口成功后调用this.$router.push方法发现无法实现页面的跳转效果, 因此改为在vuex的全局dispatch中调用注册接口

固定路由列表中添加用户注册页面路由

src/router/index.js文件的固定路由列表中添加注册组件的路由

代码语言:javascript
复制
  import Register from '@/views/register/index'

export const constantRoutes = [
  {
    id: '0',
    path: '/register',
    component: Register,
    hidden: true
  },
   //...... 省略其他路由
 ]

登录组件中添加用户注册的跳转链接

src/views/login/index.vue文件中的模板代码部分的登录按钮标签下面添加如下两行代码

代码语言:javascript
复制
  <div>
   <router-link to="/resetPass" class="forget-password">忘记密码</router-link>
   <router-link class="register" to="/register">注册账号</router-link>
 </div>

同时对忘记密码注册账号两个链接添加样式(忘记密码功能尚待实现)

代码语言:javascript
复制
  <style lang="scss" scoped>
    .register, .forget-password{
        width: 20%;
        height: 35px;
        color: blue;
        margin-right: 20px;
        cursor: pointer;	
  }
</style>

权限控制白名单中添加用户注册路由路径

在路由跳转控制文件src/permission.js文件中将注册用户的路由添加到白名单中

代码语言:javascript
复制
  const whiteList = ['/login', '/register', '/auth-redirect']

如果不在白名单中加上用户注册的路由,你会发现在用户登录界面压根无法跳转到用户注册界面的

效果体验

在启动后端服务后,在vue-element-admin项目下通过 鼠标右键->git bash进入命令控制台

然后输入npm run dev 项目启动前端服务

然后在谷歌浏览器中输入:http://localhost:3000/回车进入登录界面

点击下面的【注册账号】链接就能跳转到用【用户注册】页面

然后填写好用户注册信息并上传头像

填写好用户注册信息后就可以点击下面的【提交】按钮提交注册了,注册成功后系统会弹框提示用户中注册成功,并重新跳转到【用户登录】界面

写在最后

本文演示了在spring-boot项目中继承阿里云对象存储sdk实现了图片上传和用户提交登录两个接口的详细实现,同时前端使用element-ui库中的upload组件调用后端图片上传接口实现了附件上传功能,实现了一个完整的用户登录信息的校验和提交注册及注册成功后的页面跳转等功能。相信对想要了解一个系统的用户模块是如何实现用户的注册以及注册成功后的页面跳转的完整功能的是如何实现的读者朋友一定会有所帮助的!

本文的全部源码已经提交到gitee仓库,需要完整源码的读者可通过下面的链接克隆到本地磁盘

后端blogserver项目源码地址

https://gitee.com/heshengfu1211/blogserver.git

前端vue-element-admin项目源码地址

https://gitee.com/heshengfu1211/vue-element-admin.git

原创不易,希望看到这里的同学都能点亮右下角的【在看】,谢谢支持!

推荐阅读

【1】看官网自学Kafka太慢,我选择在掘金小册订阅《图解Kafka之实战指南》

【2】VueblogServer项目短信验证码登录功能前端实现

【3】巧用Druid数据源实现数据库连接密码的加密解密

【4】手把手带你在集成SpringSecurity的SpringBoot应用中添加短信验证码登录认证功能

【5】SpringBoot项目中快速集成腾讯云短信SDK实现手机验证码功能

【6】从源码的角度详细分析SpringBoot启动流程

【7】Spring Cloud Alibaba微服务项目中Redis实现分布式事务锁实践

【8】记一次使用Nacos 2.0.3版本搭建微服务注册中心和客户端的踩坑填坑详细过程

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2022-07-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 阿福谈Web编程 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 引言
  • 实现用户注册流程
    • 用户注册完整流程
      • 用户注册信息及校验
      • 后台接口设计
        • 上传头像接口
          • 用户注册接口
          • 后端代码实现
            • 用户头像上传接口编码实现
              • 用户注册接口编码实现
              • 前端代码实现
                • 完成用户注册界面vue组件编码
                  • 完成用户注册组件编码
                    • 增加校验方法
                      • 导出用户注册接口方法
                        • vuex全局存储中添加用户注册action
                          • 固定路由列表中添加用户注册页面路由
                            • 登录组件中添加用户注册的跳转链接
                              • 权限控制白名单中添加用户注册路由路径
                              • 效果体验
                                • 写在最后
                                • 推荐阅读
                                相关产品与服务
                                对象存储
                                对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
                                领券
                                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档