詳解SpringBoot項目整合Vue做一個完整的用戶註冊功能

前言

用戶註冊功能是每一個系統的入口門面功能,很多人可能會以為很簡單,不就是一個簡單的CRUD嗎?其實不然,要把前後端功能都做出來,頁面跳轉也沒問題,還真不簡單。這次筆者做這麼一個看似簡單的用戶註冊功能就花瞭足足兩天多時間,中間調試和解決Bug也花瞭好長時間。這次我就把自己做出的完整功能的實現過程作瞭一個提煉分享到我的公眾號上來。希望有需要瞭解如何實現用戶註冊完整過程的讀者朋友能夠仔細看一看。

說明:本文前後端代碼的實現分別在本人之前二次開發的開源項目vue-element-adminvueblog兩個項目的基礎上進行

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 用戶手機號碼
email 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倉庫地址如下,對源碼感興趣的讀者朋友可以克隆到本地參考

blogserver項目gitee倉庫地址

vue-element-admin項目gitee倉庫地址

到此這篇關於SpringBoot項目整合Vue做一個完整的用戶註冊功能的文章就介紹到這瞭,更多相關SpringBoot項目整合Vue做一個完整的用戶註冊功能內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: