SpringBoot回顾5-拦截器、跨域开启

151 阅读4分钟

SpringBoot回顾5-拦截器、跨域CORS开启

本文已参与「新人创作礼」活动,一起开启掘金创作之路。

本篇写一下在SpringBoot中拦截器功能的实现,再配合一个登陆拦截的demo。最后再讲讲什么是CORS。

SpringBoot中的拦截器

在SpringBoot1.5之前,都需要重写WebMvcConfigurerAdapter来添加自定义拦截器(已经@Deprecated了)

image-20201008095018231

在SpringBoot2.0后,推荐实现WebMvcConfigurer或者继承WebMvcConfigurationSupport来实现。

WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式代替传统的xml配置文件形式来进行对框架的个性化配置,可以自定义一些Handler、Interceptor、ViewResolver、MessageConverter等。

新建一个SpringBoot项目,我们在config包下创建自己的MvcConfig,我们来看看接口中有哪些方法可以实现

1、实现WebMvcConfigurer接口

乍一看有这么多可以实现的方法,我们看看常用的有哪些

image-20201008095121965

常用的方法

 /* 拦截器配置 */
default void addInterceptors(InterceptorRegistry registry) {
	}
/* 视图跳转控制器 */
default void addViewControllers(ViewControllerRegistry registry) {
	}
/**
     *静态资源处理
**/
default void addResourceHandlers(ResourceHandlerRegistry registry) {
	}
/* 默认静态资源处理器 */
default void configureDefaultServletHandling(DefaultServletHandlerConfigurer configurer) {
	}
/**
     * 这里配置视图解析器
 **/
default void configureViewResolvers(ViewResolverRegistry registry) {
	}
/* 配置内容裁决的一些选项*/
default void configureContentNegotiation(ContentNegotiationConfigurer configurer) {
	}
/** 解决跨域问题 **/
default void addViewControllers(ViewControllerRegistry registry) {
	}

我们知道在SpringBoot项目中的静态资源都是放在resources下,可以写一个静态资源处理的配置方法

image-20201008095615293

静态资源不拦截

public class MyMvcConfig implements WebMvcConfigurer {

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/static/***")
                .addResourceLocations("classpath:/static/");

    }
}

这样设置的话就表示静态资源路径(/static)下的访问不会被拦截

那请求路径拦截呢?

注意这里也自己先写好拦截器,才能注册生效

@Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/index.html","/","/user/login","/css/*","/js/**","/img/**");
    }

自定义登录拦截器

package com.feng.interceptor;

import org.springframework.web.servlet.HandlerInterceptor;

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

/**
 * <h3>springboot-interceptor-cors-test</h3>
 * <p></p>
 *
 * @author : Nicer_feng
 * @date : 2020-10-08 10:04
 **/
public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //登陆成功之后应该有用户的session
        Object loginUser = request.getSession().getAttribute("loginUser");
        if(loginUser==null) {//没有登陆
            request.setAttribute("msg","请先登录");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        }else {
            return true;
        }
    }
}

默认访问路径

@Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("").setViewName("index");
    }

这里表示我们只需要输入 http://localhost:8080/即可访问到index.html

image-20201008101355758

登陆拦截demo实现

先看一下项目一览

image-20201008140800532

先编写一个controller类

LoginController.java

package com.feng.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.thymeleaf.util.StringUtils;

import javax.servlet.http.HttpSession;
import java.util.Map;
@Controller
public class LoginController {

    @RequestMapping("login")
    public String index()
    {
        return "login";
    }

    @PostMapping(value="/login")
    public String login(@RequestParam("username") String username, @RequestParam("password") String password, Map<String,Object> map,
                        HttpSession session){

        //验证用户名和密码,输入正确,跳转到dashboard
        if(!StringUtils.isEmpty(username)&&"123".equals(password)){

            session.setAttribute("userName",username);
            System.out.println("----" + username);
            map.put("age",30);
            return "redirect:/dashboard";

        }
        else  //输入错误,清空session,提示用户名密码错误
        {
            session.invalidate();
            map.put("msg","用户名密码错误");
            return "login";
        }
    }


    @RequestMapping("dashboard")
    public String goMain(Map<String,Object> map)
    {
        map.put("name","冯半仙");
        map.put("age",22);
        map.put("sex","男");
        return "dashboard";
    }
}

发送post请求,代替了RequestMapping(value="/login",method="post")

