前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]

Posted 小智RE0

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了前后端分离学习笔记 ---[跨域问题,JWT,路由守卫,Axios设置请求拦截和响应拦截]相关的知识,希望对你有一定的参考价值。

跨域问题

跨域是指从一个域名的网页去请求另一个域名的资源。
浏览器对 javascript 具有默认的安全限制,同源处理策略;

同源 : 协议,域名,端口都相同;
只要 协议,域名,端口中任何一个是不同的,就是跨域;

如果一个网页可以随意地访问另外一个网站的资源,那么就有可能在客户完全不知情的情况下出现安全问题。
但是在某些情况下时需要跨域的,这次学习前后端项目分离时,前后端服务器的端口不一样,这就需要跨域进行请求和响应数据信息;或者说在同一项目中需要访问子域系统模块,就需要跨域访问;
那么当然也可以不跨域完成,即在一个打开的窗口中内嵌窗口进行链接请求访问.

后端配置解决跨域

跨域资源共享(Cross-originResource Sharing)

  • 实现跨站访问控制,可安全地进行跨站数据传输;
  • 服务器端对于 CORS 的支持,需要设置 Access-Control-Allow-Origin;要在后台中加上响应头来允许域请求.
  • 当浏览器检测到了配置,才允许 Ajax 进行跨域的访问;

(1) 在类或方法上使用注解@CrossOrigin("...配置Ip和端口");仅达到局部配置跨域的效果[仅在使用了注解的地方生效];

比如在上次搭建的案例中就用了这个注解;前后端分离学习笔记(1) —[Vue基础]

(2)手工设置响应头-----使用 HttpServletResponse 对象来添加响应头
(Access-Control-Allow-Origin)来授权原始域
Origin 的值也可以设置为"*" ,表示全部放行

例如

@RequestMapping("/test")
    @ResponseBody
    public String index(HttpServletResponse response)
        response.addHeader("Access-Control-Allow-Origin", "http://127.0.0.1:5277/");
        return "Hello World";
    

(3)使用配置类完成全局跨域设置—>返回一个新的 CorsFilter Bean,并添加映射路径和具体的 CORS
配置信息

配置类

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
import java.util.Collections;


@Configuration
public class CorsConfig 
    @Bean
    public CorsFilter corsFilter() 
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        //1,允许任何来源
        corsConfiguration.setAllowedOriginPatterns(Collections.singletonList("*"));
        //2,允许任何请求头
        corsConfiguration.addAllowedHeader(CorsConfiguration.ALL);
        //3,允许任何方法
        corsConfiguration.addAllowedMethod(CorsConfiguration.ALL);
        //4,允许凭证
        corsConfiguration.setAllowCredentials(true);

        UrlBasedCorsConfigurationSource source
                = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return new CorsFilter(source);
    

Json web token

传统的基于session访问鉴权机制

在之前的学习中,后端标识信息的处理一般都放在了后端的session中;

根据 http 协议,我们并不能知道是哪个用户发出的请求,所以为了让我们的应用能识别是哪个用户发出的请求,我们只能在服务器存储一份用户登录的信息,这份登录信息会在响应时传递给浏览器,告诉其保存为 cookie,以便下次请求时发送给我们的应用.

那么当出现不同的客户端,不同的用户时,独立的服务器已无法承载更多的用户,而这时候基于 session 认证就出现问题了,服务器可能会受到恶意的信息交互.

  • 扩展性: 用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,限制了负载均衡器的能力 , 限制了应用的扩展能力。
  • CSRF (跨站请求伪造):因为是基于 cookie 来进行用户识别的, cookie 如果被截获,用户就会很容易受到跨站请求伪造的攻击。

基于 token 的访问鉴权机制

它是无状态的,不需要在服务端去保留用户的认证信息或者会话信息。这就意味着基于 token 认证机制的应用不需要去考虑用户在哪一台服务器登录了,便于应用扩展.

  • 用户使用账号和密码发出 post 请求;
  • 服务器使用私钥创建一个 jwt;
  • 服务器返回此 jwt给浏览器;
  • 浏览器将该 jwt 串在请求头中像服务器发送请求;
  • 服务器验证该 jwt;
  • 返回响应的资源到浏览器。


Json web token

