梦入琼楼寒有月,行过石树冻无烟

📖 earlier posts 📖

Spring security 跨域与CORS

跨域即分为三种,分别为协议跨域、主机跨域、端口跨域等,其中主要根据协议不同、域名不同、端口不同,在这三种之中只要有一个不同,就算是跨域。就比如你访问 http://pv.zsun.org.cn,然后还访问了https://pv.zsun.org.cn/这就对应了跨域中的协议不同,一个时http协议另一个则是https协议,所以造成了跨域。为了解决跨域的问题,其中主要包括了JSONP、NGINX转发、CORS等,其中JSONP和CORS都需要后端进行参与。

CORS

CORS(Cross-Origin Resource Sharing,跨域资源共享)是一个系统,他由一系列的传输HTTP头组成,这些HTTP头将会决定浏览器是否阻止前端JavaScript代码来获取请求相应,同源安全策略是默认阻止“跨域”请求,但是如果使用CORS则会给web服务器一个选择、允许和拒绝请求访问到他们的资源。

就比如Origin来发起请求域:

  1. Access-Control-Request-Method:将要发起跨域请求的方式(POST/GET……)
  2. Access-Control-Request-Headers:将要发起跨域请求中包含的请求头字段
    1. 服务器在响应字段中来表明是否允许这个跨域请求,浏览器收到后如果检测到不合要求,就会拒绝后面的请求
  3. Access-Control-Allow-Origin:允许那些域来访问
    1. *表示所有的域请求
  4. Access-Control-Allow-Method:允许那些请求方式
  5. Access-Control-Allow-Headers:允许那些请求头字段
  6. Access-Control-Credentials:是否允许携带Cookie

Spring security 实现CORS

CrossOrigin

DemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class DemoApplication {

@GetMapping("/")
@CrossOrigin(origins = "http://localhost:8080")
public String index() {
return "The is Spring security world!";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

WebMvcConfigurerAdapter

WebCorsConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;

@Configuration
public class WebCorsConfig {
@Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
@Override
public void addCorsMappings(CorsRegistry registry) {
/*
addMapping 所有域都能访问
allowedOrigins 那些域可以被访问
allowedMethods 允许那些请求
allowedHeaders 允许那些请求字段
allowCredentials 是否允许携带Cookie
*/
registry.addMapping("/**")
.allowedOrigins("http://localhost:8080")
.allowedMethods("POST","GET")
.allowedHeaders("*")
.allowCredentials(true);
}
};
}
}

Spring security 密码加密

在注册的过程中,通常会有一定的入库处理机制,以钟山计算机端口安全联合众测平台为例,密码入库时都是由MD5运算加密后的密文,以防止不法分子通过相关技术手段对站点进行脫库时明文密码的直接获取。通过使用密文给不法分子增加技术难度从而留有一定的解密时间来提醒用户尽快修改密码等公告的发布与通知。在目前,我们通常会使用MD5、SHA等摘要算法,尽管有很多在线解密的站点,通常在密码加密的场景中,我们会将用户输入的明文密码进行MD5或HAS运算后存储到数据库,之后在用户登录的时候将数据和数据库中的data进行对比的这种方案。

密文的解密方式

通常我们可通过碰撞,即存在两个甚至多个得到同一直的情况下就会存在被碰撞的可能性。当然还有就是最直接的方法构建一张反查表,就是说将密码经过md5后运算出的直来进行穷举,如果该站点数据库中有部分密码和反查表中大部分数据收录,则很容易被进行破解至明文。在或者还有一种非常暴力的方式则是“暴力破解”,主要依靠的是强大的字典而不是使用者的技术实力,按照道理来讲,只要你的字典足够丰富且完善,成功率就越高,如果你的字典跑完了还没有破解成功的话,那基本上是意味着失败了。

彩虹表

起初黑客们通过字典、碰撞的方式进行破解,对于简单的密码则非常有用,但是如果遇到复杂的密码和密码系统,就会产生较大的字典,为了解决逆向破解的难题,并增加表的容量,瑞士洛桑联邦技术学院的Philippe Oechslin就提出了一种优化型的时间与内存交换算法。

这让表不在是只存储米嗯问与密文的一一对应关系,而是一个密码对的集合,并在有限的内存中以极高的效率破解更多可能的密码,这也被一些黑客改进了部分流程,最后成为了一种彩虹表。

彩虹表的大小通常都超过了100g以上,彩虹表每一条散列链都会代表一组相同特征的铭文,但每一条散列链并不需要完整的存储,仅仅存储开始节点和结束节点即可。例如图中的H则代表一次散列计算,R1\R2\R3代表一次递归性质的约减计算R旁边的字母则会代表一条散列链会经历3次约减,也就是说这条散列链可以携带三条铭文,后面的都会由K所指代。当我们需要破解一个散列的时候,只需要对其做相应的约减计算,便可以推导出其所在的散列链,再有散列链正推就可能会得到该散列链的明文,由于K的存在,所以可以携带更多的数据。虽然彩虹表看似如此强大,但是如果当遇到被加盐的时候,也是会变得弱不禁风的。

实现密码加密

UserServiceImpl.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.example.zhongce.service;
import com.example.zhongce.model.User;
import com.example.zhongce.repository.RoleRepository;
import com.example.zhongce.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.HashSet;

@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserRepository userRepository;

@Autowired
private RoleRepository roleRepository;

@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;

@Override
public void save(User user) {
user.setPassword(bCryptPasswordEncoder.encode(user.getPassword()));
user.setRoles(new HashSet<>(roleRepository.findAll()));
userRepository.save(user);
}

@Override
public User findByUsername(String username) {
return userRepository.findByUsername(username);
}
}

具体代码可通过阅读或浏览钟山计算机端口安全联合众测平台进行得知:https://gitee.com/zhongshan_union/Zhongshan-computer-port-security-joint-public-testing-platform/blob/master/src/main/java/com/example/zhongce/service/UserServiceImpl.java

Spring security 会话管理

会话管理的基本概念可以理解为,管理会话的限制,就比如在大型的站点之中,基本上都是不允许多个帐号同时登录的,就比如腾讯QQ,假如PC1已经登入QQ,PC2再次登入QQ则会出现PC1QQ已经被踢下线的情况。而在网络设备中,只要该设备被登录,就不会允许再次登录的这种功能效果,Spring security为我们提供了完善的会话管理功能,和安全防御手段如:会话固定攻击,会话超时检测以及会话并发控制等。

什么是会话?

会话(session)即无状态的HTTP实现用户状态可维持的一种解决方案,在通常的情况每个请求之间都没有相互的关联性,也就是说用户访问的时候没有一个身份记录,也就是说谁都访问了,但是我不知道也查不到。session的出现解决了这个问题,服务器通过与用户达成一个约定,每个请求之间都携带着一个id类的信息

在用户首次访问的时候,系统会为该程序生成一个sessionId,并因此添加至cookie之中,当用户的会话期限内,每个请求都会自动携带该cookie,因此系统会轻易的判断这个识别是来此那个一个用户的请求。

Spring security 的会话固定攻击防御

在spring security之中,会话固定个攻击的防御手段也可通过用户登录之后重新生成新的session,在继承WebSecurityConfigurationAdapter的时候,Spring security则已经启用了这种配置。

