首頁 > 軟體

詳解SpringBoot專案整合Vue做一個完整的使用者註冊功能

2022-07-15 10:00:23

前言

使用者註冊功能是每一個系統的入口門面功能,很多人可能會以為很簡單,不就是一個簡單的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 介面入參

引數名稱引數型別是否必傳備註
fileMultipartFile多媒體圖片檔案

2.1.4 介面出參

引數名稱引數型別範例值備註
statusInteger200狀態碼:200-成功; 500-失敗
msgString“success”響應資訊:“success”-上傳頭像成功; "upload file failed"-上傳頭像失敗
dataStringvueblog2022.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 介面入參

引數名稱引數型別是否必填備註
usernameString使用者賬號
nicknameString使用者暱稱
passwordString使用者登入密碼
userfaceString使用者頭像連結地址
phoneNumLong使用者手機號碼
emailString使用者郵箱地址

2.2.3 介面出參

引數名稱引數型別範例值備註
statusInteger200響應碼: 200-成功;500-失敗
msgString註冊成功響應訊息
dataInteger0註冊成功標識: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-zd]+$)(?![a-zA-z!@#$%^&*]+$)(?![d!@#$%^&*]+$)[a-zA-Zd!@#$%^&*]+$/
  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做一個完整的使用者註冊功能內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com