前情提要
- 上一节我们用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请求并填写必要的信息即可完成一个用户的创建
- 借助以前编写过的rest repository代码,我们已经有了
- 首先我们需要在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的包
- 然后在包中编写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等