详解SpringBoot项目整合Vue做一个完整的用户注册功能
作者:heshengfu1211
前言
用户注册功能是每一个系统的入口门面功能,很多人可能会以为很简单,不就是一个简单的CRUD吗?其实不然,要把前后端功能都做出来,页面跳转也没问题,还真不简单。这次笔者做这么一个看似简单的用户注册功能就花了足足两天多时间,中间调试和解决Bug也花了好长时间。这次我就把自己做出的完整功能的实现过程作了一个提炼分享到我的公众号上来。希望有需要了解如何实现用户注册完整过程的读者朋友能够仔细看一看。
说明:本文前后端代码的实现分别在本人之前二次开发的开源项目vue-element-admin
和vueblog
两个项目的基础上进行
1 实现用户注册流程
1.1 用户注册完整流程
1.2 用户注册信息及校验
2 后台接口设计
2.1 上传头像接口
2.1.1 接口url
http://localhost:8081/blog/upload/user/avatar
2.1.2 请求类型
POST
2.1.3 接口入参
参数名称 | 参数类型 | 是否必传 | 备注 |
---|---|---|---|
file | MultipartFile | 是 | 多媒体图片文件 |
2.1.4 接口出参
参数名称 | 参数类型 | 示例值 | 备注 |
---|---|---|---|
status | Integer | 200 | 状态码:200-成功; 500-失败 |
msg | String | “success” | 响应信息:“success”-上传头像成功; "upload file failed"-上传头像失败 |
data | String | vueblog2022.oss-cn-shenzhen.aliyuncs.com/avatar/63be… | 上传头像成功后的下载地址 |
2.2 用户注册接口
2.2.1 接口url
http://localhost:8081/blog/user/reg
2.2.2 请求类型
POST
2.2.3 接口入参
参数名称 | 参数类型 | 是否必填 | 备注 |
---|---|---|---|
username | String | 是 | 用户账号 |
nickname | String | 是 | 用户昵称 |
password | String | 是 | 用户登录密码 |
userface | String | 否 | 用户头像链接地址 |
phoneNum | Long | 是 | 用户手机号码 |
String | 否 | 用户邮箱地址 |
2.2.3 接口出参
参数名称 | 参数类型 | 示例值 | 备注 |
---|---|---|---|
status | Integer | 200 | 响应码: 200-成功;500-失败 |
msg | String | 注册成功 | 响应消息 |
data | Integer | 0 | 注册成功标识:0-注册成功;1-用户名重复; null-内部服务异常 |
3 后端代码实现
3.1 用户头像上传接口编码实现
文件上传,这里选用了阿里云的对象存储,需要先开通阿里云对象存储服务,关于如何开通阿里云短信服务并将阿里云对象存储服务集成到SpringBoot项目中,请参考我之前发布的文章SpringBoot项目集成阿里云对象存储服务实现文件上传
3.1.1 服务层编码
新建OssClientService
类继承阿里云对象存储服务SDK完成图片上传功能
@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
方法关闭这个实例
3.1.2 控制器层编码
新建UploadFileController
类完成从前端接收附件参数,并调用OssClientService
服务实现图片上传
@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; } }
3.2 用户注册接口
3.2.1 数据库访问层编码
在UserMapper
接口类中新增注册用户抽象方法
int registerUser(UserDTO user);
然后在UserMapper.xml
文件中完成用户数据入库sql编写
<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>
3.2.2 服务层编码
在CustomUserDetailsService
接口类中添加注册用户抽象方法
int registerUser(UserDTO user);
然后在 CustomUserDetailsService
接口类的实现类UserService
类中完成用户注册逻辑
@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; } }
3.2.3 控制器层编码
在LoginRegController
类中完成用户登录接口从前端接收参数到调用UserService
服务类完成用户注册业务
@RequestMapping(value = "/login_page", method = RequestMethod.GET) @ApiOperation(value = "loginPage", notes = "尚未登录跳转", produces = "application/json", consumes = "application/json", response = RespBean.class) public RespBean loginPage() { return new RespBean(ResponseStateConstant.UN_AUTHORIZED, "尚未登录,请登录!"); } @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)
方法中添加如下几行代码即可
http.authorizeRequests() .antMatchers("/user/reg").anonymous() .antMatchers("/upload/user/avatar").anonymous()
完成后端编码后可以启动Mysql服务和redis服务,然后运行BlogserverApplication
类中的Main方法成功后就可以通过postman工具测试接口了
4 前端代码实现
4.1 完成用户注册界面vue组件编码
在src/views
目录下新建register
文件夹,然后在register
目录下新建index.vue
文件
完成用户注册组件编码
这里的文件上传选择了element-ui
组件库中的upload组件
<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, uploadUrl: uploadUrl, phoneNum: '', email: '' }, onLoading: false, avatarUrl: '', password2Style: { dispaly: 'none', color: 'red' }, // 表单校验规则 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>
4.2 工具类中增加校验方法
src/utils/validate.js
中增加校验密码和手机号码的方法
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) }
以上校验均使用正则表达式校验
4.3 API文件中添加用户注册方法
src/api/user.js
文件中新增用户注册接口方法
export function register(data) { return request({ url: '/user/reg', method: 'post', data }) }
4.4 全局方法中添加用户注册方法
src/store/modules/user.js
文件中的actions
对象中增加用户注册行为方法
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
中调用注册接口
4.5 路由列表中添加用户注册组件
在src/router/index.js
文件的固定路由列表中添加注册组件的路由
import Register from '@/views/register/index' export const constantRoutes = [ { id: '0', path: '/register', component: Register, hidden: true }, //...... 省略其他路由 ]
4.6 登录组件中添加用户注册的跳转链接
在src/views/login/index.vue
文件中的模板代码部分的登录按钮标签下面添加如下两行代码
<div> <router-link to="/resetPass" class="forget-password">忘记密码</router-link> <router-link class="register" to="/register">注册账号</router-link> </div>
同时对忘记密码和注册账号两个链接添加样式(忘记密码功能尚待实现)
<style lang="scss" scoped> .register, .forget-password{ width: 20%; height: 35px; color: blue; margin-right: 20px; cursor: pointer; } </style>
4.7 路由跳转控制中添加白名单
在路由跳转控制文件src/permission.js
文件中将注册用户的路由添加到白名单中
const whiteList = ['/login', '/register', '/auth-redirect'] // no redirect whitelist
如果不在白名单中加上用户注册的路由,你会发现在用户登录界面压根无法跳转到用户注册界面的
5 效果体验
在启动后端服务后,在vue-element-admin项目下通过 鼠标右键->git bash进入命令控制台
然后输入npm run dev
项目启动前端服务
然后在谷歌浏览器中输入:http://localhost:3000/回车进入登录界面
点击下面的【注册账号】链接就能跳转到用【用户注册】页面
填写好用户注册信息后就可以点击下面的【提交】按钮提交注册了,注册成功后系统会弹框提示用户中注册成功,并重新跳转到【用户登录】界面
6 写在最后
本文演示了在spring-boot
项目中继承阿里云对象存储sdk实现了图片上传和用户提交登录两个接口的详细实现,同时前端使用element-ui
库中的upload
组件调用后端图片上传接口实现了附件上传功能,实现了一个完整的用户登录信息的校验和提交注册及注册成功后的页面跳转等功能。
相信对想要了解一个系统的用户模块是如何实现用户的注册以及注册成功后的页面跳转的完整功能的是如何实现的读者朋友一定会有所帮助的!
本文前后端项目代码git仓库地址如下,对源码感兴趣的读者朋友可以克隆到本地参考
到此这篇关于SpringBoot项目整合Vue做一个完整的用户注册功能的文章就介绍到这了,更多相关SpringBoot项目整合Vue做一个完整的用户注册功能内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
您可能感兴趣的文章:
- 详解springboot springsecuroty中的注销和权限控制问题
- SpringBoot使用Spring Security实现登录注销功能
- SpringBoot--- SpringSecurity进行注销权限控制的配置方法
- Vue+springboot批量删除功能实现代码
- springboot和vue前后端交互的实现示例
- SpringBoot3结合Vue3实现用户登录功能
- 基于SpringBoot和Vue3的博客平台的用户注册与登录功能实现
- SpringBoot和Vue.js实现的前后端分离的用户权限管理系统
- Vue结合Springboot实现用户列表单页面(前后端分离)
- vue+springboot用户注销功能实现代码