JWT 是为在网络应用环境间传递声明而执行的一种基于 JSON 的开放标准((RFC 7519);定义的一种简洁的,自包含的方法用于通信双方之间以 JSON 对象的形式安全的传递信息。
由于是经过签名的,所以通信信息是安全的;JWT 可使用 HMAC 算法/ RSA 的公私秘钥对进行签名.

  • 只要一次登录后;在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证,访问不同的域明也没有问题.
  • 信息交换在通信的双方之间使用 JWT 对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

特点

  • 简洁(Compact): 可以通过 URL,POST 参数或者在 HTTP header 发送,数据量小,当然传输速度也很快.
  • 它具有自包含(Self-contained)性:负载中包含了所有用户所需要的信息,避免了多次查询数据库(但是建议不要将重要信息存入其中[比如密码,私密信息等]);
  • 由于 Token 在传递时会以 JSON 加密的形式发到倒客户端,所以 JWT 是跨语言的,原则上任何 web 形式都支持。
  • 无需在服务端保存会话信息,因为只要系统登录后,它每次接受请求时都会去验证请求头中的token令牌是否符合设定的签名规则,较适用于分布式微服务。

JWT结构
由三部分构成
头部(header)

完整的头部例如:

 'typ': 'JWT',
 'alg': 'HS256'

然后将头部进行 base64 转码,构成了第一部分.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
10010101 01010101
100101 010101 01010

载荷(payload, 保存用户的信息[例如用户Id,账号])
签证(signature).

jwt 的第三部分是一个签证信息,这个签证信息由三部分组成:
• header (base64 后的)
• payload (base64 后的)
• secret

具体的搭建引入
在项目的pom.xml中引入依赖

<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.8.2</version>
 </dependency>

配置类设置

package com.xiaozhi.backserver.startspringboot.util;

import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.auth0.jwt.interfaces.JWTVerifier;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @author by @CSDN 小智RE0
 * @date 2021-12-28 
 */

public class JWTUtil 
    /**
     * jwt生成token
     * @param id 管理员Id
     * @param account 管理员账号
     * @param type    管理员类型
     * @return
     */
    public static String token (Integer id, String account,Integer type)
        String token = "";
        try 
            //过期时间 为1970.1.1 0:0:0 至 过期时间  当前的毫秒值 + 有效时间
            Date expireDate = new Date(new Date().getTime() + 1800*1000);
            //秘钥及加密算法
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            //设置头部信息
            Map<String,Object> header = new HashMap<>();
            header.put("typ","JWT");
            header.put("alg","HS256");
            //携带id,账号信息,生成签名
            token = JWT.create()
                    .withHeader(header)
                    .withClaim("id",id)
                    .withClaim("account",account)
                    .withClaim("type",type)
                    .withExpiresAt(expireDate)
                    .sign(algorithm);
        catch (Exception e)
            e.printStackTrace();
            return  null;
        
        return token;
    

    //验证Token是否还有效; true有效,false失效;
    public static boolean verify(String token)
        try 
            //验签
            Algorithm algorithm = Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE");
            JWTVerifier verifier = JWT.require(algorithm).build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
         catch (Exception e) //当传过来的token如果有问题,抛出异常
            return false;
        
    

    /**
     * 获得token 中playload部分数据,按需使用
     * @param token
     * @return
     */
    public static DecodedJWT getTokenInfo(String token)
        return JWT.require(Algorithm.HMAC256("ZCEQIUBFKSJBFJH2020BQWE")).build().verify(token);
    

后端配置拦截

自定义登录拦截器

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * 登录拦截器
 *
 * */
public class LoginInterceptor implements HandlerInterceptor 
    //预处理-->进入到控制器之前
    //返回false 不进入处理器,返回true,就进入拦截器中执行
    //在控制器之前走这个,决定是否走这个控制器
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception 
        //在请求头中取到token令牌;
        String token = request.getHeader("token");
        //验证token;
        boolean b = JWTUtil.verify(token);
        //若令牌不符合则响应信息为 401;
        if(!b)
            response.getWriter().print(401);
        
        return b;
    


配置

import com.xiaozhi.backserver.startspringboot.util.LoginInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class InterceptorConfig implements WebMvcConfigurer

	public void addInterceptors(InterceptorRegistry registry) 
		InterceptorRegistration inter =  registry.addInterceptor(new LoginInterceptor());
				inter.addPathPatterns("/**");
				//放行地址;登录请求访问路径
				inter.excludePathPatterns("/api/login/login");
	

路由守卫

全局配置
在路由配置的index.js中配置路由守卫,若在当前浏览器的session中没有找到token令牌,则强制将回退到登录页面;

//to-将要访问的页面地址,from-从哪个页面访问的,next-放行函数
rout.beforeEach((to, from, next) => 
	//如果用户访问的登录页,直接放行;
	if (to.path == '/login') 
		return next();
	 else 
		//若没有令牌,则推到登录页面;
		var token = window.sessionStorage.getItem("token");
		if (token == null) 
			return next("/login");
		 else 
			next();
		
	
)

Axios设置前端请求拦截和响应拦截

注意这里的401响应拦截,和后端设置的401登录请求拦截正好形成一个前后闭环;

//导入axios;
import axios from 'axios';
//设置访问后台服务器地址
axios.defaults.baseURL = "http://127.0.0.1:5277/api/";
//将 axios 挂载;
Vue.prototype.$http = axios;

//全局请求拦截和响应拦截配置;
//axios 请求拦截
axios.interceptors.request.use(config => 
	//为请求头对象,添加 Token 验证的 token 字段;
	config.headers.token = window.sessionStorage.getItem('token');
	return config;
)
// 添加响应拦截器
axios.interceptors.response.use((res) => 
	//后端拦截器拦截状态码;
	if (res.data == 401) 
		//清除session信息;
		window.sessionStorage.clear();
		router.replace("/login");
	
	//服务器异常状态码拦截;
	if(res.data.code == 500)
		this.$message(message:resp.data.msg,type: 'warning');
		return;
	
	return res;
);

基础整合一下登录案例

后端这里的话,和之前的差不多,服务层,数据访问层这里就不放了

/**
 * @author by @CSDN 小智RE0
 */
@RestController
@RequestMapping(value = "/api/login")
public class LoginController 


    CommonResult commonResult;

    @Autowired
    LoginService loginService;


    //跨域接收;
    //@CrossOrigin("http://localhost:8080/")
    @RequestMapping(value = "/login")
    public CommonResult login(@RequestBody Admin admin)
        try
            System.out.println(admin);

            Admin admin1 = loginService.loginUser(admin);
            if(admin1!=null)
                String token = JWTUtil.token(admin1.getId(), admin1.getAccount(),admin1.getType());
                admin1.setToken(token);

                commonResult = new CommonResult(200,"正确",admin1);
            else
                commonResult = new CommonResult(201,"账号或密码错误",null);
            
        catch (Exception e)
            e.printStackTrace();
            commonResult = new CommonResult(500,"服务器错误",null);
        
        return commonResult;
    

需要注意的是,数据响应时,注意分级
响应时同样还是用一个公共类进行封装,数据存储在属性data中;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @author by @CSDN 小智RE0
 * @date 2021-12-28 16:02
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class CommonResult 
    private Integer code;
    private String msg;
    private Object data;

在取得响应的数据时需要写两级data,第一级的data是axios数据请求响应时的数据域,第二级才是公共类中的属性data数据;

前端登录组件Login.vue

<template>
  <div class="login_container">
     <!-- 登录盒子-->
     <div class="login_box">
          <!-- 头像盒子-->
          <div class="img_box">
                <img src="../assets/logo.png" />
          </div>
        <div style="margin-top: 100px; padding-right: 30px;">
			<!-- 登录表单-->
			 <el-form ref="form" :model="form" label-width="80px">
			   <el-form-item label="账号">
			     <el-input v-model="form.account"></el-input>
			   </el-form-item>
			   <el-form-item label="密码">
			     <el-input type="password" v-model="form.password"></el-input>
			   </el-form-item>
			   
			   <el-form-item>
			     <el-button type="primary" plain @click="login()">登录</el-button>
			     <el-button>取消</el-button>
			   </el-form-item>
			 </el-form>
		</div>
     </div>
  </div>
</template>

<script>
	export default
		data:function()
			return
				form:
					account:"",
					password:""
				
			
		,
		methods:
		  login()
			  //注意这里定义 _this, this是vue对象;
			  var _this = this;
			  //console.log(this.form);
			  this.$http.post("login/login",this.form).then(function(resp)
				  //回调函数;
				 console.log(resp);
				 //警告消息弹框;
				  if(resp.data.code == 201)
					  _this.$message(message:resp.data.msg,type: 'warning');
					  Python全栈100天学习笔记Day48 前后端分离开发入门

前后端分离学习笔记 --[管理员权限分配操作菜单案例]

Swagger笔记

B站云E办Vue+SpringBoot前后端分离项目——MVC三层架构搭建后台项目

第二季SpringBoot+Vue前后端分离项目实战笔记

springboot+vue前后端分离项目(后台管理系统)