跳转页面这里用了 return "redirect:/dashboard";会直接调用controller,

return "dashboard"只会访问目录下的文件

我们简单的写一个login页面

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    <p style="color:red" th:text="${msg}"  th:if="${not #strings.isEmpty(msg)}"></p>
    username<input type="text" name="username">
    password<input type="password" name="password">
    <button type="submit">login</button>

</form>

</body>
</html>

这里面用了thymeleaf模板,如果后台校验后密码不正确,th:text="msg"th:if="{msg}" th:if="{not #strings.isEmpty(msg)}会存在值,则会生效。

action配置为th:action="@{/login}" method="post",告诉模板跳转的post请求是/login

很简单的前端页面,几乎没有美化,先看能不能用,等下再美化

再写一个登陆成功返回的页面

dashborad.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<h1>注册成功</h1>
姓名:<spn th:text="${name}"></spn>
年龄:<spn th:text="${age}"></spn>
性别:<spn th:text="${sex}"></spn>
</body>
</html>

此时我们是没配置拦截器的,跑起来以后发现在地址栏直接输入http://localhost:8080/dashboard也能直接访问后台地址,显然是不允许的。所以我们需要配置拦截器,当用户名和密码不正确时,页面跳回登录页面,并显示无权限访问

GIF 2020-10-8 14-19-53

添加HandlerInterceptor拦截器

在com.feng下新建component包,取名LoginHandlerInterceptor实现HandlerInterceptor接口,并实现三个接口,全部选上

image-20201008142443108

可以看到里面有三个方法,

preHandle 在请求处理之前进行调用(Controller方法调用之前)

postHandle 请求处理之后进行调用,但是在视图被渲染之前(Controller方法调用之后)

afterCompletion 在整个请求结束之后被调用,也就是在DispatcherServlet 渲染了对应的视图之后执行 (主要是用于进行资源清理工作)

LoginHandlerInterceptor.java

package com.feng.component;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
public class LoginHandlerInterceptor implements HandlerInterceptor {
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        Object user = request.getSession().getAttribute("userName");
        System.out.println("afterCompletion----" + user + " ::: " + request.getRequestURL());
    }

    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        Object user = request.getSession().getAttribute("userName");
        System.out.println("postHandle----" + user + " ::: " + request.getRequestURL());

    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object user = request.getSession().getAttribute("userName");
        System.out.println("preHandle----" + user + " ::: " + request.getRequestURL());

        if (user == null) {
            request.setAttribute("msg","无权限请先登录");
            // 获取request返回页面到登录页
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        }
        return true;
    }

}

我们的前置方法先去从session里看userName有没有值(我们前面在controller中已经把userName存入session中),如果没有值就强行返回到登录页面,并把未登录警告封装到msg中传给前端

最后是配置自己的拦截器

MyWebMvcConfigurer.java

package com.feng.component;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class MyWebMvcConfigurer implements WebMvcConfigurer {
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/index.html").setViewName("login");
        //设置login为index下的视图解析器,输入localhost:8080/index仍然跳转login
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        //	"/**"过滤所有请求,后面的excl内表示放行的请求
        registry.addInterceptor(new LoginHandlerInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login","/index.html")
                .excludePathPatterns("/assets/**");

    }

}

运行

可以看到除了首页,我们那儿也去不了,除非登录成功

GIF 2020-10-8 14-31-54

优化前端页面

是不是觉得界面太丑了,我们可以用Bootstrap来优化前端页面

我们把login页面替换为如下

login.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Title</title>
    <link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
    <link href="assets/css/signin.css"  rel="stylesheet">
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.4.0/jquery.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/js/bootstrap.min.js"></script>
</head>
<body class="text-center">
<form  class="form-signin" th:action="@{/login}" method="post">

    <img class="mb-4" src="assets/img/bootstrap-solid.svg" alt="" width="72" height="72">
    <h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
    <p style="color:red" th:text="${msg}"  th:if="${not #strings.isEmpty(msg)}"></p>
    <label class="sr-only" >Username</label>
    <input type="text" name="username" class="form-control" placeholder="Username"
           required="" autofocus="">
    <label class="sr-only" >Password</label>
    <input type="password" name="password" class="form-control" placeholder="Password"
           required="">
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me"/> Remember me
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit" >Sign in</button>
    <p class="mt-5 mb-3 text-muted">© 2019-2020</p>

