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时由 loginNameidurls 三个参与。
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的工具类)根据 loginNameidurls 三个元素来生成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.htmlmain.js 都跟着刷新,token同时也会在Vuex中变为初始值 ‘’,所以要把token存在seessionStorage(浏览器关闭token就消失),localStorage(只要不删除token就一直存在)中。

2 使用(完整代码在下面一节中)

1.在src–store–index.js中引入持vuex-persistedstate

import persistedState from 'vuex-persistedstate'
  1. 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 要点

  1. 下面代码放在src-router-index.jsexport default router上就行。
  2. .js文件之间调用的话要先引包,所以先引入vuex,用于获取touken。
  3. 定义允许访问的白名单。
  4. 通过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对象中的loginNameid拼接在原有的请求参数上
2.白名单通过@Resource注入进工具类中
3.拼接上新的参数后需要重构新的 URI requestexchange
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;
    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值