spring-boot+vue实现前后端分离
基于springboot+vue实现前后端分离后,对其添加身份校验的功能;
实现思路是在用户登录成功后由服务器为其发放一个通行证,只有拥有通行证,才可以访问前端的部分页面,实现权限区分.
而前端只会确认是否有通行证,不会对通行证的真伪进行检验,后端的工作是对请求进行拦截,来判断用户的通行证的有效性.
一 . 后端搭建
- 在pom文件中添加jwt依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
- 创建通行证工具类,并提供生成通行证,校验通行证,获得通行证归属的用户的三个方法;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Date;
import java.util.stream.StreamSupport;
/*提供令牌生成和校验*/
public class JwtUtil {
//令牌有效时间
private static final long EXPIREE_TIME = 5 * 60 * 1000;
private static final String SECRECT = "gw";
//获得令牌
public static String getToken(String userId) {
Date date = new Date(System.currentTimeMillis() + EXPIREE_TIME);
JWTCreator.Builder builder = JWT.create();
//为令牌赋值:用户
builder.withAudience(userId);
//为令牌赋值:失效时间
builder.withExpiresAt(date);
//为令牌加密
Algorithm algorithm = Algorithm.HMAC256(SECRECT);
String sign = builder.sign(algorithm);
//为令牌赋值:用户
return sign;
}
//获得令牌的归属用户
public static String getUserId(String token) {
String userId = JWT.decode(token).getAudience().get(0);
return userId;
}
//校验通行证是否正确
public static boolean checkToken(String token) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRECT);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
DecodedJWT verify = jwtVerifier.verify(token);
return true;
} catch (JWTVerificationException ex) {
throw new RuntimeException("token 失效了");
}
}
}
- 创建一个自定义注解,用来拦截需要校验的请求,注意加此注解的方法后,需要通行证有效慈爱可以访问.
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* 自定义一个需要在方法前标识的注解(此方法需要拦截)
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtToken {
}
- 创建一个处理拦截的类,用来拦截请求
import com.qf.j2005.anno.JwtToken;
import com.qf.j2005.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
/**
* 创建对令牌的的拦截工具类
*/
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
// 执行控制器方法前激发
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
StringBuffer requestURL = request.getRequestURL();
// 获取令牌
String token = request.getHeader("LINGPAI");
System.out.println("url:" + requestURL);
System.out.println("token:" + token);
// 如果执行的不是处理器方法,则不必拦截
if (!(handler instanceof HandlerMethod)) {
System.out.println("没加注解不拦截...");
return true;
}
HandlerMethod handler1 = (HandlerMethod) handler;
// 获取拦截的方法对象
Method method = handler1.getMethod();
// 从方法对象中查看是否含有指定的注解
JwtToken jwtToken = method.getAnnotation(JwtToken.class);
if (jwtToken != null) {//方法前含有指定注解
// 校验令牌有效性
boolean b = JwtUtil.checkToken(token);
if (b) {
return true;
} else {
throw new RuntimeException("token 失效了 请重新登录");
}
}
return true;
}
//执行完控制器方法的业务逻辑后触发
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
//执行完控制器逻辑视图后,在进入视图解析器前出发
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 自定义类InterConfig 对不需要拦截的方法进行配置
package com.qf.j2005;
import com.qf.j2005.interceptor.JwtInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.ArrayList;
import java.util.List;
public class InterConfig implements WebMvcConfigurer {
//通用来处理各种跨域的问题
@Override
public void addInterceptors(InterceptorRegistry registry) {
//配置不拦截的路径
List<String> excludePath = new ArrayList<>();
excludePath.add("/user_register"); //注册
excludePath.add("/dealLogin"); //登录
excludePath.add("/logout"); //登出
excludePath.add("/static/**"); //静态资源
excludePath.add("/assets/**"); //静态资源
registry.addInterceptor(jwtAuthenIntercetpor())
.addPathPatterns("/**")
// 排除拦截路径
.excludePathPatterns(excludePath);
}
@Bean
public JwtInterceptor jwtAuthenIntercetpor() {
return new JwtInterceptor();
}
@Override
public void addCorsMappings(CorsRegistry registry) {
registry
// 允许跨域的路径
.addMapping("/**")
// 允许跨域的path
.allowedOrigins("*")
// 允许凭证
.allowCredentials(true)
// 允许通过方法
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
// 跨域的生存期
.maxAge(3600 * 24);
}
}
- 在登录的controller中发放令牌
import com.qf.j2005.anno.JwtToken;
import com.qf.j2005.po.Admin;
import com.qf.j2005.service.AdminService;
import com.qf.j2005.utils.JwtUtil;
import com.qf.j2005.utils.Msg;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import vo.AdminVo;
import vo.ToKenVo;
import java.util.List;
import java.util.UUID;
@RestController
@Slf4j
public class AdminController {
@Autowired
private AdminService adminService;
@CrossOrigin
@RequestMapping(value = "/login", method = RequestMethod.POST)
public Msg login(@RequestBody AdminVo adminVo) {
Admin admin = adminService.findAdminByName(adminVo.getUsername());
if (admin != null) {
System.out.println(admin + "---" + adminVo);
if (admin.getPassword().equals(adminVo.getPassword())) {
admin.setPassword("");
//密码匹配,登录成功
// 发放令牌
String userId = UUID.randomUUID().toString();
String token = JwtUtil.getToken(userId);
// 封装含令牌的对象
ToKenVo tokentVo = new ToKenVo(admin, token);
System.out.println("tokentVo" + tokentVo);
return new Msg(200, "登录成功", tokentVo);
} else {
return new Msg(300, "用户名或密码错误", "");
}
}
return new Msg(400, "用户不存在", "");
}
}
@CrossOrigin
@JwtToken
@RequestMapping(value = "/users", method = RequestMethod.GET)
public Msg users() {
List<Admin> list = adminService.findAll();
if (list != null) {
return new Msg(200, "查到了 ", list);
}
return new Msg(300, "没找到 ", "");
}
}
- 对查询的请求设置注解,判断通行证的有效性,只有有效的通行证才可以访问此方法
二 . 前端搭建
一. 实现思路
思路如下:
1、第一次登录的时候,前端调后端的登陆接口,发送用户名和密码
2、后端收到请求,验证用户名和密码,验证成功,就给前端返回一个token
3、前端拿到token,将token存储到localStorage和vuex中,并跳转路由页面
4、前端每次跳转路由,就判断 localStroage 中有无 token ,没有就跳转到登录页面,有则跳转到对应路由页面
5、每次调后端接口,都要在请求头中加token
6、后端判断请求头中有无token,有token,就拿到token并验证token,验证成功就返回数据,验证失败(例如:token过期)就返回401,请求头中没有token也返回401
7、如果前端拿到状态码为401,就清除token信息并跳转到登录页面
二. 实现步骤
npm install vuex --save
(安装 Vuex,用于管理状态)- 在index.js配置vuex状态管理
import Vuex from 'vuex';
Vue.use(Vuex);
const store = new Vuex.Store({
state: {
// 存储token
LINGPAI: localStorage.getItem('LINGPAI') ? localStorage.getItem('LINGPAI') : ''
},
mutations: {
// 修改token,并将token 存入localStorage中
changeLogin(state, user) {
state.LINGPAI = user.LINGPAI;
localStorage.setItem('LINGPAI', user.LINGPAI);
}
}
});
- 在index.js配置路由守卫
Vue.use(Router);
const routes = [{
path: '/',
component: login
},
{
path: '/admin',
component: admin,
children: [
{ path: 'users', component: users },
{ path: 'orders', component: orders },
{ path: 'hello', component: HelloWorld },
]
}, {
path: '/orders',
component: orders
}, {
path: '/hello',
component: HelloWorld
}
];
const router = new Router({
mode: 'history',
routes
});
// 路由守卫
router.beforeEach((to, from, next) => {
if (to.path == '/' || to.path == '/hello') {
next();
} else {
// 获取当前用户的令牌
let token = localStorage.getItem('LINGPAI');
if (token === null || token === '') {
alert("登录已过期,请前往登录!")
next('/');
} else {
next();
}
}
});
export default router;
- 在main.js中添加请求拦截
// 引用axios,并设置基础URL为后端服务api地址
var axios = require('axios');
axios.defaults.baseURL = 'http://localhost:8085' //此处是https协议如果不是改成http
// 将API方法绑定到全局
Vue.prototype.$axios = axios
//给axios添加请求拦截器
axios.interceptors.request.use(
// function (config) {
config => {
// Do something before request is sent
if (localStorage.getItem('LINGPAI')) {
// 如果令牌存在 将令牌添加到请求头部中 key值为LINGPAI
config.headers.LINGPAI = localStorage.getItem('LINGPAI');
}
return config;
},
error => {
// Do something with request error
return Promise.reject(error);
});
- 登录成功后保存通行证
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
this.$axios
.post('login', {
username: this.ruleForm.username,
password: this.ruleForm.password,
})
.then((res) => {
// console.log(res);
if (res.data.code == 200) {
let token = res.data.info.token
// 将返回的用户信息和令牌写入localStorage
localStorage.setItem('LINGPAI', token)
// 记录用户名
localStorage.setItem('username', this.ruleForm.username)
this.$message('欢迎您!' + this.ruleForm.username)
this.$router.push('/admin')
} else {
alert(res.data.code + ' ' + res.data.msg)
}
//
})
.catch((error) => {
console.log(error)
})
} else {
console.log('error submit!!')
return false
}
})
},