sessionManagement

sessionManagement是一个会话管理的配置器,其中防御会话固定攻击的策略分为四种分别为:

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.and()
.rememberMe()
.userDetailsService(userDetailsService)
.and()
// 开启session会话管理
.sessionManagement()
.sessionFixation().none();
}

}

实际上在Spring security之中,即使没有配置,也不需要太过担心会话固定攻击,因为Security之中的HTTP防火墙或控制器会帮助我们拦截一定的不合法URL请求,就比如我压根没配置index,你访问了只能是Error Page的错误页。

Spring security 会话管理配置

在Spring security之中,我们可通过invalidSessionUrl来指定会话的失效时重定向的URL(默认重定向登录页面)。当然也可通过maximumSession来指定用户最大的并发数量(-1表示无限制),这里我们主要列举几个简单的例子来实现。

失效处理与失效时间

通过sessionManagement().invalidSessionUrl();来设置会话失效后重定向的页面,这里我们在application.propertoes中分别设置session与cokie的生命周期:

application.properties

1
2
3
4
5
6
7
8
spring.security.user.name=kun
spring.security.user.password=123

# 根据提示默认时间是30min(单位为秒),这里我们设置60m(一分钟),通过查看 TomcatServletWebServerFactory
# 我们可以得知session的默认时间是1分钟
server.servlet.session.timeout=60
# 这里是cookie的生命周期,单位为秒,-1是无限制
server.servlet.session.cookie.max-age=-1

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.and()
.rememberMe()
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionUrl("/myLogin.html");
}

}

由于我们并没有自定义登录页面所以```/myLogin.html `` `文件坐在resource/static目录下为空,所以在session会话失效的时候自然就跳转到了spring security为我们提供的默认登录页面之中,读者可根据需求自行进行修改与调整。

session 最大会话数量

通过.maximumSessions我们可以管理session中同一用户的最大会话数量,如果.maximumSessions设置为”-1“则表示无限制,除此之外的其他数字都将会被限制。而本次我们通过WebSecurityConfig.java中的.invalidSessionUrl来指定其同一用户登入后如session没过期的情况下跳转至/myLogin.html页面,而.maxSessionsPreventsLogin所表示的则是,用户session会话达到.maximumSessions的限制后,新的请求session将会被拒绝登录。

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.and()
.rememberMe()
.userDetailsService(userDetailsService)
.and()
.sessionManagement()
.invalidSessionUrl("/myLogin.html")
.maximumSessions(1).maxSessionsPreventsLogin(true);
}

}

Spring security 自动登录

自动登录在很多登入页面中都有一定的需求和存在,有时候处于站点的相关密码安全等级设置,所以会要求用户在输入密码的时候要大于或等于多少个数字或字母,有的甚至会要求大小写和一些符号混合。这就会改变我们经常使用的密码,比如我经常使用的密码是wwcx2022.,但该站点所要求的密码规范是大小写,所以就会改变成Wwcx2022.,有的甚至会要求不允许密码符号,如钟山计算机端口安全联合漏洞共享平台中的前端和后端验证都不允许标点符号的存在。
所以在登录的时候会多个密码进行重试,有的甚至会选择忘记密码而增加站点的处理,为减少用户重新提交表单和加程序处理的频率,在系统开发之中会要求用户记住密码(俗称自动登录)的功能体验,这样做的唯一好处就是可以减少程序的处理数量和给用户带来便利,本文我们使用Spring security来实现自动登录,当然还有一些使用数据库来实现的自动登录,网上有很多文章可以用于实现,读者可自行进行学习,在项目开发的过程中,我们一般建议较少的使用数据库,减少系统资源。

DemoApplication.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class DemoApplication {

@GetMapping("/")
public String index() {
return "The is Spring security world!";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

}

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demo.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.UserDetailsService;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private UserDetailsService userDetailsService;

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and().formLogin()
.and()
.rememberMe()
.userDetailsService(userDetailsService)
.and()
.csrf().disable();
}

}

Spring security 验证码

验证码(Completely Autmated Public Turing test to tell Computers and Humans Apart,CAPTCHA),中文全称为“全自动区分计算机和人类的图灵测试”,主要运用在登录、注册、提交等表单并以防止其暴力破解以及频繁提交的一种功能。
在目前,验证码技术已经衍生到滑块验证、图形验证、拼图验证等,但验证码依然被广泛使用,因其不美观而被滑块等隐藏的方式所替代,本书作者本来是不想将验证码加入进来,但实在是觉得不应该,所以才写,本书使用hutool工具集简单实现验证码。

再次之前我们需要在pom.xml文件下加入hutool的个依赖:

1
2
3
4
5
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-captcha</artifactId>
<version>5.3.10</version>
</dependency>

Controller

CaptchaController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
package com.example.demo.controller;

import cn.hutool.captcha.CaptchaUtil;
import cn.hutool.captcha.CircleCaptcha;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.OutputStream;


@Slf4j
@RestController
public class CaptchaController {
public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

@GetMapping("/code/image")
public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
response.setCharacterEncoding("UTF-8");
response.setHeader("Pragma","No-chache");
response.setHeader("Cache-Control","no-chache");
response.setDateHeader("Expires",0);
response.setContentType("image/jpeg");

/*
width @宽
height @高
codeCount @验证码位数
circleCount @循环次数
*/
CircleCaptcha captcha = CaptchaUtil.createCircleCaptcha(100,20,4,3);
System.out.println(captcha.getCode());

request.getSession().setAttribute(SESSION_KEY_IMAGE_CODE, captcha.getCode());
log.info("Captcha:" + captcha.getCode() + ",already arrive deposit HttpSession");

OutputStream outputStream = response.getOutputStream();
captcha.write(outputStream);
outputStream.flush();
outputStream.close();
}
}

configuration

WebSecurityConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
package com.example.demo.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/code/image").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
.permitAll()
.and()
.csrf().disable();
}
}

resources

static

myLogin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<html>
<head>
<meta charset="UTF-8">
<title>Loing - by Spring security</title>
</head>
<body>
<h1>The is Spring security by html</h1>
<form action="/myLogin" method="post">
<input type="text" name="username" placeholder="user"/>&nbsp;
<input type="password" name="password" placeholder="pass"/>
&nbsp;
<input type="text" placeholder="Captcha">
<img src="/code/image">&nbsp;
<input type="submit" value="login go">
</form>
</body>
</html>

Spring security 授权

controller

AdminController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.dome.controller;

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

@RestController
@RequestMapping("/admin/api")
public class AdminController {
@GetMapping("index")
public String index() {
return "hello, admin";
}
}

Userontroller.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.dome.controller;

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

@RestController
@RequestMapping("/user/api")
public class UserController {
@GetMapping("index")
public String index() {
return "hello, user";
}
}

AppController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.example.dome.controller;

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

@RestController
@RequestMapping("/app/api")
public class AppController {
@GetMapping("index")
public String index() {
return "hello, app";
}
}

configuration

WebSecurityConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.dome.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/aip/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
}