</form>

</body>
</html>

还有一个signin.css

html,
body {
  height: 100%;
}

body {
  display: -ms-flexbox;
  display: flex;
  -ms-flex-align: center;
  align-items: center;
  padding-top: 40px;
  padding-bottom: 40px;
  background-color: #f5f5f5;
}

.form-signin {
  width: 100%;
  max-width: 330px;
  padding: 15px;
  margin: auto;
}
.form-signin .checkbox {
  font-weight: 400;
}
.form-signin .form-control {
  position: relative;
  box-sizing: border-box;
  height: auto;
  padding: 10px;
  font-size: 16px;
}
.form-signin .form-control:focus {
  z-index: 2;
}
.form-signin input[type="email"] {
  margin-bottom: -1px;
  border-bottom-right-radius: 0;
  border-bottom-left-radius: 0;
}
.form-signin input[type="password"] {
  margin-bottom: 10px;
  border-top-left-radius: 0;
  border-top-right-radius: 0;
}

那个图标的话自己随便放个图就行

效果如下

image-20201008152559205

就完事儿了,下面讲讲跨域

跨域

首先说明,跨域问题主要存在浏览器端,在我们后台java是不存在的问题。跨域的请求也能发出去,服务器也能收到并返回正确结果,但是结果被浏览拦截了

之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。换句话说,浏览器安全的基石是同源策略。

在SpringBoot中解决跨域我们主要是用Cors来解决

什么是Cors?

CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它通过服务器增加一个特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。

测试

我们创建两个SpringBoot项目

image-20201008154612399

两个项目中都添加web和thymeleaf依赖

		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

在provider项目的配置类里端口配置为8080

server.port=8080

而consumer的配置为8081

server.port=8081

在provider项目下的resources/templates目录下创建index.html

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Cors跨域请求</title>
    <script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.2.0/jquery.min.js"></script>
</head>
<body>
<input type="button" onclick="getRequest()" value="GET请求">
<input type="button" onclick="postRequest()" value="POST请求">
<script>
    function getRequest() {
        $.get('http://localhost:8081/index', function (res) {
            $('body').html(res);
        })
    }

    function postRequest() {
        $.post('http://localhost:8081/index', function (res) {
            $('body').html(res);
        })
    }
</script>
</body>
</html>

然后在consumer项目下创建一个controller层

新建

IndexController.java

package com.feng.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * <h3>springboot-cors-consumer</h3>
 * <p></p>
 *
 * @author : Nicer_feng
 * @date : 2020-10-08 15:36
 **/

@RestController
public class IndexController {
    @GetMapping("index")
    public String gindex() {
        return "get hello;";
    }

    @PostMapping("index")
    public String pindex() {
        return "post hello;";
    }
}

第一次运行

两个项目都跑起来,我们点击get post请求后发现报错

GIF 2020-10-8 15-43-13

报错

可以看到报错为

Access to XMLHttpRequest at 'http://localhost:8081/index' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.

翻译一下大概意思是说我们从8080端口访问8081收到了CORS的限制,没有开启跨域

开启跨域

我们只需要在consumer项目中的controller上对应的映射路径加上@CrossOrigin(value = "http://localhost:8080"),表示允许从8080端口访问即可,加入后重启consumer项目

image-20201008160026099

第二次运行

可以看到成功访问到8081中的项目资源

GIF 2020-10-8 16-02-00

全局配置

还记得博客刚开始中的WebMvcConfigurer接口里有个眼熟的方法吗

image-20201008160415599

没错呀,我们也可以通过项目配置,为整个项目开启跨域,咋整?也实现这个方法,新建一个CorsConfig类

image-20201008160513763

CorsConfig.java

package com.feng.config;

import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

/**
 * <h3>springboot-cors-consumer</h3>
 * <p></p>
 *
 * @author : Nicer_feng
 * @date : 2020-10-08 16:03
 **/
public class CorsConfig implements WebMvcConfigurer {
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        //所有请求都会处理跨域
        registry.addMapping("/**")
                // 允许谁访问
                .allowedOrigins("http://localhost:8080")
                // 允许通过的请求数
                .allowedMethods("*")
                // 允许的请求头
                .allowedHeaders("*");
    }
}

我们把刚才controller上配置的@CrossOrigin注释掉,重启服务器

第三次运行

GIF 2020-10-8 16-09-40

收工