Vue+Springboot登入校验
1 登入校验的内容
所有内容包括,验证登入信息,颁发数字签名token,让后续访问每次都携带token,重新封装前端发来的请求,根据不同用户的权限来判断请求是否通过。
2 登入校验成功后用JwtUtil生成token
(这个类在用户模块中)
因为/login
即登入这一权限,在白名单中,所以在下面的第4步中直接跳过token
验证直接用户模块进行登入验证,然后生成token
返回
1 生成和解析token的工具类JwtUtil
(在pojo模块中)
1 导入jwt包
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
2 要点
1.解析token不成功会抛出异常,在第五节的TokenFilter
被接住。
2.生成token时由 loginName
,id
, urls
三个参与。
3.其中urls时ArrayList<String>
类型的权限集合,时user实体类的属性,但不在数据表中,且不会Json序列化即不会传递到前端。
@JsonIgnore
@TableField(exist = false)
private List<String> urls;
package com.wy.util;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.wy.pojo.UmsUser;
import java.util.Arrays;
/**
* @program: JwtUtil
* @Description: TODO
* @Author: sucre1136@gmail.com
*/
public class JwtUtil {
// 根据传入的User对象生成token
public static String token(UmsUser user) {
return JWT.create()
// 构建信息
.withClaim("loginName", user.getLonginName())
.withClaim("id", user.getId())
// urls为用户所对应的权限,用于在TokenFilter中和请求地址相匹配
// 匹配成功才放行
.withClaim("urls", user.getUrls())
// 加签名
.sign(Algorithm.HMAC256("sucre"));
}
// 根据token解析出User
public static UmsUser decoder(String token) throws Exception {
try {
DecodedJWT jwt = JWT.require(Algorithm.HMAC256("sucre"))
.build()
.verify(token);
String loginName = jwt.getClaim("loginName").asString();
Long id = jwt.getClaim("id").asLong();
String[] urls = jwt.getClaim("urls").asArray(String.class);
UmsUser user = new UmsUser();
user.setId(id);
user.setLonginName(loginName);
user.setUrls(Arrays.asList(urls));
return user;
} catch (Exception ex) {
// 解析失败抛出异常
throw new Exception("token不合法");
}
}
}
2 后端的/login
请求的内容,认证成功后返回token
给前端 (在用户模块中)
1 要点
1.先验证用户账号密码是否正确。
2.再从资源权限表中获取改用户的权限的list
集合,这个要自己在资源表中写sql
语句
3.用JwtUtil
(生成token的工具类)根据 loginName
,id
, urls
三个元素来生成token
。
4.在用户实体类中添加一个urls
属性,用来存储后端地址。
/**
* 权限列表
*/
@JsonIgnore
@TableField(exist = false)
private List<String> urls;
5.在资源权限实体类中添加一个children
属性,用于存储资源权限的实体类
/**
* 子菜单
*/
@TableField(exist = false)
private List<UmsResource> children;
// 用于加密密码的工具类
@Resource
BCryptPasswordEncoder passwordEncoder;
// 用户获取当前用户有哪些权限的接口
@Resource
IUmsResourceService resourceService;
// 返回token字符串
public Map<String,Object> login(String username, String password) throws Exception {
QueryWrapper<UmsUser> wrapper = new QueryWrapper<>();
wrapper.eq("phone", username)
.or().eq("longin_name", username)
.or().eq("email", username);
UmsUser user = this.getOne(wrapper);
// BCryptPasswordEncoder只能加密不能解密,所以这里采用匹配的方式
if (user == null || !passwordEncoder.matches(password, user.getPassword())) {
throw new Exception("用户名或密码错误");
}
if (user.getActive() == false) {
throw new Exception("已经失效的用户请联系管理员");
}
List<UmsResource> resources = null;
try {
resources = resourceService.getByUserId(user.getId());
} catch (Exception e) {
e.printStackTrace();
throw new Exception("该用户没有任何权限");
}
// 创建权限和菜单两个list集合
ArrayList<String> urls = new ArrayList<>();
ArrayList<UmsResource> menu = new ArrayList<>();
// 去除重复的权限
for (UmsResource resource : resources) {
// 如果等于0的话说明是目录
if(resource.getType()==0 ){
// 如果是顶级目录直接放入返回值集合,是次级目录就放入顶级目录的子集中
if(resource.getParentId()==0 ){
menu.add(resource);
}else {
for (UmsResource umsResource : resources) {
// 如果resource的上一级目录是umsResource,那么resource的前端地址就放入umsResource的子集中
if(resource.getParentId().longValue()==umsResource.getId().longValue()){
if(umsResource.getChildren()==null){
umsResource.setChildren(new ArrayList<>());
}
if(!umsResource.getChildren().contains(resource.getFrontUrl())){
umsResource.getChildren().add(resource);
}
}
}
}
}
// 如果是后端地址
if (StringUtils.isNotBlank(resource.getBackUrl()) && !urls.contains(resource.getBackUrl())) {
urls.add(resource.getBackUrl());
}
}
user.setUrls(urls);
// 生成token
String token = JwtUtil.token(user);
HashMap<String, Object> map = new HashMap<>();
map.put("token", token);
map.put("menu", menu);
return map;
}
2 登入通过后给token赋值
1 Vuex持久化插件vuex-persistedstate
在Vuex中定义的变量会存在本地文件中,不会因为页面刷新而初始化。
1 安装
npm install vuex-persistedstate
因为每次刷新页面的时候,index.html
和 main.js
都跟着刷新,token
同时也会在Vuex
中变为初始值 ‘’
,所以要把token
存在seessionStorage
(浏览器关闭token
就消失),localStorage
(只要不删除token
就一直存在)中。
2 使用(完整代码在下面一节中)
1.在src–store–index.js中引入持vuex-persistedstate
import persistedState from 'vuex-persistedstate'
- 在
export default new Vuex.Store(){}
中粘贴如下内容(存储在会话中Storage: window.sessionStorage
,还是本地文件中Storage: window.localStorage
看需求)
plugins:[persistedState({
Storage: window.localStorage
Storage: window.sessionStorage
})],
2 在.Vue
文件中Vuex
中定义的变量的赋值和调用
1.赋值
this.$store.commit('SET_TOKEN',response)
2.调用
this.$store.getters.GET_TOKEN
2.完整代码
import Vue from 'vue'
import Vuex from 'vuex'
import persistedState from 'vuex-persistedstate'
Vue.use(Vuex)
export default new Vuex.Store({
plugins:[persistedState({
Storage: window.localStorage
})],
// 用于定义数据
state: {
token: '' ,
menu: ''
},
// 定于数据的set方法
// state是this的意思用户调用
mutations: {
SET_TOKEN: (state,token) =>{state.token = token},
SET_MENU: (state,menu) =>{state.menu = menu}
},
// 给数据定义get方法,
getters: {
GET_TOKEN: (state) =>{return state.token},
GET_MENU: (state) =>{return state.menu}
},
actions: {
},
modules: {
}
})
3 后端登入验证完成后,赋值然并跳转到index页面(在登入界面的login方法下
)
menu是层级菜单的属性,用于生成动态菜单。
login(){
this.post(this.url.login,this.form,response =>{
this.$store.commit('SET_TOKEN',response.token)
this.$store.commit('SET_MENU',response.menu)
this.$router.push('/index')
})
}
4 动态菜单界面
<template>
<el-menu router >
<!-- 父菜单 -->
<el-submenu
v-for="item in menu "
:key="item.id"
:index="item.frontUrl">
<template slot="title">
<span>{{item.name}}</span>
</template>
<!-- 子菜单 -->
<el-menu-item
v-for="child in item.children"
:key="child.id"
:index="child.frontUrl">
{{child.name}}
</el-menu-item>
</el-submenu>
</el-menu>
</template>
<script>
export default {
name: 'MyMenu',
// 获取动态菜单的数据menu
computed: {
menu(){
return this.$store.getters.GET_MENU
}
}
}
</script>
<style scoped lang="less">
.el-menu {
height: 100%;
}
</style>
3 使用Vue的路由来拦截没有登入的请求(没有token
就不让操作前端界面)
1 要点
- 下面代码放在
src-router-index.js
的export default router
上就行。 .js
文件之间调用的话要先引包,所以先引入vuex,用于获取touken。- 定义允许访问的白名单。
- 通过
router.beforeEach
这个方法来拦截没有登入的用户。
// 引入Vuex,用于获取token
import store from '@/store/index.js'
// 定义白名单
const white = ['/login']
// to 表示要去那里
// from表示从哪里来的
// next是一个函数,表示允许通过
router.beforeEach((to,from,next) =>{
// 如果是白名单上的地址就放行
// 防止出现一直进入登入界面的死循环
for(let i = 0 ;i<white.length;i++){
if(to.fullPath===white[i]){
next()
return
}
}
// 如果有token就通过,没有就去登入界面
if(store.getters.GET_TOKEN){
next()
}else{
next('/login')
}
})
export default router
4 让每一个前端的请求都携带token
在src-plugins-axios.js
中修改
1 要点
1.引入Vuex用户获取token
2.在_axios.interceptors.request.use{}
方法中的return config
之前添加如下的判断语句,common['token']
中的token
是后端获取token内数据的属性名,可以修改,但是后端获取的时候一定要对应上。
if(store.getters.GET_TOKEN){
config.headers['token'] = store.getters.GET_TOKEN
}
3.添加完之后代码如下
//引入Vuex用于获取token
import store from '@/store/index'
_axios.interceptors.request.use(
function(config) {
// Do something before request is sent
//如果有token就在请求中带上token
if(store.getters.GET_TOKEN){
config.headers['token'] = store.getters.GET_TOKEN
}
return config;
},
function(error) {
// Do something with request error
return Promise.reject(error);
}
);
5 如果不是白名单中的地址,后端在网关中验证token
的合法性
1 在gateway配置文件中自定义一个白名单配置
注意- /ums-user/login
中-和-
/ums-user/login
之间是由空格隔开的,不然会连带-
一起被当作属性读走
su:
white:
urls:
- /ums-user/login
2 实体类MyWhite
用来接收配置文件中定义的不进行token
验证的白名单
1 导包(用于从配置文件中读取配置然后放入实体类中
)
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-configuration-processor -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<version>2.4.0</version>
</dependency>
2 MyWhite
实体类(这个类在gateway
模块中)
1 注意
1.把白名单写到配置文件中,用于读取配置文件的实体类要加上以下两个注解:
@Component
和 @ConfigurationProperties(prefix ="su.white" )
2.其中第二个注解中的前缀prefix 是配置文件的前两级。
3.实体类属性名必须和配置文件中的第三级一样即 urls
4. 注意 -
和 /ums-user/login
之间要有空格
package com.wy.config;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.List;
/**
* su:
* white:
* urls:
* - /ums-user/login
*
* ConfigurationProperties 注解的前缀为配置文件中的上两级即 prefix ="su.white"
* 注意 "-" 和 "/ums-user/login" 之间要有空格
*
*/
@Component
@ConfigurationProperties(prefix ="su.white" )
public class MyWhite {
// 属性名必须和配置文件中的第三级一样即 urls
private List<String> urls;
public List<String> getUrls() {
return urls;
}
public void setUrls(List<String> urls) {
this.urls = urls;
}
}
3 验证token的过滤器工具类TokenFilter
(这个类在gateway模块中)
1 要点
1.这个类作用是解析token
获得user
对象,把user
对象中的loginName
和id
拼接在原有的请求参数上
2.白名单通过@Resource
注入进工具类中
3.拼接上新的参数后需要重构新的 URI
request
和 exchange
3.在解析token验证合法后再用解析生成的urls
权限集合来匹配请求的地址,如果请求地址再urls中就放行。
4.如果权限写错了的话,就在解析token
后的if
语句判断为false
的那里返回 return chain.filter(exchange);
,这样权限校核就不会有效了,所有请求都会通过。
package com.wy.config;
import com.alibaba.fastjson.JSONObject;
import com.wy.pojo.UmsUser;
import com.wy.util.JwtUtil;
import com.wy.util.ResultJson;
import org.apache.commons.lang3.StringUtils;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.util.UriComponentsBuilder;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import javax.annotation.Resource;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
* @program: TokenFilter
* @Description: TODO
* @Author: sucre1136@gmail.com
*/
@Component
public class TokenFilter implements GlobalFilter, Ordered {
// 这里是通过从配置文件中读取白名单
@Resource
MyWhite myWhite;
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 拿到request和response
ServerHttpRequest request = exchange.getRequest();
ServerHttpResponse response = exchange.getResponse();
// 定义地址匹配器
AntPathMatcher pathMatcher = new AntPathMatcher();
List<String> urls = myWhite.getUrls();
// 匹配后端的白名单 如果有就通过
String url = request.getURI().getPath();
for (String path : urls) {
if (pathMatcher.match(path, url)) {
return chain.filter(exchange);
}
}
/**
* 如果没通过就验证toker
* 获取token "token"是前端添加token时定义的属性名
* 获取到的是一个list集合
* 没token就报没有权限异常
* 有token就获取token
*/
List<String> list = request.getHeaders().get("token");
String token = null;
if (list == null && list.size() <= 0) {
return error(response, ResultJson.auth());
}
token = list.get(0);
//重新构建新的URI
try {
// 解析token 获得User对象
UmsUser user = JwtUtil.decoder(token);
/**
* 认证通过后需要判断是否有操作权限
* 同过解析出来的user对象获取权限地址urls
* 然后拿着情趣地址和urls进行匹配,匹配到了才放行
* url 请求地址 urlList是权限地址
*/
List<String> urlList = user.getUrls();
for (String path : urlList) {
// 如果匹配上了 在构造新的request请求
if (pathMatcher.match(path, url)) {
// 获取到原先的uri用于构建新的uri
URI oldUri = request.getURI();
// 获取到原先的uri的参数
String parmas = oldUri.getRawQuery();
// 用StringBuilder拼接token解析出来的 登录名 和 id
// controll层的方法用"userlogin","userid"来接收这两个属性
StringBuilder builder = new StringBuilder();
if (StringUtils.isNotBlank(parmas)) {
builder.append(parmas).append("&");
}
builder.append("userlogin=" + user.getLonginName())
.append("&userid=" + user.getId());
// 根据原先的 oldUri 和 新的请求参数来构建新的 URI
URI newUri = UriComponentsBuilder.fromUri(oldUri)
.replaceQuery(builder.toString())
.build(true)
.toUri();
// 重新构建一个 request对象 放入新的uri
ServerHttpRequest newRequest = request.mutate().uri(newUri).build();
// 重新构建新的 exchange 对象 放入新的request
ServerWebExchange newExchange = exchange.mutate().request(newRequest).build();
// 返回新的exchange
return chain.filter(newExchange);
}
}
// 如果没进if就报没有权限的错误
return error(response, ResultJson.auth());
} catch (Exception e) {
e.printStackTrace();
// 如果中间出现异常就返回异常信息
return error(response, ResultJson.error(e.getMessage()));
}
}
// 自定义错误异常用于返回给前端 ResultJson是自定义的返回结果 自己定义去
private Mono<Void> error(ServerHttpResponse response, ResultJson resultJson) {
response.getHeaders().set("content-type", "application/jason;charset=utf-8");
DataBuffer dataBuffer = response.bufferFactory()
.wrap(JSONObject.toJSONString(resultJson).getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(dataBuffer));
}
public int getOrder() {
return 0;
}
}