通过上述我们所定义的不同权限,分别为管理员、用户、匿名,所分配的权限不同,而管理员和用户是需要登入才可以访问的,匿名帐号app则可访问拥有权限下的所有资源。通过访问localhost:8080/user/api/index我们可以得知,Spring security的默认帐号角色,就是user。

HTTP 状态码(HTTP Status Code)是由RFC 2016定义的一种用于表示一个HTTP请求响应状态的规范,总共由三位数字组成。通常由2XX表示操作成功、4XX表示客户端导致的失败,5xx表示服务器引起错误。

多用户

WebSecurityConfig.java (InMemoryUserDetailsManager)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
package com.example.dome.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.antMatchers("/admin/api/**").hasRole("ADMIN")
.antMatchers("/user/aip/**").hasRole("USER")
.antMatchers("/app/api/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin();
}
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager inMemoryUserDetailsManager = new InMemoryUserDetailsManager();
inMemoryUserDetailsManager.createUser(User.withUsername("user").password("123").roles("USER").build());
inMemoryUserDetailsManager.createUser(User.withUsername("admin").password("123").roles("USER","ADMIN").build());
return inMemoryUserDetailsManager;
}
@Bean
public static PasswordEncoder passwordEncoder() {
return NoOpPasswordEncoder.getInstance();
}
}

对于Spring security中的多用户,实际上spring security主要拥有三种权限,分别为管理员(admin),用户(user)以及匿名/访客(visitor),可通过数据库或Spring security中的配置类进行实现。在写项目的过程中建议这种授权情况不要使用数据库进行授权,通过配置文件就可以实现,首先确定管理员和用户,

https://gitee.com/zhongshan_union/Zhongshan-computer-port-security-joint-public-testing-platform/blob/master/src/main/java/com/example/zhongce/WebSecurityConfig.java

然后访客权限可通过antMatchers来实现访客权限可以访问的页面,这种方式也被开源项目钟山计算机端口安全联合众测平台使用。

Spring security 创建项目与自定义登陆页

本文通过idea进行一些实例演示,通过Spring Initializer工具来创建spring security项目的例子,首先我们在自动构建项目中选择 Spring security 以及 Spring Web,然后点击next即可进行下一步,通过Spring Initializer来选定一些常用的项目以来,当创建项目完成后,Idea将会自动生成pom.xml文件,我们可以通过该文件看出Spring Initializr总共引入了那些依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

DomeApplication.class

在程序的启动类中,我们可以通过设置一个程序入口来访问,之后Spring security会在访问的时候直接使用其自带的登入页面,而登入页面的帐号及其密码则通过application.properties即spring全局配置文件中进行配置,启动类代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.dome;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@SpringBootApplication
public class DomeApplication {
@GetMapping("/")
public String index() {
return "Helo,world,the is Spring security world.";
}
public static void main(String[] args) {
SpringApplication.run(DomeApplication.class, args);
}

}

application.properties

1
2
3
// user@帐号 passowrd@密码
spring.security.user.name=kun
spring.security.user.password=123

自定义登陆页

WebSecurityConfig.class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.dome.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry expressionInterceptUrlRegistry = httpSecurity.authorizeRequests();
ExpressionUrlAuthorizationConfigurer.AuthorizedUrl authorizedUrl = (ExpressionUrlAuthorizationConfigurer.AuthorizedUrl) expressionInterceptUrlRegistry.anyRequest();
authorizedUrl.authenticated();

FormLoginConfigurer<HttpSecurity> formLoginConfigurer =
httpSecurity.formLogin();
formLoginConfigurer.loginPage("/myLogin.html");
formLoginConfigurer.permitAll();
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.example.dome.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
.permitAll()
.and()
.csrf().disable();
}
}

这这里我们主要提供了两种方法,通常我们在写项目的时候都会使用第二种方法,而第一种方法这是一个非常令人无语的事情,使用了HttpSecurity实际上是对应了Spring security命名空间的配置方式,允许我们特定的HTTPP请求配置安全策略,所以相对第二种方法而讲,我们喜欢第二种,HttpSecurity提供了很多配置的相关方法。分别对应命名空间配置中的子标签分如:

ID DA
authorizeRequest() <intercept-url>
formLogin() <form-login>
httpBasic() <http-basic>
csrf() <csrf>

通过调用这些方法后,除了使用and()方法来结束当前标签,才会返回到Httpsecurity,否则将会自动进入对应标签域。authorizeRequests()方法实际撒汗功能返回了一个url拦截注册器,可以调用它提供的anyanyRequest()、antMatchers()和regexMatchers()方法来进行匹配系统的url,并为其指定安全策略。

formLogin()方法和httpBasic()方法都将声明需要Spring security提供表单认证方式,分别返回对应的配置器,其中formLogin().loginPage()为指定自定义登陆页面,同时Spring security会注册一个路由用于接受请求,而csrf()方法则是Spring security所提供的夸站请求伪造 防护公鞥,当WebSeecurityConfig继承WebSecurityConfigurerAdapter的时候将会默认开启csrf()方法。

myLogin.html

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- static/myLogin.html -->
<html>
<head>
<meta charset="UTF-8">
<title>Loing - by Spring security</title>
</head>
<body>
<h1>The is Spring security by html</h1>
<form action="/login" method="post">
<input type="text" name="username" placeholder="user"/>&nbsp;
<input type="password" name="password" placeholder="pass"/>
&nbsp;<input type="submit" value="login go">
</form>
</body>
</html>

指定登陆处理路径

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
package com.example.dome.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
.loginProcessingUrl("/login") // 指定处理路径
.permitAll()
.and()
.csrf().disable();
}
}

登录成功与登入失败逻辑 (successHandler and failureHandler)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
package com.example.dome.configuration;

import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.ExpressionUrlAuthorizationConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure (HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/myLogin.html")
.loginProcessingUrl("/login") // 指定处理路径
// 登入成功处理
.successHandler(new AuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
PrintWriter out = httpServletResponse.getWriter();
out.write("{\"error_code\":\"0\",\"message\":\"Hello login -by spring security\"}");
}
})
// 登入失败处理
.failureHandler(new AuthenticationFailureHandler() {
@Override
public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
httpServletResponse.setContentType("application/json;charset=UTF-8");
httpServletResponse.setStatus(401);
PrintWriter out = httpServletResponse.getWriter();
// cause
out.write("{\"error_code\":\"401\",\"name\":\"" + e.getClass() + "\", \"message\":\"" + e.getMessage() + "\"}");
}
})
.permitAll()
.and()
.csrf().disable();
}
}

Spring Security OAuth

开放授权(Open Authorization)是一种开放的协议,即地桑房可以使用第三方应用代表资源所有者获取有限的访问授权机制,在整个授权的过程中,第三方站点无需涉及用户的密码即可获取部分的资源。在很多的站点场景之中,都提供了第三方登入等相关信息,如QQ、微信扫码登录等机制,而这当我们点击第三方登录时会跳转到授权的登录页面,然后登入成功就会跳转到访问的目标站点之中,而这过程中该站点就不会获取到用户的密码等相关信息,主要流程如下:

  1. 客户端要求用户提供授权许可
  2. 用户同意向客户端提供授权许可
  3. 客户端携带用户所提供的授权许可向授权服务器申请访问令牌
  4. 授权服务器验证客户端是否携带授权许可,如有效则发放访问令牌
  5. 客户端使用访问令牌申请服务器资源
  6. 资源服务器验证访问令牌,确认正确后向客户端提供资源。

