用户注册功能是每一个系统的入口门面功能,很多人可能会以为很简单,不就是一个简单的CRUD吗?其实不然,要把前后端功能都做出来,页面跳转也没问题,还真不简单。这次笔者做这么一个看似简单的用户注册功能就花了足足两天多时间,中间调试和解决Bug也花了好长时间。这次我就把自己做出的完整功能的实现过程作了一个提炼分享到我的公众号上来。希望有需要了解如何实现用户注册完整过程的读者朋友能够仔细看一看。
说明:本文前后端代码的实现分别在本人之前二次开发的开源项目vue-element-admin
和vueblog
两个项目的基础上进行
1)接口url
http://localhost:8081/blog/upload/user/avatar
3)接口入参
参数名称 | 参数类型 | 是否必传 | 备注 |
---|---|---|---|
file | MultipartFile | 是 | 多媒体图片文件 |
4)接口出参
参数名称 | 参数类型 | 示例值 | 备注 |
---|---|---|---|
status | Integer | 200 | 状态码:200-成功; 500-失败 |
msg | String | “success” | 响应信息:“success”-上传头像成功; “upload file failed”-上传头像失败 |
data | String | https://vueblog2022.oss-cn-shenzhen.aliyuncs.com/avatar/63be8be25fee4c0f8df679238435d8d2.png | 上传头像成功后的下载地址 |
1)接口url
http://localhost:8081/blog/user/reg
3)接口入参
参数名称 | 参数类型 | 是否必填 | 备注 |
---|---|---|---|
username | String | 是 | 用户账号 |
nickname | String | 是 | 用户昵称 |
password | String | 是 | 用户登录密码 |
userface | String | 否 | 用户头像链接地址 |
phoneNum | Long | 是 | 用户手机号码 |
String | 否 | 用户邮箱地址 |
参数名称 | 参数类型 | 示例值 | 备注 |
---|---|---|---|
status | Integer | 200 | 响应码: 200-成功;500-失败 |
msg | String | 注册成功 | 响应消息 |
data | Integer | 0 | 注册成功标识:0-注册成功;1-用户名重复; null-内部服务异常 |
文件上传,这里选用了阿里云的对象存储,需要先开通阿里云对象存储服务,关于如何开通阿里云短信服务并将阿里云对象存储服务集成到SpringBoot项目中,请参考我之前发布的文章SpringBoot项目集成阿里云对象存储服务实现文件上传
新建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
方法关闭这个实例
新建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;
}
}
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>
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;
}
}
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工具测试接口了
在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文件,且不超过10Mdiv>
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>
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)
}
以上校验均使用正则表达式校验
src/api/user.js
文件中新增用户注册接口方法export function register(data) {
return request({
url: '/user/reg',
method: 'post',
data
})
}
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
中调用注册接口
src/router/index.js
文件的固定路由列表中添加注册组件的路由import Register from '@/views/register/index'
export const constantRoutes = [
{
id: '0',
path: '/register',
component: Register,
hidden: true
},
//...... 省略其他路由
]
在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>
src/permission.js
文件中将注册用户的路由添加到白名单中const whiteList = ['/login', '/register', '/auth-redirect'] // no redirect whitelist
如果不在白名单中加上用户注册的路由,你会发现在用户登录界面压根无法跳转到用户注册界面的
在启动后端服务后,在vue-element-admin项目下通过 鼠标右键->git bash进入命令控制台
然后输入npm run dev
项目启动前端服务
然后在谷歌浏览器中输入:http://localhost:3000/回车进入登录界面
然后填写好用户注册信息并上传头像
填写好用户注册信息后就可以点击下面的【提交】按钮提交注册了,注册成功后系统会弹框提示用户中注册成功,并重新跳转到【用户登录】界面
本文演示了在spring-boot项目中继承阿里云对象存储sdk实现了图片上传和用户提交登录两个接口的详细实现,同时前端使用element-ui库中的upload组件调用后端图片上传接口实现了附件上传功能,实现了一个完整的用户登录信息的校验和提交注册及注册成功后的页面跳转等功能。相信对想要了解一个系统的用户模块是如何实现用户的注册以及注册成功后的页面跳转的完整功能的是如何实现的读者朋友一定会有所帮助的!
本文前后端项目代码仓库地址
blogserver项目代码gitee地址
vue-element-admin项目gitee地址