SpringBoot回顾5-拦截器、跨域CORS开启
本文已参与「新人创作礼」活动,一起开启掘金创作之路。
本篇写一下在SpringBoot中拦截器功能的实现,再配合一个登陆拦截的demo。最后再讲讲什么是CORS。
SpringBoot中的拦截器
在SpringBoot1.5之前,都需要重写WebMvcConfigurerAdapter来添加自定义拦截器(已经@Deprecated了)
在SpringBoot2.0后,推荐实现WebMvcConfigurer或者继承WebMvcConfigurationSupport来实现。
WebMvcConfigurer配置类其实是Spring内部的一种配置方式,采用JavaBean的形式代替传统的xml配置文件形式来进行对框架的个性化配置,可以自定义一些Handler、Interceptor、ViewResolver、MessageConverter等。
新建一个SpringBoot项目,我们在config包下创建自己的MvcConfig,我们来看看接口中有哪些方法可以实现
1、实现WebMvcConfigurer接口
乍一看有这么多可以实现的方法,我们看看常用的有哪些
常用的方法
/* 拦截器配置 */
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下,可以写一个静态资源处理的配置方法
静态资源不拦截
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
登陆拦截demo实现
先看一下项目一览
先编写一个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="{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也能直接访问后台地址,显然是不允许的。所以我们需要配置拦截器,当用户名和密码不正确时,页面跳回登录页面,并显示无权限访问
添加HandlerInterceptor拦截器
在com.feng下新建component包,取名LoginHandlerInterceptor实现HandlerInterceptor接口,并实现三个接口,全部选上
可以看到里面有三个方法,
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/**");
}
}
运行
可以看到除了首页,我们那儿也去不了,除非登录成功
优化前端页面
是不是觉得界面太丑了,我们可以用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;
}
那个图标的话自己随便放个图就行
效果如下
就完事儿了,下面讲讲跨域
跨域
首先说明,跨域问题主要存在浏览器端,在我们后台java是不存在的问题。跨域的请求也能发出去,服务器也能收到并返回正确结果,但是结果被浏览拦截了
之所以会跨域,是因为受到了同源策略的限制,同源策略要求源相同才能正常进行通信,即协议、域名、端口号都完全一致。浏览器出于安全的考虑,使用 XMLHttpRequest对象发起 HTTP请求时必须遵守同源策略,否则就是跨域的HTTP请求,默认情况下是被禁止的。换句话说,浏览器安全的基石是同源策略。
在SpringBoot中解决跨域我们主要是用Cors来解决
什么是Cors?
CORS是一个W3C标准,全称是”跨域资源共享”(Cross-origin resource sharing),允许浏览器向跨源服务器,发出XMLHttpRequest请求,从而克服了AJAX只能同源使用的限制。它通过服务器增加一个特殊的Header[Access-Control-Allow-Origin]来告诉客户端跨域的限制,如果浏览器支持CORS、并且判断Origin通过的话,就会允许XMLHttpRequest发起跨域请求。
测试
我们创建两个SpringBoot项目
两个项目中都添加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请求后发现报错
报错
可以看到报错为
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项目
第二次运行
可以看到成功访问到8081中的项目资源
全局配置
还记得博客刚开始中的WebMvcConfigurer接口里有个眼熟的方法吗
没错呀,我们也可以通过项目配置,为整个项目开启跨域,咋整?也实现这个方法,新建一个CorsConfig类
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注释掉,重启服务器
第三次运行
收工