四种授权模式

授权码模式(Authorization Code)

授权码模式是功能较为完整,安全性和用户信息存在应用服务器宝楼风险小、服务器与服务器之间的访问难以被抓取等相关优点。

  1. 重定向到认证服务器

  2. 认证服务器用户授权

  3. 认证服务器->应用服务器重定向应用服务器,带上授权码

  4. 应用服务器->认证服务器获取访问令牌(用授权码换得)

  5. 认证服务器->应用服务器认证服务器获取访问令牌

  6. 应用服务器->资源服务器访问资源服务器

隐式授权(Implicit Grant)

隐式授权模式是指访问令牌通过重定向的方式传递到用户浏览器之中,之后通过js获取访问令牌。通过使用隐士验证我们可以免于后台和存储,将信息存储与Token或Cookie之中,基本上通过认证后可直接进行访问。

密码授权模式(Password Credentials)

密码授权模式即通过浏览器直接携带用户的密码向授权服务器申请令牌,这种授权模式不用通过跳转到授权服务器进行,而是通过资源服务器提供的页面进行。

客户端授权模式(Client Credentials)

与上面三种模式相比之下,客户端授权模式相对于较为简单,只需要客户端请应用公钥、密钥等洗脑洗想授权服务器申请访问令牌,从而货渠道服务器所提供的资源。

OAuth 2.0 github and login

如果要接入github的授权,则需要访问https://github.com/settings/applications/new,来申请一个应用并在此填写应用名称、主页URL、应用说明、认证时重定向的页面即可。(需要注意的是在本地开发的时候,主页URL需要设置为```http://localhost:8080```,认证重定向地址设置为```http://localhost:8080/login/oauth2/code/github```)。
点击确定后将会跳转至应用创建成功页面,并获得到客户端ID(Client ID)以及客户端密码(Client secrets)

Client ID
****cd6dcb6f7289b5c56
****2a5b45fa0b528837b2181617e7

Spring Security OAuth默认的重定向模板是{baseUrl}/login/oauth2/code{registrationId}registrationId是Client Registrationa的唯一ID,通常接入的OAuth服务提供商的简称来命名。

resources

application.yml

1
2
3
4
5
6
7
8
9
10
spring:
security:
oauth2:
client:
registration:
github:
clientId: <clientid>
clientSecret: <clientSecret>
# 需要获取的信息
scope: read:user, user:email

static

login.html

1
2
3
4
5
6
7
8
9
10
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Spring boot security and OAuth2.0 github login</title>
</head>
<body>
<h1>Login github> </h1><a href="/oauth2/authorization/github">Go</a>
</body>
</html>

Controller

IndexController.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import java.security.Principal;

@RestController
class IndexController {
@Autowired
private OAuth2AuthorizedClientService auth2AuthorizedClientService;

@GetMapping(value = "/")
public String index(Principal principal) {
return "Hey," + principal.getName(); // getName 为获取其github id
}
}

Configuration

WebSecurityConfiguration.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
package com.example.demo.configuration;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login.html")
.defaultSuccessUrl("/").permitAll()
.and()
.logout()
.logoutRequestMatcher(new AntPathRequestMatcher("/logout","GET"))
.deleteCookies("true")
.logoutSuccessUrl("/login.html?logout")
.permitAll();
}
}

Spring security

Spring security 是一个功能呢个强大且高度可定制的身份验证和访问控制框架,主要用于保护Spring应用程序,并致力于为Java应用提供身份验证和授权。

Spring Security is a powerful and highly customizable authentication and access-control framework. It is the de-facto standard for securing Spring-based applications.
Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements

在Spring security成为 Spring 的子项目之前,他的前身名为“Acegi Security”,在成为spring的子项目之后更名为正式更名为“Spring security”。

认证和授权

spring security主要针对的即是认证和授权,分别通过认证和授权来起到一定的保护性作用,而这些通常不是spring security所独有的:

认证

认证是用于确认用户或个人在某个系统或场合中是否合法,这里主要应用在登入系统的用户。可以从我们日常生活中所了解,开门需要钥匙,则通过钥匙进去则为合法,这里的钥匙泛指注册,开门的动作泛指登录。spring security支
持广泛的认证技术,而这些认证技术大多由第三方组织或相关标准化组织进行开发,并由spring security进行集成,目前已经集成的认证技术如下:

ID DA FA
1 HTTP BASIC authentication haders 基于IEFT RFC 的标准认证
2 HTTP Diget authentication headers 基于IETF RFC 的标准认证
3 HTTP X.509 client certificate exchange 基于IETF RFC 的标准认证
4 LDAP Lightweight Directory Access Protocol 常见的跨平台身份验证方式
5 Form-based authentiation 用于简单的用户界面需求
6 OpenID authentication 去中心化身份验证方式
7 Authentication based on pre-established request head 一种用户身份验证及授权的集中式的安全基础方案
8 Jasig Ceantral Authentication Service 单点登入解决方案
9 Transparent authentication context propagation for Remote Method Invocation (RIM) and HttpInvoker Spring 远程调用协议
10 Automatic “remember-me” authentication 允许在指定到期前自行重新登入
11 Anonymous authentication 允许匿名用户使用特定的身份安全的访问资源
12 Run-as authentication 允许在会话中变换用户的身份机制
13 Java Authentication and Authorization Service JAAS Java 验证和授权API
14 Java EE container authentication 允许系统继续使用容器管理这种身份验证方式
15 Kerberos 使用对称密钥机制,允许客户端与服务器相互确认身份验证协议

授权

授权的过程是指用户通过认证之后,是否允许对某个功能的操作过程。我们可以假设授权是指你家里的客人开完门之后,是否可以进入你的主卧,需要征得你的同意。这里我们可分为用户和管理员两个核心的概念,你是家里的主人,即管理员,客人是用户,是否能进入某个地方需要征得你的同意。

除以上之外,Spring security还引入了很多的第三方包用于支持更多的认证技术,Spring security允许编写自己独一无二的认证技术,在绝大的情况下,既然使用 Spring boot 、 spring 开发项目,使用自家的spring security岂不是更好?且完美。除了认证以外,在授权上,Spring srcurity不仅仅支持基于URL的web请求授权,还支持方法访问授权对象授权等,覆盖了大部分的授权范围和场景。

Spring cloud Gateway


Gateway 是 Spring cloud 官方所推出的第二代网关(API Gateway)框架,在为服务系统中,网关的主要作用就是当用户调用的所有微服务,都要经过网关。

在为服务中,除于不同微服务可能会带来不同的开发语言和协议,因此需要通过网关来处理服务的调用

网关统一向外部系统提供 REST API,在 Spring cloud 中,使用 Zuul、Gateway 等作为网关可以实现出动态路由、监控、回退、安全功能等。

特点

Gateway 基于 Spring Framework 5、Project Reactor 、Spring boot 2.0 构建,可以匹配任何请求属性的路由,能编写谓词(Predicates)以及过滤器(Filters)等。

谓词主要是用于确定给定输入的 true 或 false,在网关中谓词和过滤器也可以用于特定的路由。

