vue+springmvc前后端分离开发(十三)(实现注册与登录功能)

前情提要

  • 上一节我们用vuetify简单做了首页、登录页和注册页,并且为按钮绑定事件以完成路由跳转功能
  • 这一节我们将使用vue-axios来完成对后端接口请求,并且实现注册与登录功能

跨域问题

  • 在前后端分离开发中,经常会遇见跨域问题,即发送post、put等请求到后端接口上时,会因为同源策略被禁止,实际上这是spring security的一种保护机制,为了保证用户只能通过网站页面的一些操作才能访问接口
  • 在根目录kmhc这个包中创建一个CORSFilter类,并写上如下代码(注意该类必须和Application类同级,不然无法生效
package kmhc;

import java.io.IOException;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;

@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
@Profile("dev")
public class CORSFilter implements Filter {

	@SuppressWarnings("unused")
	private FilterConfig config;

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
			throws IOException, ServletException {
		HttpServletResponse httpServletResponse = (HttpServletResponse)response;
		HttpServletRequest httpServletRequest = (HttpServletRequest)request;
		httpServletResponse.setHeader("Access-Control-Allow-Origin", "*");
		httpServletResponse.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE, PATCH");
		httpServletResponse.setHeader("Access-Control-Max-Age", "3600");
		httpServletResponse.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN");
		if ("OPTIONS".equalsIgnoreCase(httpServletRequest.getMethod())) {
           httpServletResponse.setStatus(HttpServletResponse.SC_OK);
        } else {
            chain.doFilter(request, response);
        }
	}
	
	@Override
    public void init(FilterConfig filterConfig) throws ServletException {
        config = filterConfig;
    }
}

注册功能

  • 什么是注册功能?
    • 注册功能实际上就是在数据库中添加一条用户信息的记录
  • 如何实现注册功能?
    • 借助以前编写过的rest repository代码,我们已经有了/api/users这样一个接口,对这个接口发送post请求并填写必要的信息即可完成一个用户的创建
  • 首先我们需要在main.js中导入axios和vue-axios(vue-axios需要用到axios,所以没有安装axios依赖的请先安装),并使用Vue.use的方式装配
import Vue from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import vuetify from './plugins/vuetify'
import axios from 'axios'
import VueAxios from 'vue-axios'

// 使用Vue.use的方式装载组件可以不破坏Vue的原型属性
Vue.use(VueAxios, axios)

Vue.config.productionTip = false

new Vue({
  router,
  store,
  vuetify,
  render: h => h(App)
}).$mount('#app')

  • 然后我们需要借用vue-axios对接口发送请求,在Register.vue中添加如下代码
<template>
  <!--建议使用v-container来管理-->
  <v-container fluid>
    <!--一个小卡片-->
    <v-row justify="center">
      <!--细节控制,不然卡片的大小会不自然-->
      <v-col sm="6" md="4" lg="3">
        <v-card class="mx-auto">
          <v-img class="white--text align-end" height="200px" src="../assets/user/top.jpeg">
            <v-card-title>Register</v-card-title>
          </v-img>
          <!--表单填写部分-->
          <v-card-text>
            <!--ref是vue特有的别名,无需通过id来获取元素-->
            <v-form ref="registerForm" v-model="valid" lazy-validation autocomplete="off">
              <!--用户名-->
              <v-row>
                <v-col>
                  <v-text-field v-model="form.username" :rules="rules.usernameRules" label="用户名"></v-text-field>
                </v-col>
              </v-row>
              <!--密码-->
              <v-row>
                <v-col>
                  <v-text-field v-model="form.password" :rules="rules.passwordRules" label="密码" :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'" @click:append="show = !show" :type="show ? 'text': 'password'"></v-text-field>
                </v-col>
              </v-row>
              <!--确认密码-->
              <v-row>
                <v-col>
                  <v-text-field v-model="form.passwordConfirm" :rules="[rules.passwordConfirmRules.required, rules.passwordConfirmRules.equal(form.password, form.passwordConfirm)]" label="确认密码" :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'" @click:append="show = !show" :type="show ? 'text': 'password'"></v-text-field>
                </v-col>
              </v-row>
            </v-form>
          </v-card-text>
          <!--动作执行按钮-->
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn :disabled="!valid" color="primary" @click="register()">Register</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
    <!--错误提示,用于反馈创建用户时的错误-->
    <v-snackbar
      v-model="snackbar"
    >
      用户名已存在!
      <template v-slot:action="{ attrs }">
        <v-btn
          color="pink"
          text
          v-bind="attrs"
          @click="snackbar = false"
        >
          Close
        </v-btn>
      </template>
    </v-snackbar>
  </v-container>
</template>

<script>
export default {
  name: 'Register',
  data: () => ({
    // 用于验证表单内容填写是否正确
    valid: true,
    // 用于控制密码是否以可见的形式显示
    show: false,
    // 用于控制消息条是否显示
    snackbar: false,
    // 表单填写的内容
    form: {
      username: '',
      password: '',
      passwordConfirm: ''
    },
    // 验证规则
    rules: {
      usernameRules: [
        v => !!v || '用户名不能为空',
        v => ((v || '').length >= 1 && (v || '').length <= 20) || '用户名必须在1-20个字符以内'
      ],
      passwordRules: [
        v => !!v || '密码不能为空',
        v => ((v || '').length >= 6 && (v || '').length <= 30) || '密码必须在6-30个字符以内'
      ],
      passwordConfirmRules: {
        required: v => !!v || '请输入确认密码',
        equal: (password, passwordConfirm) => password === passwordConfirm || '两次密码不一致'
      }
    }
  }),
  methods: {
    // 注册功能
    register () {
      // 点击注册按钮时首先验证表单填写的正确性
      const valid = this.$refs.registerForm.validate()
      if (valid) {
        // 用vue-axios发送post请求
        this.axios.post('http://127.0.0.1:9001/api/users', {
          username: this.form.username,
          password: this.form.password
        }).then(() => {
          // 注册成功,跳转到登录页面
          this.$router.push({
            name: 'Login'
          })
        }).catch((error) => {
          // 发生请求错误,此处错误可能是因为用户名已存在,也有可能是提交的数据不符合要求,也有可能是服务器内部错误
          // 当返回的状态码是409时,说明用户名已存在
          console.log(error)
          // 显示错误消息
          this.snackbar = true
        })
      }
    }
  }
}
</script>

测试注册功能

  • 首先来测试正确填写表单,成功注册的情况,输入如下信息
    正确填写表单内容
  • 点击注册按钮,发现路由跳转到登录上,说明注册成功,打开mysql命令行,进入kmhc数据库,输入命令select * from users where username = "test1";查询是否真正成功
  • 然后再测试用户名已存在的情况下注册是否成功,仍然输入以上的内容,尝试创建一个用户名为test1的用户,得到如下结果,提示用户名已存在
    用户名已存在

登录功能

  • 什么是登录功能?
    • 实际上就是对提交上来的用户名和密码进行验证,在验证成功以后返回用户的基本信息
  • 如何实现登录功能?
    • 首先我们是缺少“身份验证”这一缺口的,所以需要自己编写这个接口,好在spring security提供了现成的认证逻辑,我们只需要使用它就好了
    • 然后在前端向这个接口发送请求即可
  • 首先编写登录接口,我们需要在后端项目中创建一个名叫api的包
    创建api包
  • 然后在包中编写UserController类,代码如下
package kmhc.api;

import java.util.Optional;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.rest.webmvc.support.RepositoryEntityLinks;
import org.springframework.hateoas.EntityModel;
import org.springframework.hateoas.Link;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import kmhc.domain.user.User;
import kmhc.form.LoginForm;
import kmhc.repository.user.UserRepository;
import lombok.extern.slf4j.Slf4j;

@RestController
@Slf4j
public class UserController {
	
	@Autowired
	private UserRepository userRepo;
	
	@Autowired
	private RepositoryEntityLinks repositoryEntityLinks;
	
	@PostMapping(path = "/api/login")
	public ResponseEntity<EntityModel<User>> login(@RequestBody @Valid LoginForm form, HttpServletRequest request) throws ServletException {
		String username = form.getUsername();
		String password = form.getPassword();
		try {
			// try login
			request.login(username, password);
		} catch (ServletException servletException) {
			throw servletException;
		}
		// log authentication
		log.info(username + " is authenticated!");
		// return the user resource
		Optional<User> optional = userRepo.findByUsername(username);
		if (optional.isPresent()) {
			// get the resource url
			User user = optional.get();
			Link link = repositoryEntityLinks.linkToItemResource(User.class, user.getId());
			EntityModel<User> userEntity = EntityModel.of(user, link);
			return new ResponseEntity<>(userEntity, HttpStatus.OK);
		}
		throw new UsernameNotFoundException("User '" + username +"' not found!");
	}
}
  • 以上代码做了几件事
    • 接收对/api/login接口的POST请求
    • 对提交上来的数据通过@Valid进行验证
    • 用request.login()方法验证用户名和密码的正确性(这是一种简单粗暴的方法,以后会用oauth2来更好地实现用户身份功能
    • 记录日志
    • 返回用户资源
  • LoginForm是一个类,里面用注解对属性做了简单的验证,属性名要和POST请求提交上了的信息一致才行
package kmhc.form;

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Data;

@Data
public class LoginForm {

	@NotNull
	private String username;
	
	@Size(min = 6, message = "password must be at least 6 characters!")
	private String password;
}

  • 随后需要对http做小小的修改,使其允许任何人访问/api/login接口
@Override
protected void configure(HttpSecurity http) throws Exception {
	http
		.csrf().disable()
		.cors()
		.and()
		.authorizeRequests()
		.antMatchers(HttpMethod.POST, "/api/login").permitAll()
		.antMatchers(HttpMethod.POST, "/api/users").permitAll()
		.antMatchers(HttpMethod.GET, "/api/users/{username}").permitAll()
		.antMatchers("/api/users/{username}").authenticated()
		.antMatchers(HttpMethod.GET, "/api/users").hasRole("ADMIN")	
		.antMatchers("/api/authorities/**").hasRole("ADMIN")
		.antMatchers("/api/groups/**").hasRole("ADMIN")
		.antMatchers("/api/groupAuthorities/**").hasRole("ADMIN")
		.and()
		.formLogin().disable()
		.httpBasic()
		.and()
		.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
		
}
  • 然后我们就需要编写Login.vue对后端接口发送请求了,代码如下
<template>
  <!--建议使用v-container来管理-->
  <v-container fluid>
    <!--一个小卡片-->
    <v-row justify="center">
      <!--细节控制,不然卡片的大小会不自然-->
      <v-col sm="6" md="4" lg="3">
        <v-card class="mx-auto">
          <v-img class="white--text align-end" height="200px" src="../assets/user/top.jpeg">
            <v-card-title>Login</v-card-title>
          </v-img>
          <!--表单填写部分-->
          <v-card-text>
            <!--ref是vue特有的别名,无需通过id来获取元素-->
            <v-form ref="loginForm" v-model="valid" lazy-validation autocomplete="off">
              <!--用户名-->
              <v-row>
                <v-col>
                  <v-text-field v-model="form.username" :rules="rules.usernameRules" label="用户名"></v-text-field>
                </v-col>
              </v-row>
              <!--密码-->
              <v-row>
                <v-col>
                  <v-text-field v-model="form.password" :rules="rules.passwordRules" label="密码" :append-icon="show ? 'mdi-eye' : 'mdi-eye-off'" @click:append="show = !show" :type="show ? 'text': 'password'"></v-text-field>
                </v-col>
              </v-row>
            </v-form>
          </v-card-text>
          <!--动作执行按钮-->
          <v-card-actions>
            <v-spacer></v-spacer>
            <v-btn :disabled="!valid" color="primary" @click="login()">Login</v-btn>
          </v-card-actions>
        </v-card>
      </v-col>
    </v-row>
    <!--错误提示,用于反馈创建用户时的错误-->
    <v-snackbar
      v-model="snackbar"
    >
      用户名或密码不正确!
      <template v-slot:action="{ attrs }">
        <v-btn
          color="pink"
          text
          v-bind="attrs"
          @click="snackbar = false"
        >
          Close
        </v-btn>
      </template>
    </v-snackbar>
  </v-container>
</template>

<script>
export default {
  name: 'Login',
  data: () => ({
    // 用于验证表单内容填写是否正确
    valid: true,
    // 用于控制密码是否以可见的形式显示
    show: false,
    // 用于控制消息条是否显示
    snackbar: false,
    // 表单填写的内容
    form: {
      username: '',
      password: ''
    },
    // 验证规则
    rules: {
      usernameRules: [
        v => !!v || '用户名不能为空',
        v => ((v || '').length >= 1 && (v || '').length <= 20) || '用户名必须在1-20个字符以内'
      ],
      passwordRules: [
        v => !!v || '密码不能为空',
        v => ((v || '').length >= 6 && (v || '').length <= 30) || '密码必须在6-30个字符以内'
      ]
    }
  }),
  methods: {
    // 登录功能
    login () {
      // 首先验证表单填写的正确性
      const valid = this.$refs.loginForm.validate()
      if (valid) {
        // 表单填写无误,向login接口发送post请求
        this.axios.post('http://127.0.0.1:9001/api/login', {
          username: this.form.username,
          password: this.form.password
        }).then(response => {
          // 验证成功,获取用户信息
          console.log(response)
        }).catch(error => {
          console.log(error)
          // 用户认证错误,显示消息条
          this.snackbar = true
        })
      }
    }
  }
}
</script>

测试登录功能

  • 首先测试正确的登录请求,输入正确的用户名和密码
    输入正确的用户名和密码
  • 按下F12,打开开发者工具栏,在控制台中可以看到输出
    登录成功的输出
  • 可以看到后端确实返回了admin的一些信息
  • 再输入错误的用户名和密码,查看控制台信息错误的输入
  • 可以看到后端返回了一个401,这个状态码表示用户身份验证未通过

保存用户登录状态

  • 虽然后端成功返回了用户信息,但是我们并没有使用它,我们需要将vuex中的全局用户信息替换为返回的信息
  • 再store/index.js中修改代码为如下形式
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    // 代表用户自身,最终不一定会以这种方式呈现
    user: {
      id: null,
      username: '',
      password: '',
      enabled: true,
      firstName: '',
      lastName: '',
      gender: 0,
      phone: '',
      email: '',
      icon: '',
      birthday: new Date().toISOString().substr(0, 10),
      joinedDate: new Date().toISOString().substr(0, 10),
      // 和用户相关的一些链接,比如用户自身的描述
      _links: []
    }
  },
  mutations: {
    // 登录后设置user
    setUser (state, user) {
      state.user = user
    },
    // 注销后清除user
    removeUser (state) {
      state.user = {
        id: null,
        username: '',
        password: '',
        enabled: true,
        firstName: '',
        lastName: '',
        gender: 0,
        phone: '',
        email: '',
        icon: '',
        birthday: new Date().toISOString().substr(0, 10),
        joinedDate: new Date().toISOString().substr(0, 10),
        // 和用户相关的一些链接,比如用户自身的描述
        _links: []
      }
    }
  },
  actions: {
  },
  modules: {
  }
})
  • 再在Login.vue的login()方法中添加登录成功时,设置全局user的逻辑
login () {
  // 首先验证表单填写的正确性
  const valid = this.$refs.loginForm.validate()
  if (valid) {
    // 表单填写无误,向login接口发送post请求
    this.axios.post('http://127.0.0.1:9001/api/login', {
      username: this.form.username,
      password: this.form.password
    }).then(response => {
      // 验证成功,获取用户信息
      console.log(response)
      // 设置全局user
      this.$store.commit('setUser', response.data)
      // 回到主页
      this.$router.push({
        name: 'Home'
      })
    }).catch(error => {
      console.log(error)
      // 用户认证错误,显示消息条
      this.snackbar = true
    })
  }
}
  • 这时候点击登录按钮就可以看到左边导航栏的注销按钮出现
    注销按钮

实现注销

  • 注销其实就是将vuex中的user初始化,上文代码的store/index.js已经实现了注销的逻辑
  • 为注销按钮绑定一个事件,调用vuex中的注销方法即可

代码仓库

  • 因为本文代码内容较多,在阅读上造成很多困难,所以给出代码仓库,请读者们对比仓库自行修正,代码仓库

至此,简单的注册、登录和注销功能已经实现,下一节会详细讲解如何用oauth2去实现用户的登录注册,返回token等

  • 3
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值