过滤器可以重写数据,但由于 Gateway 是异步的,如果需要对响应的 body 进行修改需要使用 writeWith() 所提供的 GlobalFilter、GatewayFilter 类型。

其中 GlobalFilter 是对所有路由有效的,而 GatewayFilter 类型仅对指定范围生效

除此之外 Gateway 还支持路径重写、动态路由和集成了 Hystrix 断路器以及 DiscoveryClient 的集成和限流。

工作流程

首先 HTTP/HTTPS 强求 Spring cloud Gateway,DispatcherHandler 接受请求,并通过 RoutePredicateHandlerMapping 进行路由匹配,如果网关路由与请求的路由进行匹配 则将请求发送到 FilteringWebHandler,如果不匹配,那么则将请求发送给 DispatcherHandler 进行处理。

最后 FilteringWebHandler 通过自定义的过滤器来发送请求,这会将请求转发到具体的服务中,最后处理结果给用户。

谓词接口和谓词工厂

谓词(Predicate),在 Java 8 中引入的一个函数式接口,主要用于接受输入参数并返回布尔值的结果,Spring cloud Gateway 通过 Predicate 接口来判断当前路由是否满足指定条件。

谓词工厂(Route Predicate Factories)主要作用就是当符合谓词条件就使用该路由进行匹配,否则就忽略。

Spring cloud Gateway 的路由规则是由 RouteDefinitionLocator 类进行管理,默认情况下使用 Spring Boot 的 @ConfigurationProperties 机制来加载属性。

实现

Java API


可以通过最简单的 Java API 的方式来构建路由,主要通过 @Bean 来实现一个自定义的 RouteLocator 类来实现 自定义路由转发规则。在此之前,需要通过添加 gateway 依赖来完成:

1
2
3
4
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

由于 Spring cloud gateway 使用的是 Netty+WebFlux 实现的,因此不需要引入 Web 模块依赖。

之后在启动类添加自定义 RouteLocator 类即可:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;

/**
* 将请求转发给 baidu.com
*
* @author kunlun
* @date 2021/7/8
*/
@SpringBootApplication
public class DemoApplication {

public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}

@Bean
public RouteLocator routeLocator (RouteLocatorBuilder builder) {
return builder.routes().
route("guonei", r -> r.path("/guonei/**")
.uri("http://news.baidu.com/")).build();
}

}

在上述的过程中,route 主要将与 guonei 路由想匹配的通过 Spring cloud Gateway 转发到了 http://news.baidu.com/guonei 中,因此访问 http://localhost:8081/guonei 就是访问 http://news.baidu.com/guonei

其中 r.path 参数下的 /guonei/** 指的是这将会匹配包含所有任何后缀携带 /guonei 的路径模式,还需要注意的是 Spring cloud Gateway 支持两种的配置,上面所实现的是基于RouteLocator 的实现,还有 properties 配置文件的实现

application.yml / application.proerties

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: guonei
uri: http://news.baidu.com/
predicates:
- Path=/guonei
application:
name: GatewayForward

server:
port: 8081

与 Java API 一样,访问 http://localhost:8081/guonei 就是访问 http://news.baidu.com/guonei,同样的实现了路由的功能。

路由谓词工厂或路由规则(RoutePredicateFactory)

在 Gateway 中,主要通过 RoutePredicateFactory(谓词工厂)来创建谓词(Predicate),因此也提供了很多种谓词工厂,这也是在上面所提到的:“谓词工厂(Route Predicate Factories)主要作用就是当符合谓词条件就使用该路由进行匹配,否则就忽略。”

通过谓词工厂,使得开发人员可以简单配置即可获得很多需要和想要的路由规则,及谓词(Predicate),这些路由规则会根据 HTTP 请求进行不同属性来匹配,其中最为主要的为下述几类:

RoutePredicateFactory type info
AfterRoute……(省略 PredicateFactory) datetime 请求时间满足在配置时间之后
BeforeRoute datetime 请求时间满足在配置时间之前
BetweenRoute datetime 请求时间满足在配置时间之前
CookieRoute Cookie 请求指定了 Cookie 正则匹配的指定值
HeaderRoute Header(数据头) 请求指定了请求上下文所匹配的指定值
CloudFoundryRouteServiceRoute Header 请求 Headers 是否包含了指定的名称
MethodRoute Method(方法) 请求的方法是否匹配配置的方法
PathRoute Path(路径) 请求路径是否匹配指定值
Query Queryparam(查询参数) 请求查询的查询参数与配置文件的相匹配
RemoteAddreRoute Remoteaddr(远程地址) 远程地址是否匹配指定值
HostRoute Host(主机) 请求主机是否匹配指定值

After

AfterRoutePredicateFactory(After路由谓词工厂)是一个 datetime(日期时间) 类型的为此,他主要表达的是如果请求在配置文件中 时间在配置文件之后 发生的请求允许。

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- After=2022-01-20T18:06:06+08:00[Asia/Shanghai]
application:
name: GatewayForward

server:
port: 8081

After 的核心意思就是只能在配置规定后的时间进行访问,但只能通过 UTC + 时区的方式来添加规则,否则将直接报错。After 的主要作用就是在 2021 年 1 月 20 日之后才可进行访问,否则将会直接报错,直接访问 http://localhost:8081/ 即可。

Before

Before 也是属于 datetime 类型之一,其核心含义是当请求满足配置时间之前,而之前的 After 路由谓词工厂只是满足配置时间之后:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Before=2022-01-20T18:06:06+08:00[Asia/Shanghai]
application:
name: GatewayForward

server:
port: 8081

Between

Between 可以理解为是两个 UTC 时间之间的请求,因此他有两个参数,分别是 datetime1 以及 datetime2 这是一个 ZonedDateTime 对线,因此请求需要满足 datetione1 之后且 datetime2 之前的请求:

1
2
3
4
5
6
7
8
9
10
11
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Between=2019-01-20T18:06:06+08:00[Asia/Shanghai],2022-01-20T18:06:06+08:00[Asia/Shanghai]
application:
name: GatewayForward

如上面的 Between 路由规则是,在 2019-01-20T18:06:06+08:00[Asia/Shanghai] 之后,2022-01-20T18:06:06+08:00[Asia/Shanghai] 之前的请求将会进行匹配,否则将会忽视。

CookieRoutePredicateFactory 主要用于匹配指定值,虽然很多作者说直接可以一条参数走到低:mycookie,mycookievalue,但很遗憾并没有作用,因此只能通过全展开的方式来实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- name: Cookie
args:
name: meteor_login_token
regexp: ipQdtc89kEO7-JaBWLdgwPHzX9oDy6l-2qXmZ_OeHye
application:
name: GatewayForward

server:
port: 8081

在上述的 cookie 中,主要指定了 cookie 名字 meteor_login_token,以及 cookie_value ipQdtc89kEO7-JaBWLdgwPHzX9oDy6l-2qXmZ_OeHye,因此如果请求的 cookie 与配置文件相匹配则通过,否则将会忽略。

Header 即请求上下文的头部信息,当请求与配置的 Header 一致时进行转发,在官方文档中被称之为:X-Request-Id 与正则表达式合在一起进行匹配,但并没有什么作用。因此我们可以理解为 Header 请求头是根据上下文头部信息来指定服务器的域名和服务器正在侦听的 TCP 端口号:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Header=Host,localhost:8081

application:
name: GatewayForward

server:
port: 8081

当请求的上下文头部信息与配置文件相互匹配则允许请求,否则将会直接进行忽略。

Host

Host 路由谓词工厂与 Header 类似,或者说两者都可以实现其效果,但 Host 路由谓词工厂很明显比 Hedaer 更加的简单且好理解。因此他主要的作用就是匹配所请求的主机是否和配置文件相互一致,否则将不具备转发功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Host=localhost:8081

application:
name: GatewayForward

server:
port: 8081

在上述配置文件中,请求的主机是 localhost:8081 因此与配置文件的信息相互匹配,可以通过网关使用路由进行跳转。

Method


Method 路由谓词工厂相比上述几个 header 类型的谓词工厂显得特别实用,他主要用指定访问资源以响应预检请求时允许的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Method=GET,POST

application:
name: GatewayForward

server:
port: 8081

就比如上述的配置文件,我们允许了 GET,POST 方法请求,但不允许 PUT 方法请求,因此使用该方法请求将会直接被网关所进行拦截。

Query


Query 路由谓词工厂主要是当 请求查询的查询参数与配置文件的相匹配,就比如 localhost:8081?name=111 正好与配置文件中的 query=namename 相互匹配,则允许请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: after
uri: http://news.baidu.com/
predicates:
- Query=name

application:
name: GatewayForward

server:
port: 8081

RemoteAddre

RemoteAddre 与 Header 路由谓词工厂类似,简单理解就是他们的进阶版,他可以根据 CIDR 表示法进行匹配,也就是说当你写入规则是 192.168.0.1/24 时,你 192.168.0.104/24 也可以请求此接口,但如果你不是这个网段下的,则会被拒绝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
spring:
cloud:
gateway:
routes:
- id: remoteaddr_route
uri: http://news.baidu.com
predicates:
- RemoteAddr=192.168.0.1/24

application:
name: GatewayForward

server:
port: 8081

localhost 是 127.0.0.1 的地址映射,因此在 RemoteAddr 路由谓词工厂规则中他会被直接拒绝。

Weight


Weight 是权重路由谓词工厂,当 Gateway 中有多个路由网关配置时可以发挥作用,他主要分为 组(group) 以及 权重(weight) 进行组合:

1
2
3
4
5
6
7
8
9
10
11
12
spring:
cloud:
gateway:
routes:
- id: weight_one
uri: http://news.baidu.com
predicates:
- Weight=group1, 1
- id: weight_two
uri: http://baidu.com
predicates:
- Weight=group1, 8

就比如上述的配置中,weight_two 的权重比 weight_one 高,因此网关匹配的是 weight_two 的路由。

Gateway consul 服务转发


在正常的微服务架构中,都会选择依赖服务中心来进行注册,这通常需要对每个服务提供者进行单独的配置,Gateway 的出现爱你提供了默认转发的工作,当网关注册到服务中心后,网关则会代替服务中心来提供转发的服务

1
2
3
4
5
6
7
8
9
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

application.yml

application.yml 配置文件中,需要开启服务转发的选项,即 spring.cloud.gateway.discovery.locator.enabled 即可,之后在启动类中添加 @EnableDiscoveryClient 注解以此来实现服务发现:

1
2
3
4
5
6
7
8
9
10
11
12
13
spring:
application:
name: Gateway Consul
cloud:
consul:
host: localhost
port: 8500
gateway:
discovery:
locator:
enabled: true
server:
port: 8515

过滤器(Filter)

过滤器的存在

过滤器之所以在网关中存在,是因为在微服务系统中,服务提供者非常的多,因此如果在每个服务中都有诸如鉴权、限流以及日志输出等则会占用服务的消耗(也就是说每个服务我都要写一遍这些功能,则会让开发很烦躁)因此可以直接通过网关来进行处理。

网关应可以处理鉴权、限流等工作

PRE and POST

PRE

根据请求的生命周期,在 Gateway 中,过滤器会分为 PRE、POST 两种,其中 PRE 代表 在路由执行之前的请求 执行该过滤,因此主要应用在参数校验、鉴权、流量监控、日志输出、协议转换功能。

POST

POST 主要是在 请求被路由跳到微服务之后所执行 的过滤器,因此这种过滤器可以实现出相应头(HTTP Header)以及收集统计信息和指标、响应转发、日志输出、流量监控等功能。

GatewayFilter and GlobalFilter

GatewayFilter (网关过滤器)

过滤器分为两类,其中网关过滤器(GatewayFilter)只会在 单个路由或者分组路由中。网关过滤器是一种 可以修改请求传入的 HTTP 请求或输出 HTTP 相应 的特定路由使用,Gateway 内置了 20 多种网关过滤器工厂来编写网关过滤器:

Id Name type Info
1 AddRequestHeader Request 用于在请求头中添加自定义的键值对
2 AddRequestParameter Request 用于在请求参数中添加请求参数的键值对
3 AddResponseHeader Response 用于在相应头中添加
4 Hystrix 断路器 用于将断路器引入网关路由中,可以让服务免受级联故障影响,并在故障时提供回退响应(使用时需要使用名为 HystrixCommand 的参数)
5 PrefixPath Path 用于使用简单的 Prefix
6 PreserveHostHeader Header 设置路由过滤器的请求属性,检查是否发送原主机头或由 HTTP 客户端确定主机头
7 RequestRateLimiter Request 用于确定当前请求是否继续(如果不允许则会返回 HTTP 429 - Too Many Request
8 RedirectTo Request 用于接受请求的状态和 URL 参数(该状态是一个重定向的 300 系列 HTTP 代码,类似与 301。但 URL 是 Location 头部的值)
9 RemoveNonProxyHeaders Headers 从转发的请求中删除请求头
10 RemoveRequestHeader Request 通过请求头名删除请求头
11 RemoveResponseHeader Response 通过响应头名删除响应头
12 RewritePath Path 通过 Java 正则表达式重写请求路径
13 SaveSession Session 在转发下游调用之前,强制执行保存 Session 的操作
14 SecureHeaders Headers 为响应头添加安全头
15 SetPath Path 该方法允许通过路径的模板段来操作请求路径,使用 Spring 的 URI 模板,支持多种匹配
16 SetResponseHeader Response 需要通过 Key-Value 对来设置响应头
17 SetStatus Response 用于设置请求响应状态,但需要 Status 参数(需要有效的 Spring HttpStatus,例如整形的 404 或枚举类型字符串 NOT_FOUND
18 StripPrefix Request 用于剥离前缀(需要 parts 参数,用于表明请求被发送到下游之前从请求路径中所剥离的元素数量)
19 Retry Request 主要用于重试,但需要 Retries、Statuses、Methods、Series 等参数
Retries:重试的次数
Statuses:重试的 HTTP 状态码(通过 org.springframework.http.HttpStatus 表示)
Methods:重试的 HTTP 状态码(通过 org.springframework.http.HttpMethod 表示)
Series:重试的状态码(通过 org.springframework.http.HttpStatus.Series 表示)
20 RequestSize Request 用于限制请求大小,当请求超过限制时启用,限制请求达到下游服务,该过滤器将 RequestSize 作为参数
AddRequestHeader(在请求头中添加自定义的键值对)

AddRequestHeader 即主要在请求头中来添加自定义的键值对,其主要将应用过滤器 AddRequestHeader=X-Request-Name,X-Request-Value 应用并添加至请求头中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: Gateway
cloud:
gateway:
routes:
- id: /guonei
uri: http://news.baidu.com
predicates:
- Method=GET
filters:
- AddRequestHeader=X-Request-Name,X-Request-Value

server:
port: 8210

logging:
level:
ROOT: DEBUG
会将 ```X-Request-Name
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

###### AddRequestParameter (添加请求参数)
![](https://49812933408852955071488026628034-1301075051.cos.ap-nanjing.myqcloud.com/20210713131345.png)

AddRequestParameter 主要的作用就是在请求头(Header)中添加请求参数的键值对,即将 ```RouteDefinition /guonei``` 过滤器 ```_genkey_0=KEY_NAME, _genkey_1=KEY_VALUE``` 应用在 ```AddRequestParameter``` (添加请求参数)中

```yml
spring:
application:
name: Gateway
cloud:
gateway:
routes:
- id: /guonei
uri: http://news.baidu.com
predicates:
- Method=GET
filters:
- AddRequestParameter=KEY_NAME,KEY_VALUE

server:
port: 8210

logging:
level:
ROOT: DEBUG

这回将 KEY_NAME=KEY_VALUE 添加到所有匹配请求的下游请求所查询的字符串中,AddRequestParameter 即添加请求参数会用于匹配路径或主机的 URI 变量。

AddResponseHeader (添加响应头)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
spring:
application:
name: Gateway
cloud:
gateway:
routes:
- id: /guonei
uri: http://news.baidu.com
predicates:
- Method=GET
filters:
- AddResponseHeader=X-Response-Name,X-Response-Value

server:
port: 8210

logging:
level:
ROOT: DEBUG

此刻 AddResponseHeader 会将 X-Response-Name=X-Response-Value 所添加至响应头中。

Hystrix


路由容错控制器的主要的运用场景就是在遇到超时、服务器端错误来发挥作用,通过 Gateway 来集成 Hystrix(含 resilience4j)通过过滤器来加入 fallbackUri 得以实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

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

/**
* 容错控制器
*
* @author kunlun
* @date 2021/7/16
*/
@RestController
public class NotFoundController {


@RequestMapping(value = "/fallback")
public Mono<Map<String, String>> notFound() {
Map<String, String> stringMap = new HashMap<>();
stringMap.put("code", "503");
stringMap.put("data", "Current service is unavailable");
stringMap.put("gateway", "GatewayConfigurationFaultToleranceRouting");
return Mono.just(stringMap);
}
}

.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
spring:
application:
name: GatewayConfigurationFaultToleranceRouting
cloud:
gateway:
routes:
- id: hey
uri: lb://service-provider/hey
predicates:
- Path=/hey
- Method=GET
filters:
- AddRequestParameter=name,key
# 服务熔断后使用的地址 @RequestMapping(value = "/fallback")
- name: CircuitBreaker
args:
name: fallback
fallbackUri: forward:/fallback
discovery:
locator:
# 允许服务发现
enabled: true
consul:
host: localhost
port: 8500
discovery:
service-name: service-provider

server:
port: 8210

需要注意的是由于我们这个是熔断,需要服务提供者的存在,因此你可以将服务提供者快速关闭来达到他短时间无法提供服务,之后网关才可认定这是由于服务出错导致的,如果直接关闭服务提供者那返回的只是 404

最后启动 consul 集群以及服务提供者,通过 GET 方式请求 http://localhost/hey 即可完成路由容错控制器的实现,当然你也可以直接通过 lb://service-provider 来进行配置你的服务路由。

GlobalFilter (全局过滤器)

另一种则为全局过滤器(GlobalFilter)这种过滤器与网关过滤器的区别是会 应用到所有路由中,在 Gateway 内置中,有九种全局过滤器:

Id Name Info
1 Forward Routing Filter 在 exchange 的 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 属性值中查找 URI(如果 URI 是 forward 协议,则将它用于 DispatcherHandler (调度程序处理程序) 处理请求)
2 LoadBalancerClient Filter 在 exchange 的 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 值中查找 URI,如果 URI 是 lb 协议,则会使用 Spring cloud LoadBalancerClient 将名称(lb://myservice 中的 myservice)解析为实际的主机和端口,并替换 URI 中相同属性。(除此过滤器还会查看 ServerWebExchangeU体力是。GATEWAY_SCHEME_PREFIX_ATTR 属性来判断他是否等于 lb
3 Netty Routing Filter 假设 ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR 中的 URI 使用的是 HTTP 或 HTTPS 协议,则运行 Netty Routing Filter。他用 Netty HttpClient 所发出的下游代理请求。相应应该放在 ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 的 exchange 中以便在以后的过滤器中使用
4 Netty Write Response Filter ServerWebExchangeUtils.CLIENT_RESPONSE_ATTR 值中存在 NettyHttpClientResponse 则会运行 Netty Write Response Filter。(他在所有其他过滤器完成后运行,并将代理相应写回到网关客户端的响应数据中)
5 RouteToRequestUriFilter 如果 ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR 值中存在 Route 对象,则会将路由到请求地址,他会根据请求 URI 创建一个新的 URI。新的 URI 则会位于 ServerWebExchangeUtils.GATEWAY_REQUEST_URI_ATTR 值中(假设 URI 具有协议前缀,如 lb:ws://serviceid 则会将从 URI 中所剥离 lb 协议,并防止在 ServerWebExchangeUtils.GATEWAY_SCHEMEPREFIX_ATTR 中以便稍后在过滤器中所使用)
6 Websocket Routing Filter 假设 ServerWebExhcangeUtils.GATEWAY_REQUEST_URL_ATTR 值中的 URI 是 ws 或 wss 协议,则运行 Websocket Routing Filter (利用 Spring Web Socket 底层代码将 WebSocket 请求转发到下游)
也可以通过 URI 前面添加 lb 来对 Websocket 来进行负载均衡,如 lb:ws://serviceid(需要注意的是如果在普通 HTTP 上使用 SockJS 来作为回退,则会匹配正常的 HTTP 路由以及 Websocket 路由)
7 Gateway Metrics Filter 使用需要添加 spring-boot-starter-actuator 依赖,默认会通过 spring.cloud.gateway.mertics.enable 设置为 false Gateway Metrics Filter 才会运行。该过滤器会添加一个名为 gateway.request 指标(Metrics),并包含四个属性:
routeId:路由器 ID
routeUri:API将会被路由转到 URI
outcome:由 HttpStatus.Series 所分类的结果
status:HTTP 请求返回给客户端的状态
8 Combined Global Filter and GatewayFilter Ordering(组合是全局过滤器和网管过滤器排序) 当请求进入并匹配到一个路由时, Filtering Web Handler 会将 GlobalFilter 的所有实例和 GatewayFilter 的所有路由特定实例添加到过滤器链中。(这个组合的过滤器链由 org.springframeworking.core.Ordered 接口的排序,并通过 gegOrder() 方法或注解 @Order 来设置)同时网关也会对过滤器逻辑执行的 “PRE” 和 ”POST“ 阶段来进行区分
9 Marking An Exchange As Routed (路由交换) 网关在路由 WEB 服务交换后(ServerWebExchange),会将 Gateway Already Routed 添加到 exchange 属性来将该交换标识为 ”路由”(一旦被请求标识为路由,则其他的路由过滤器会跳过该请求,也可以通过便携方法将交换标识为路由,或检查交换是否已经成为路由)

更多可以参考 https://cloud.spring.io/spring-cloud-gateway/reference/html/#the-addrequestheader-gatewayfilter-factory - Spring cloud gateway 官方文档

限流

限流的主要目的是防止过渡的消耗资源,从而来保障服务对所有用户的可用性,常见的限流措施如诸以限制并发数为目的的数据库连接池/线程池,以及瞬时并发数的限制,和限制每秒的平均速率都是最为常见的。而这些基本上都会在网关层上进行实现,如使用 nginx、zuul、gateway、openresty、kong 等方式进行限流。

以维基百科的话来说就是在计算机网络中,控制网络接口请求速率

如果没有进行限流的话,则有几率会被恶意提交,导致后续服务无法得到提供,从而达到拒绝服务攻击,而限流在高并发中,是一个必不可少的一个因素之一,通过限流发挥的作用从而保护用户的可用性,甚至防止一些恶意请求等问题的发生。

限流算法

计数器算法(固定窗口)

计数器算法是一个简单以及暴力的,他从第一个请求开始计数,在假设我们设置一个 QPS(每秒查询数为 100),一秒内请求超过了设定值,那么接下来的请求都会被拒绝,等过一秒后才可继续查询,这时会将计数恢复成0。

看似这个算法很好且可以解决一些简单的问题,但这个算法也存在的一些问题,如我在 100ms 内通过了 100 个请求,则后续的 900ms 内的请求都会被拒绝,因此这种现象也被称之为 “突刺现象”。虽然我们可以通过将单位时间设置的更短,但依旧无法解决核心的问题,这时就诞生了漏桶算法和令牌桶算法。

计数器算法(滑动窗口)

滑动窗口又是计数器算法的一个改进,主要解决了固定窗口上的 临界值 的问题,将一个计时器分为了诺干个小的计时器,这些计时器都是独立的,当请求时间大于第一个计时器最大时间时,那么将会向前平移一个计时器(同时将前一个计时器丢弃,然后将当前的计时器设置为第一个计时器)。

如上图中,窗口大小为 1s,那么每过 0.2ms就会删除前一个格子,从而向右移一个格子,滑动窗口最为主要的就是他的滑动,使得可以控制的更加细密,以此来解决临界值的问题,但滑动窗口和固定窗口都无法解决突刺现象这一问题。

漏桶算法

漏桶算法(Leaky Bucket)内主要有两个变量,分别为 桶的大小以及漏洞(孔的大小)的大小,可以将请求比喻水,当水倒入漏桶中,需要有一定的处理速度(孔的大小决定处理的速度),而桶的大小决定了可以容纳请求的大小,如果请求太大,则会导致漏桶装不下请求从而拒绝后续的请求。

令牌桶算法

令牌桶算法(Token Bucket)这种算法和我们在医院排队叫号比较相似,当请求到达的时候,会去令牌桶(由令牌工厂生产令牌)中取得一个令牌,之后等待响应。

这也达到了一定限流的目的,首先我们可以决定令牌工厂放入令牌的速率,也可以定时或者根据一些特定的规则来达到目的,从而达到增加/减少令牌的数量达到限流的效果。

限流的实现

Spring cloud Gateway 内置了限流工厂 RequestRateLimiterGatewayFilterFactory 底层使用 redis lua 脚本进行实现,因此需要添加 Consul、Gateway、Redis 的依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis-reactive -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>

之后编写我们的限流配置类,主要通过名称、地址、以及 API 进行限流三种,也可以根据不同的状况自定义配置(还有一个跟均 CPU 进行限流的?)

LimitConfig.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
package com.example.demo.config;

import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;

/**
* 限流配置文件,根据名称、地址、API
*
* @author kunlun
* @date 2021/7/23
*/
@Component
public class LimitConfig {

@Bean
KeyResolver userKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getQueryParams().
getFirst("user"));
}
@Primary
@Bean
KeyResolver ipKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getRemoteAddress().
getHostName());
}

@Bean
KeyResolver apiKeyResolver() {
return exchange ->
Mono.just(exchange.getRequest().getPath().
value());
}
}
application.yml

最后通过配置文件 application.yml 来链接 consul、redis 以及配置路由规则和全局过滤等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
spring:
application:
name: GatewayLimit
redis:
host: localhost
port: 6379
database: 1
password: toor
cloud:
gateway:
routes:
- id: hey
uri: lb://service-provider/hey
predicates:
- Path=/hey
- Method=GET
filters:
- name: RequestRateLimiter # 用于确定当前请求是否继续(如果不允许则会返回 429
args:
key-resolver: '#{@ipKeyResolver}'
redis-rate-limiter.replenishRate: 1 # 令牌桶每秒填充的平均速率
redis-rate-limiter.burstCapacity: 3 # 令牌桶总流量
discovery:
locator:
# 允许服务发现
enabled: true
consul:
host: localhost
port: 8500
discovery:
service-name: service-provider

server:
port: 8210

保证 redis、consul 等已经启动且可提供服务

端点

端点在 Spring cloud Gateway 中主要提供的作用就是方便开发或者运维人员进行调试,以此来获取过滤器列表、路由列表、路由信息、刷新路由信息以及添加\删除路由等操作,这依赖于 Spring boot Actuator ,需要注意的是这些端点都会在 /actuator/gateway 路径下,分别提供对应的服务:

Id Name Method Info Uri
1 globalfilters GET 展示所有全局过滤器 /actuator/gateway/globalfilters
2 routefilters GET 展示所有过滤器工厂 /actuator/gateway/routefilters
3 refresh POST 清空路由缓存 /actuator/gateway/refresh
4 routes GET 获取路由列表 /actuator/gateway/routes
5 routes{name} GET 获取指定的路由信息 /actuator/gateway/routes/{name}
6 routes{name} POST 添加一个路由 /actuator/gateway/routes/{name}
7 routes{name} DELETE 移出一个路由 /actuator/gateway/routes/{name}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<dependencies>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-data-redis-reactive -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
<version>2.5.2</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-consul-discovery</artifactId>
</dependency>
<!-- https://mvnrepository.com/artifact/org.springframework.boot/spring-boot-starter-actuator -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
<version>2.5.2</version>
</dependency>

.

1
2
3
4
5
6
……
management:
endpoints:
web:
exposure:
include: '*'

开启后,可以访问 http://localhost:8210/actuator/gateway/routes/hey 来进行测试是否可以该路由的信息,如果没有返回则可能是 Actuator 配置上有一点点的问题

management 中,默认只开启了两个端点,分别为 health、info 两个 因此我们可以通过上述配置开启全部的断点访问,更详细的可以查阅 Actuator API: https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html

📖 more posts 📖