Coding & Life

求知若饥,虚心若愚

0%

ResponseEntity

ResponseEntity对象是Spring对请求响应的封装。它集成了HttpEntity对象,包含了http的响应码(httpstatus)、响应头(header)、响应体(body)三个部分。一个获取用户信息的Spring MVC接口通常我们直接返回实体即可(配合@RestController)

1
2
3
4
5
6
@GetMapping("/user")
public User userinfo() {
User user = new User();
user.setUsername("felord.cn");
return user;
}

等同于使用ResponseEntity作为控制器接口的返回值:

1
2
3
4
5
6
@GetMapping("/user")
public ResponseEntity<User> userinfo() {
User user = new User();
user.setUsername("felord.cn");
return ResponseEntity.ok(user);
}

但是使用ResponseEntity时我们可以做更多事情

自定义响应码

上面的ResponseEntity.ok已经包含了返回200Http响应码,我们还可以通过ResponseEntity.status(HttpStatus|int)来自定义返回的响应码

自定义响应体

放置响应的响应体,通常就是我们接口的数据,这里是一个例子:

1
ResponseEntity.status(HttpStatus.ok).body(Object)

响应头

通常我们制定Spring MVC接口的响应头是通过@RequestMapping和其Restful系列注解中的header()consumesproduces()这几个属性设置。如果你使用了ResponseEntity,可以通过链式调用来设置:

1
2
3
4
5
6
ResponseEntity.status(HttpStatus.ok)
.allow(HttpMethod.GET)
.contentType(MediaType.APPLICATION_JSON)
.contentLength(1048576)
.header("My-Header","felord.cn")
.build();

所有的标准请求头都有对应的设置方法,你也可以通过header(String headerName, String... headerValues)设置自定义请求头

大致原理

我们来看一个用来处理Spring MVC控制器接口返回值的抽象接口HandlerMethodReturnValueHandler

1
2
3
4
5
6
7
8
9
10
11
public interface HandlerMethodReturnValueHandler {
/**
* 支持的返回值类型
*/
boolean supportsReturnType(MethodParameter returnType);

/**
* 将数据绑定到视图,并设置处理标志以指示已直接处理响应,后续的其它方法就不处理了,优先级非常高
*/
void handleReturnValue(@Nullable Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception;
}

它的一个重要实现HttpEntityMethodProcessor就是处理返回类型为HttpEntity的控制器方法的处理器。它会把ResponseEntity携带的三种信息交给ServletServerHttpResponse对象渲染视图,并设置处理标志以指示已直接处理响应,后续的其他方法不处理了,优先级非常高

实战运用

通常让你写个下载文件接口都是拿到HttpServletResponse对象,然后配置好Content-Type往里面写流。如果用ResponseEntity会更加简单优雅

1
2
3
4
5
6
7
8
@GetMapping("/download")
public ResponseEntity<Resource> load() {
ClassPathResource classPathResource = new ClassPathResource("application.yml");
String filename = classPathResource.getFilename();
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentDisposition(ContentDisposition.inline().filename(filename, StandardCharsets.UTF_8).build());
return ResponseEntity.ok().headers(httpHeaders).body(classPathResource);
}

上面是一个把Spring Boot配置文件application.yml下载下来的例子。主要分为三步:

  • 将要下载的文件封装成org.springframework.core.io.Resource对象,它有很多实现。这里用了ClassPathResource,其他InputStreamResourcePathResource都是常用的实现。

  • 然后配置下载请求头Content-Disposition。针对下载它有两种模式:inline表示在浏览器直接展示文件内容;attachment表示下载为文件。另外下载后的文件名也在这里指定,请不要忘记文件扩展名,例如这里application.yml。如果不指定Content-Disposition,你需要根据文件扩展名设置对应的Content-Type,会麻烦一些。

  • 最后是组装ResponseEntity<Resource>返回。

转载自@felord.cn

时间过得真是快,现在已经是2022年了。作为开发来说,时间处理是非常繁琐的。从Java 8开始有了新的时间API、时间的处理更加优雅,不再需要借助三方类库,而且线程安全。今天来梳理一下新API的格式化

新API的时间格式化

新的时间API的时间格式化由java.time.format.DateTimeFormatter负责

本地化时间

结合枚举FormatStyle定义的风格,DateTimeFormatter预定义了基于本地(Local)风格的时间格式。我们来看这段代码:

1
String format = DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM).format(ZonedDateTime.now());

如果你在中国,格式化结果:

1
202216日 下午4:22:01

如果你在美国:

1
Jan 6, 2022, 4:21:10 PM

有三个静态方法及其重载来格式化本地化时间,具体已经整了成了思维导图:

格式化时间图

ISO/RFC规范格式

DateTimeFormatter还内置了ISORFC的时间格式,基于内置的DateTimeFormatter静态实现。举个例子:

1
2
3
4
5
// 静态实例
DateTimeFormatter isoWeekDateFormatter = DateTimeFormatter.ISO_WEEK_DATE;
// 执行格式化
String format = isoWeekDateFormatter.format(LocalDateTime.now());
// format = 2022-W01-4

其他的如下表格所示:

范式格式化

这种方式应该是我们最常用的方式了。通过字母和符号构建一个范式(Patterns),使用ofPattern(String)或者ofPattern(String, Locale)方式传递构建的范式。例如,d MMM uuuu将把2011-12-03格式化为2011年12月3日。从一个模式中创建的格式可以根据需要多次使用,它是不可改变的,并且是线程安全的。

相信什么yyyy-MM-dd HH:mm:ss你都玩腻了,下面给你看点没有见过的:

1
2
3
4
// 最后面是两个V 不是W 单个V会报错 
String pattern = "G uuuu'年'MMMd'日' ZZZZZ VV";
String format= DateTimeFormatter.ofPattern(pattern).format(ZonedDateTime.now());
// format = 公元 2022年1月7日 +08:00 Asia/Shanghai

表格给你整理好了,你试一试:

转载自@felord.cn

事务四个特性:原子性 一致性 隔离性 持久性,下面详细介绍事务的隔离性

定义

与原子性、持久性侧重于研究事务本身不同,隔离性研究的是不同事务之间的相互影响。隔离性是指,事务内部的操作与其他事务是隔离的,并发执行的各个事务之间不能互相干扰。严格的隔离性,对应了事务隔离级别中的 Serializable(可串行化) 但实际应用中出于性能方面的考虑很少会使用可串行化。

锁机制

首先来看两个事务的写操作之间的相互影响。隔离性要求同一时刻只能有一个事务对数据进行写操作,InnoDB通过锁机制来保证这一点。

锁机制的基本原理可以概括为:事务在修改数据之前,需要先获得相应的锁;获得锁之后,事务便可以修改数据;该事务操作期间,这部分数据是锁定的,其他事务如果需要修改数据,需要等待当前事务提交或回滚后释放锁。

行锁与表锁

按照粒度,锁可以分为表锁、行锁以及其他位于两者之间的锁。表锁在操作数据时会锁定整张表,并发性能较差;行锁则只锁定需要操作的数据,并发性能好。但是由于加锁本身需要消耗资源(获得锁、检查锁、释放锁等都需要消耗资源),因此在锁定数据较多情况下使用表锁可以节省大量资源。MySQL中不同的存储引擎支持的锁是不一样的,例如MyIsam只支持表锁,而InnoDB同时支持表锁和行锁,且出于性能考虑,绝大多数情况下使用的都是行锁。

如何查看锁信息

有多种方法可以查看InnoDB中锁的情况,例如:

1
2
select * from information_schema.innodb_locks; #锁的概况
show engine innodb status; #InnoDB整体状态,其中包括锁的情况

下面来看一个例子:

1
2
3
4
5
6
#在事务A中执行:
start transaction;
update account SET balance = 1000 where id = 1;
#在事务B中执行:
start transaction;
update account SET balance = 2000 where id = 1;

此时查看锁的情况:

show engine innodb status查看锁相关的部分:

通过上述命令可以查看事务24052和24053占用锁的情况;其中lock_type为RECORD,代表锁为行锁(记录锁);lock_mode为X,代表排它锁(写锁)。

脏读、不可重复读和幻读

首先来看并发情况下,读操作可能存在的三类问题:

  1. 脏读:当前事务(A)中可以读到其他事务(B)未提交的数据,这种现象就是脏读。举例如下(以账户余额表为例):

  1. 不可重复读:在事务A中先后两次读取同一数据,两次读取的结果不一样,这种现象成为不可重复读。脏读与不可重复读的区别在于:前者读到的是其他事务未提交的数据,后者读到的是其他事务已提交的数据。举例如下:

  1. 幻读:在事务A中按照某个条件先后两次查询数据库,两次查询结果的条数不同,这种现象称为幻读。不可重复读与幻读的区别可以通俗的理解为:前者数据变了,后者是数据的行数变了。举例如下:

事务隔离级别

SQL标准中定义了四种隔离级别,并规定了每种隔离级别下上述几个问题是否存在。一般来说,隔离级别越低,系统开销越低,可支持的并发越高,但隔离性也越差。隔离级别与读问题的关系如下:

在实际应用中 读未提交 在并发时会导致很多问题,而性能相对于其他隔离级别提高缺很有限,因此使用较少 可串行化 强制事务串行,并发效率很低,只有当数据一致性要求极高且可以接受没有并发时使用,因此使用也较少。因此在大多数数据库系统中,默认的隔离级别是 读已提交(如Oracle)可重复读(后文简称RR)

可以通过如下两个命令分别查看全局隔离级别和本次会话的隔离级别:

InnoDB默认的隔离级别是RR,后文会重点介绍RR。需要注意的是,在SQL标准中,RR是无法避免幻读问题的,但是InnoDB实现的RR避免了幻读问题。

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
public class Main { // 1.第一步,准备加载类

public static void main(String[] args) {
new Main(); // 4.第四步,执行main方法,new一个类,但在new之前要处理匿名代码块
}

static int num = 4; // 2.第二步,静态变量和静态代码块的加载顺序由编写先后决定

{
num += 3;
System.out.println("b"); // 5.第五步,按照顺序加载匿名代码块,代码块中有打印
}

int a = 5; // 6.第六步,按照顺序加载变量

{ // 成员变量第三个
System.out.println("c"); // 7.第七步,按照顺序打印c
}

Main() { // 类的构造函数,第四个加载
System.out.println("d"); // 8.第八步,最后加载构造函数,完成对象的建立
}

static { // 3.第三步,静态块,然后执行静态代码块,因为有输出,故打印a
System.out.println("a");
}

static void run() // 静态方法,调用的时候才加载 注意看,e没有加载
{
System.out.println("e");
}
}

执行以上代码可以总结加载顺序:静态代码块(静态变量) -> 匿名代码块(成员变量) -> 构造方法 -> 静态方法(不调用不加载

  • 静态代码块只加载一次
  • 静态方法只有调用才会加载
  • 静态代码块和静态变量按照代码编写前后顺序执行
  • 匿名代码块和成员变量按照代码编写前后顺序执行
1
2
3
4
5
public class Print {
public Print(String s) {
System.out.println(s + " ");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Parent {
public static Print obj1 = new Print("1");

public Print obj2 = new Print("2");

public static Print obj3 = new Print("3");

static {
new Print("4");
}

public static Print obj4 = new Print("5");

public Print obj5 = new Print("6");

public Parent() {
new Print("7");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Child extends Parent {
static {
new Print("a");
}

public static Print obj1 = new Print("b");

public Print obj2 = new Print("c");

public Child() {
new Print("d");
}

public static Print obj3 = new Print("e");

public Print obj4 = new Print("f");

public static void main(String[] args) {
Parent obj1 = new Child();

Parent obj2 = new Child();
}
}

执行main方法打印顺序:1 3 4 5 a b e 2 6 7 c f d 2 6 7 c f d

  • 先执行父类的静态代码块和静态变量初始化,并且静态代码块和静态变量的执行顺序只跟代码中出现的顺序有关;
  • 执行子类的静态代码块和静态变量初始化;
  • 执行父类的实例变量初始化;
  • 执行父类的构造函数;
  • 执行子类的实例变量初始化;
  • 执行子类的构造函数;

如果类已经被加载,则静态代码块和静态变量就不用重复执行,再创建类对象时,只执行与实例相关变量初始化和构造方法

1. 前言

我们上一篇介绍了UsernamePasswordAuthenticationFilter的工作流程,留下了一个小小的伏笔,作为一个Servlet Filter应该存在一个doFilter实现方法,而它却没有,其实它的父类AbstractAuthenticationProcessingFilter提供了具体的实现。稍后我们会根据这个实现引出今天的主角AuthenticationManager,来继续介绍用户的认证过程。

2. AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter作为UsernamePasswordAuthenticationFilter的父类,实现了认证过滤器的处理逻辑。我们来看看它的核心方法doFilter的实现:

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
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {

HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;

// 先通过请求的uri来判断是否需要认证,比如默认的/login
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);

return;
}

if (logger.isDebugEnabled()) {
logger.debug("Request is to process authentication");
}

Authentication authResult;

try {
// 接着就是执行子类钩子方法attemptAuthentication来获取认证结果对象Authentication ,这个对象不能是空 否则直接返回
authResult = attemptAuthentication(request, response);
if (authResult == null) {
// return immediately as subclass has indicated that it hasn't completed
// authentication
return;
}
// 处理session 策略,这里默认没有任何策略
sessionStrategy.onAuthentication(authResult, request, response);
}
catch (InternalAuthenticationServiceException failed) {
logger.error(
"An internal error occurred while trying to authenticate the user.",
failed);
// 如果遇到异常 就会交给认证失败处理器 AuthenticationFailureHandler 来处理
unsuccessfulAuthentication(request, response, failed);

return;
}
catch (AuthenticationException failed) {
// Authentication failed
unsuccessfulAuthentication(request, response, failed);

return;
}

// 认证成功后继续其它过滤器链 并最终交给认证成功处理器 AuthenticationSuccessHandler 处理
if (continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}

successfulAuthentication(request, response, chain, authResult);
}

大部分逻辑这里是清晰的,关键在于attemptAuthentication方法,这个我们已经在上一文分析了是通过AuthenticationManagerauthenticate方法进行认证逻辑的处理,接下来我们将重点分析这个接口来帮助我们了解Spring Seucirty的认证过程。

3. AuthenticationManager

AuthenticationManager这个接口方法非常奇特,入参和返回值的类型都是Authentication。该接口的作用是对用户的未授信凭据进行认证,认证通过则返回授信状态的凭据,否则将抛出认证异常AuthenticationException

3.1 AuthenticationManager的初始化流程

那么AbstractAuthenticationProcessingFilter中的 AuthenticationManager是在哪里配置的呢?看过之前文章的应该知道WebSecurityConfigurerAdapter中的void configure(AuthenticationManagerBuilder auth)是配置AuthenticationManager的地方,我根据源码总结了一下AuthenticationManager的初始化流程,相信可以帮助你去阅读相关的源码:

需要注意的是如果我们使用自定义配置一定不能按照类似下面的错误示范:

1
2
3
4
5
6
7
8
9
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(weChatSecurityConfigProperties.getUserDetailsService());
daoAuthenticationProvider.setPasswordEncoder(multiPasswordEncoder());
auth.authenticationProvider(daoAuthenticationProvider);
// 调用 super 将导致不生效 所以下面语句不要写
super.configure(auth);
}

3.2 AuthenticationManager的认证过程

AuthenticationManager的实现ProviderManager管理了众多的AuthenticationProvider。每一个AuthenticationProvider都只支持特定类型的Authentication,然后是对适配到的Authentication进行认证,只要有一个AuthenticationProvider认证成功,那么就认为认证成功,所有的都没有通过才认为是认证失败。认证成功后的Authentication就变成授信凭据,并触发认证成功的事件。认证失败的就抛出异常触发认证失败的事件。

从这里我们可以看出认证管理器AuthenticationManager针对特定的Authentication提供了特定的认证功能,我们可以借此来实现多种认证并存。

4. 总结

通过本文我们对Spring Security认证管理器AuthenticationManager的初始化过程和认证过程进行了分析,如果你熟悉了AuthenticationManager的逻辑可以实现多种认证方式的并存等能力,实现很多有用的逻辑,这对集成Spring Security到项目中非常重要。

转载自@felord.cn

1. 前言

前面关于Spring Security写了两篇文章,一篇是介绍UsernamePasswordAuthenticationFilter,另一篇是介绍 AuthenticationManager。很多同学表示无法理解这两个东西有什么用,能解决哪些实际问题?所以今天就对这两篇理论进行实战运用,我们从零写一个短信验证码登录并适配到Spring Security体系中。如果你在阅读中有什么疑问可以回头看看这两篇文章,能解决很多疑惑。

当然你可以修改成邮箱或者其它通讯设备的验证码登录。

2. 验证码生命周期

验证码存在有效期,一般5分钟。 一般逻辑是用户输入手机号后去获取验证码,服务端对验证码进行缓存。在最大有效期内用户只能使用验证码验证成功一次(避免验证码浪费);超过最大时间后失效。

验证码的缓存生命周期

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
public interface CaptchaCacheStorage {

/**
* 验证码放入缓存.
*
* @param phone the phone
* @return the string
*/
String put(String phone);

/**
* 从缓存取验证码.
*
* @param phone the phone
* @return the string
*/
String get(String phone);

/**
* 验证码手动过期.
*
* @param phone the phone
*/
void expire(String phone);
}

我们一般会借助于缓存中间件,比如RedisEhcacheMemcached等等来做这个事情。为了方便收看该教程的同学们所使用的不同的中间件。这里我结合Spring Cache特意抽象了验证码的缓存处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
private static final String SMS_CAPTCHA_CACHE = "captcha";
@Bean
CaptchaCacheStorage captchaCacheStorage() {
return new CaptchaCacheStorage() {

@CachePut(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String put(String phone) {
return RandomUtil.randomNumbers(5);
}

@Cacheable(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String get(String phone) {
return null;
}

@CacheEvict(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public void expire(String phone) {

}
};
}

务必保证缓存的可靠性,这与用户的体验息息相关。

接着我们就来编写和业务无关的验证码服务了,验证码服务的核心功能有两个:发送验证码验证码校验。其它的诸如统计、黑名单、历史记录可根据实际业务定制。这里只实现核心功能。

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
/**
* 验证码服务.
* 两个功能: 发送和校验.
*
* @param captchaCacheStorage the captcha cache storage
* @return the captcha service
*/
@Bean
public CaptchaService captchaService(CaptchaCacheStorage captchaCacheStorage) {
return new CaptchaService() {
@Override
public boolean sendCaptcha(String phone) {
String existed = captchaCacheStorage.get(phone);
if (StringUtils.hasText(existed)) {
// 节约成本的话如果缓存中有当前手机可用的验证码 不再发新的验证码
return true;
}
// 生成验证码并放入缓存
String captchaCode = captchaCacheStorage.put(phone);
log.info("captcha: {}", captchaCode);

//todo 这里自行完善调用第三方短信服务发送验证码
return true;
}

@Override
public boolean verifyCaptcha(String phone, String code) {
String cacheCode = captchaCacheStorage.get(phone);

if (Objects.equals(cacheCode, code)) {
// 验证通过手动过期
captchaCacheStorage.expire(phone);
return true;
}
return false;
}
};
}

接下来就可以根据CaptchaService编写短信发送接口/captcha/{phone}了。

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
@RestController
@RequestMapping("/captcha")
public class CaptchaController {

@Resource
CaptchaService captchaService;


/**
* 模拟手机号发送验证码.
*
* @param phone the mobile
* @return the rest
*/
@GetMapping("/{phone}")
public Rest<?> captchaByMobile(@PathVariable String phone) {
//todo 手机号 正则自行验证

if (captchaService.sendCaptcha(phone)){
return RestBody.ok("验证码发送成功");
}
return RestBody.failure(-999,"验证码发送失败");
}

}

3. 集成到Spring Security

下面的教程就必须用到前两篇介绍的知识了。我们要实现验证码登录就必须定义一个Servlet Filter进行处理。它的作用这里再重复一下:

  • 拦截短信登录接口。
  • 获取登录参数并封装为Authentication凭据。
  • 交给AuthenticationManager认证。

我们需要先定制AuthenticationAuthenticationManager

3.1 验证码凭据

Authentication在我看来就是一个载体,在未得到认证之前它用来携带登录的关键参数,比如用户名和密码、验证码;在认证成功后它携带用户的信息和角色集。所以模仿UsernamePasswordAuthenticationToken来实现一个CaptchaAuthenticationToken,去掉不必要的功能,抄就完事儿了:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
package cn.felord.spring.security.captcha;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/**
* 验证码认证凭据.
* @author felord.cn
*/
public class CaptchaAuthenticationToken extends AbstractAuthenticationToken {

private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

private final Object principal;
private String captcha;

/**
* 此构造函数用来初始化未授信凭据.
*
* @param principal the principal
* @param captcha the captcha
* @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String, Collection)
*/
public CaptchaAuthenticationToken(Object principal, String captcha) {
super(null);
this.principal = principal;
this.captcha = captcha;
setAuthenticated(false);
}

/**
* 此构造函数用来初始化授信凭据.
*
* @param principal the principal
* @param captcha the captcha
* @param authorities the authorities
* @see CaptchaAuthenticationToken#CaptchaAuthenticationToken(Object, String)
*/
public CaptchaAuthenticationToken(Object principal, String captcha,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.captcha = captcha;
super.setAuthenticated(true); // must use super, as we override
}

public Object getCredentials() {
return this.captcha;
}

public Object getPrincipal() {
return this.principal;
}

public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}

super.setAuthenticated(false);
}

@Override
public void eraseCredentials() {
super.eraseCredentials();
captcha = null;
}

3.2 验证码认证管理器

我们还需要定制一个AuthenticationManager来对上面定义的凭据CaptchaAuthenticationToken进行认证处理。下面这张图有必要再拿出来看一下:

要定义AuthenticationManager只需要定义其实现ProviderManager。而ProviderManager又需要依赖AuthenticationProvider。所以我们要实现一个专门处理CaptchaAuthenticationTokenAuthenticationProviderAuthenticationProvider的流程是:

  1. CaptchaAuthenticationToken拿到手机号、验证码。
  2. 利用手机号从数据库查询用户信息,并判断用户是否是有效用户,实际上就是实现UserDetailsService接口
  3. 验证码校验。
  4. 校验成功则封装授信的凭据。
  5. 校验失败抛出认证异常。

根据这个流程实现如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
package cn.felord.spring.security.captcha;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.context.MessageSource;
import org.springframework.context.MessageSourceAware;
import org.springframework.context.support.MessageSourceAccessor;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityMessageSource;
import org.springframework.security.core.authority.mapping.GrantedAuthoritiesMapper;
import org.springframework.security.core.authority.mapping.NullAuthoritiesMapper;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.util.Assert;

import java.util.Collection;
import java.util.Objects;

/**
* 验证码认证器.
* @author felord.cn
*/
@Slf4j
public class CaptchaAuthenticationProvider implements AuthenticationProvider, InitializingBean, MessageSourceAware {
private final GrantedAuthoritiesMapper authoritiesMapper = new NullAuthoritiesMapper();
private final UserDetailsService userDetailsService;
private final CaptchaService captchaService;
private MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor();

/**
* Instantiates a new Captcha authentication provider.
*
* @param userDetailsService the user details service
* @param captchaService the captcha service
*/
public CaptchaAuthenticationProvider(UserDetailsService userDetailsService, CaptchaService captchaService) {
this.userDetailsService = userDetailsService;
this.captchaService = captchaService;
}

@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Assert.isInstanceOf(CaptchaAuthenticationToken.class, authentication,
() -> messages.getMessage(
"CaptchaAuthenticationProvider.onlySupports",
"Only CaptchaAuthenticationToken is supported"));

CaptchaAuthenticationToken unAuthenticationToken = (CaptchaAuthenticationToken) authentication;

String phone = unAuthenticationToken.getName();
String rawCode = (String) unAuthenticationToken.getCredentials();

UserDetails userDetails = userDetailsService.loadUserByUsername(phone);

// 此处省略对UserDetails 的可用性 是否过期 是否锁定 是否失效的检验 建议根据实际情况添加 或者在 UserDetailsService 的实现中处理
if (Objects.isNull(userDetails)) {
throw new BadCredentialsException("Bad credentials");
}

// 验证码校验
if (captchaService.verifyCaptcha(phone, rawCode)) {
return createSuccessAuthentication(authentication, userDetails);
} else {
throw new BadCredentialsException("captcha is not matched");
}

}

@Override
public boolean supports(Class<?> authentication) {
return CaptchaAuthenticationToken.class.isAssignableFrom(authentication);
}

@Override
public void afterPropertiesSet() throws Exception {
Assert.notNull(userDetailsService, "userDetailsService must not be null");
Assert.notNull(captchaService, "captchaService must not be null");
}

@Override
public void setMessageSource(MessageSource messageSource) {
this.messages = new MessageSourceAccessor(messageSource);
}

/**
* 认证成功将非授信凭据转为授信凭据.
* 封装用户信息 角色信息。
*
* @param authentication the authentication
* @param user the user
* @return the authentication
*/
protected Authentication createSuccessAuthentication(Authentication authentication, UserDetails user) {

Collection<? extends GrantedAuthority> authorities = authoritiesMapper.mapAuthorities(user.getAuthorities());
CaptchaAuthenticationToken authenticationToken = new CaptchaAuthenticationToken(user, null, authorities);
authenticationToken.setDetails(authentication.getDetails());

return authenticationToken;
}

}

然后就可以组装ProviderManager了:

1
ProviderManager providerManager = new ProviderManager(Collections.singletonList(captchaAuthenticationProvider));

经过3.13.2的准备,我们的准备工作就完成了。

3.3 验证码认证过滤器

定制好验证码凭据和验证码认证管理器后我们就可以定义验证码认证过滤器了。修改一下UsernamePasswordAuthenticationFilter就能满足需求:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package cn.felord.spring.security.captcha;

import org.springframework.lang.Nullable;
import org.springframework.security.authentication.AuthenticationServiceException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

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

public class CaptchaAuthenticationFilter extends AbstractAuthenticationProcessingFilter {


public static final String SPRING_SECURITY_FORM_PHONE_KEY = "phone";
public static final String SPRING_SECURITY_FORM_CAPTCHA_KEY = "captcha";


public CaptchaAuthenticationFilter() {
super(new AntPathRequestMatcher("/clogin", "POST"));
}

public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {

if (!request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

String phone = obtainPhone(request);
String captcha = obtainCaptcha(request);

if (phone == null) {
phone = "";
}

if (captcha == null) {
captcha = "";
}

phone = phone.trim();

CaptchaAuthenticationToken authRequest = new CaptchaAuthenticationToken(
phone, captcha);

// Allow subclasses to set the "details" property
setDetails(request, authRequest);

return this.getAuthenticationManager().authenticate(authRequest);
}

@Nullable
protected String obtainCaptcha(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_CAPTCHA_KEY);
}

@Nullable
protected String obtainPhone(HttpServletRequest request) {
return request.getParameter(SPRING_SECURITY_FORM_PHONE_KEY);
}

protected void setDetails(HttpServletRequest request,
CaptchaAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

}

这里我们指定了拦截验证码登陆的请求为:

1
2
3
POST /clogin?phone=手机号&captcha=验证码 HTTP/1.1

Host: localhost:8082

接下来就是配置了。

3.4 配置

我把所有的验证码认证的相关配置集中了起来,并加上了注释。

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
package cn.felord.spring.security.captcha;

import cn.hutool.core.util.RandomUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.ProviderManager;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.Objects;

/**
* 验证码认证配置.
*
* @author felord.cn
* @since 13 :23
*/
@Slf4j
@Configuration
public class CaptchaAuthenticationConfiguration {
private static final String SMS_CAPTCHA_CACHE = "captcha";

/**
* spring cache 管理验证码的生命周期.
*
* @return the captcha cache storage
*/
@Bean
CaptchaCacheStorage captchaCacheStorage() {
return new CaptchaCacheStorage() {

@CachePut(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String put(String phone) {
return RandomUtil.randomNumbers(5);
}

@Cacheable(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public String get(String phone) {
return null;
}

@CacheEvict(cacheNames = SMS_CAPTCHA_CACHE, key = "#phone")
@Override
public void expire(String phone) {

}
};
}

/**
* 验证码服务.
* 两个功能: 发送和校验.
*
* @param captchaCacheStorage the captcha cache storage
* @return the captcha service
*/
@Bean
public CaptchaService captchaService(CaptchaCacheStorage captchaCacheStorage) {
return new CaptchaService() {
@Override
public boolean sendCaptcha(String phone) {
String existed = captchaCacheStorage.get(phone);
if (StringUtils.hasText(existed)) {
// 节约成本的话如果缓存存在可用的验证码 不再发新的验证码
log.warn("captcha code 【 {} 】 is available now", existed);
return false;
}
// 生成验证码并放入缓存
String captchaCode = captchaCacheStorage.put(phone);
log.info("captcha: {}", captchaCode);

//todo 这里自行完善调用第三方短信服务
return true;
}

@Override
public boolean verifyCaptcha(String phone, String code) {
String cacheCode = captchaCacheStorage.get(phone);

if (Objects.equals(cacheCode, code)) {
// 验证通过手动过期
captchaCacheStorage.expire(phone);
return true;
}
return false;
}
};
}

/**
* 自行实现根据手机号查询可用的用户,这里简单举例.
* 注意该接口可能出现多态。所以最好加上注解@Qualifier
*
* @return the user details service
*/
@Bean
@Qualifier("captchaUserDetailsService")
public UserDetailsService captchaUserDetailsService() {
// 验证码登陆后密码无意义了但是需要填充一下
return username -> User.withUsername(username).password("TEMP")
//todo 这里权限 你需要自己注入
.authorities(AuthorityUtils.createAuthorityList("ROLE_ADMIN", "ROLE_APP")).build();
}

/**
* 验证码认证器.
*
* @param captchaService the captcha service
* @param userDetailsService the user details service
* @return the captcha authentication provider
*/
@Bean
public CaptchaAuthenticationProvider captchaAuthenticationProvider(CaptchaService captchaService,
@Qualifier("captchaUserDetailsService")
UserDetailsService userDetailsService) {
return new CaptchaAuthenticationProvider(userDetailsService, captchaService);
}


/**
* 验证码认证过滤器.
*
* @param authenticationSuccessHandler the authentication success handler
* @param authenticationFailureHandler the authentication failure handler
* @param captchaAuthenticationProvider the captcha authentication provider
* @return the captcha authentication filter
*/
@Bean
public CaptchaAuthenticationFilter captchaAuthenticationFilter(AuthenticationSuccessHandler authenticationSuccessHandler,
AuthenticationFailureHandler authenticationFailureHandler,
CaptchaAuthenticationProvider captchaAuthenticationProvider) {
CaptchaAuthenticationFilter captchaAuthenticationFilter = new CaptchaAuthenticationFilter();
// 配置 authenticationManager
ProviderManager providerManager = new ProviderManager(Collections.singletonList(captchaAuthenticationProvider));
captchaAuthenticationFilter.setAuthenticationManager(providerManager);
// 成功处理器
captchaAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
// 失败处理器
captchaAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler);

return captchaAuthenticationFilter;
}
}

然而这并没有完,你需要将CaptchaAuthenticationFilter配置到整个Spring Security的过滤器链中,这种看了胖哥教程的同学应该非常熟悉了。

请特别注意:务必保证登录接口和验证码接口可以匿名访问,如果是动态权限可以给接口添加ROLE_ANONYMOUS角色。

大功告成,测试如下:


而且原先的登录方式不受影响。

4. 总结

通过对UsernamePasswordAuthenticationFilter和 AuthenticationManager的系统学习,我们了解了Spring Security认证的整个流程,本文是对这两篇的一个实际运用。相信看到这一篇后你就不会对前几篇的图解懵逼了,这也是理论到实践的一次尝试。

代码在day11分支

转载自@felord.cn

1. 前言

欢迎阅读Spring Security 实战干货系列文章,在集成Spring Security安全框架的时候我们最先处理的可能就是根据我们项目的实际需要来定制注册登录了,尤其是Http登录认证。根据以前的相关文章介绍,Http登录认证由过滤器UsernamePasswordAuthenticationFilter进行处理。我们只有把这个过滤器搞清楚才能做一些定制化。今天我们就简单分析它的源码和工作流程。

2. UsernamePasswordAuthenticationFilter 源码分析

UsernamePasswordAuthenticationFilter继承于AbstractAuthenticationProcessingFilter(另文分析)。它的作用是拦截登录请求并获取账号和密码,然后把账号密码封装到认证凭据UsernamePasswordAuthenticationToken中,然后把凭据交给特定配置的AuthenticationManager去作认证。源码分析如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
public class UsernamePasswordAuthenticationFilter extends
AbstractAuthenticationProcessingFilter {
// 默认取账户名、密码的key
public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username";
public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password";
// 可以通过对应的set方法修改
private String usernameParameter = SPRING_SECURITY_FORM_USERNAME_KEY;
private String passwordParameter = SPRING_SECURITY_FORM_PASSWORD_KEY;
// 默认只支持 POST 请求
private boolean postOnly = true;

// 初始化一个用户密码 认证过滤器 默认的登录uri 是 /login 请求方式是POST
public UsernamePasswordAuthenticationFilter() {
super(new AntPathRequestMatcher("/login", "POST"));
}

// 实现其父类 AbstractAuthenticationProcessingFilter 提供的钩子方法 用去尝试认证
public Authentication attemptAuthentication(HttpServletRequest request,
HttpServletResponse response) throws AuthenticationException {
// 判断请求方式是否是POST
if (postOnly && !request.getMethod().equals("POST")) {
throw new AuthenticationServiceException(
"Authentication method not supported: " + request.getMethod());
}

// 先去 HttpServletRequest 对象中获取账号名、密码
String username = obtainUsername(request);
String password = obtainPassword(request);

if (username == null) {
username = "";
}

if (password == null) {
password = "";
}

username = username.trim();

// 然后把账号名、密码封装到 一个认证Token对象中,这是就是一个通行证,但是这时的状态时不可信的,一旦通过认证就变为可信的
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, password);

// 会将 HttpServletRequest 中的一些细节 request.getRemoteAddr() request.getSession 存入的到Token中
setDetails(request, authRequest);

// 然后 使用 父类中的 AuthenticationManager 对Token 进行认证
return this.getAuthenticationManager().authenticate(authRequest);
}
// 获取密码 很重要 如果你想改变获取密码的方式要么在此处重写,要么通过自定义一个前置的过滤器保证能此处能get到
@Nullable
protected String obtainPassword(HttpServletRequest request) {
return request.getParameter(passwordParameter);
}

// 获取账户很重要 如果你想改变获取密码的方式要么在此处重写,要么通过自定义一个前置的过滤器保证能此处能get到
@Nullable
protected String obtainUsername(HttpServletRequest request) {
return request.getParameter(usernameParameter);
}

// 参见上面对应的说明为凭据设置一些请求细节
protected void setDetails(HttpServletRequest request,
UsernamePasswordAuthenticationToken authRequest) {
authRequest.setDetails(authenticationDetailsSource.buildDetails(request));
}

// 设置账户参数的key
public void setUsernameParameter(String usernameParameter) {
Assert.hasText(usernameParameter, "Username parameter must not be empty or null");
this.usernameParameter = usernameParameter;
}

// 设置密码参数的key
public void setPasswordParameter(String passwordParameter) {
Assert.hasText(passwordParameter, "Password parameter must not be empty or null");
this.passwordParameter = passwordParameter;
}

// 认证的请求方式是只支持POST请求
public void setPostOnly(boolean postOnly) {
this.postOnly = postOnly;
}

public final String getUsernameParameter() {
return usernameParameter;
}

public final String getPasswordParameter() {
return passwordParameter;
}
}

为了加强对流程的理解,我特意画了一张图来对这个流程进行清晰的说明:

3. 我们可以定制什么

根据上面的流程,我们理解了UsernamePasswordAuthenticationFilter工作流程后可以做这些事情:

  • 定制我们的登录请求URI和请求方式。
  • 登录请求参数的格式定制化,比如可以使用JSON格式提交甚至几种并存。
  • 将用户名和密码封装入凭据UsernamePasswordAuthenticationToken,定制业务场景需要的特殊凭据。

4. 我们会有什么疑问

AuthenticationManager从哪儿来,它又是什么,它是如何对凭据进行认证的,认证成功的后续细节是什么,认证失败的后续细节是什么。不要走开,持续关注为你揭晓这个答案。

转载自@felord.cn

1. 前言

Spring Security真正的过滤器体系是我们了解它是如何进行认证授权防止利用漏洞的关键。

2. Servlet Filter体系

这里我们以Servlet Web为讨论目标,Reactive Web暂不讨论。我们先来看下最基础的Servlet体系,在Servlet体系中客户端发起一个请求过程是经过0到N个Filter然后交给Servlet处理。

Filter不但可以修改HttpServletRequestHttpServletResponse,可以让我们在请求响应的前后做一些事情,甚至可以终止过滤器链FilterChain的传递。

1
2
3
4
5
6
7
8
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
// 请求被servlet 处理前
if(condition){
// 根据条件来进入下一个过滤器
chain.doFilter(request, response);
}
// 请求被执行完毕后处理一些事情
}

3. GenericFilterBean

Spring Security利用了Spring IOCAOP的特性而无法脱离Spring独立存在,而Apache Shiro可以独立存在。所以今天我们要一探究竟,看看他们是如何结合的。

Spring结合Servlet Filter自然是要为Servlet Filter注入Spring Bean的特性,所以就搞出了一个抽象Filter Bean,这个抽象过滤器GenericFilterBean并不是在Spring Security下,而是Spring Web体系中,类图如下:

从类图上看Filter接口已经被注入了多个Spring Bean的特性,纳入了Spring Bean生命周期,使得Spring IoC容器能够充分的管理Filter

4. DelegatingFilterProxy

我们希望Servlet能够按照它自己的标准来注册到过滤器链中工作,但是同时也希望它能够被Spring IoC管理,所以Spring提供了一个GenericFilterBean的实现DelegatingFilterProxy。我们可以将原生的Servlet Filter或者Spring Bean Filter委托给DelegatingFilterProxy,然后在结合到Servlet FilterChain中。

5. SecurityFilterChain

针对不同符合Ant Pattern的请求可能会走不同的过滤器链,比如登录会去验证,然后返回登录结果;管理后台的接口走后台的安全逻辑,应用客户端的接口走客户端的安全逻辑。Spring Security提供了一个SecurityFilterChain接口来满足被匹配HttpServletRequest走特定的过滤器链的需求。

1
2
3
4
5
6
public interface SecurityFilterChain {
// 判断请求 是否符合该过滤器链的要求
boolean matches(HttpServletRequest request);
// 对应的过滤器链
List<Filter> getFilters();
}

6. FilterChainProxy

不同的SecurityFilterChain应该是互斥而且平等的,它们之间不应该是上下游关系。

如上图请求被匹配到不同的SecurityFilterChain然后在执行剩余的过滤器链。它们经过SecurityFilterChain的总流程是相似的,而且有些时候特定的一些SecurityFilterChain也需要被集中管理来实现特定一揽子的请求的过滤逻辑。所以就有了另外一个GenericFilterBean实现来做这个事情,它就是FilterChainProxy。它的作用就是拦截符合条件的请求,然后根据请求筛选出符合要求的SecurityFilterChain,然后链式的执行这些Filter,最后继续执行剩下的FilterChain

7. 总结

结合上面,最终上述这些概念的关系彻底搞清楚了,搞清楚过滤器的运作模式对于学习和使用Spring Security至关重要。

转载自@felord.cn

1. 前言

最近有开发小伙伴提了一个有趣的问题。他正在做一个项目,涉及两种风格,一种是给小程序出接口,安全上使用无状态的JWT Token;另一种是管理后台使用的是Freemarker,也就是前后端不分离的Session机制。用Spring Security该怎么办?

2. 解决方案

我们可以通过多次继承WebSecurityConfigurerAdapter构建多个HttpSecurityHttpSecurity 对象会告诉我们如何验证用户的身份,如何进行访问控制,采取的何种策略等等。

如果你看过之前的教程,我们是这么配置的:

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
/**
* 单策略配置
*
* @author felord.cn
* @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
* @since 14 :58 2019/10/15
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
@EnableWebSecurity
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

/**
* The type Default configurer adapter.
*/
@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER)
static class DefaultConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
}

@Override
public void configure(WebSecurity web) {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置 httpSecurity

}
}
}

上面的配置了一个HttpSecurity,我们如法炮制再增加一个WebSecurityConfigurerAdapter的子类来配置另一个HttpSecurity。伴随而来的还有不少的问题要解决。

2.1 如何路由不同的安全配置

我们配置了两个HttpSecurity之后,程序如何让小程序接口和后台接口走对应的HttpSecurity?

HttpSecurity.antMatcher(String antPattern)可以提供过滤机制。比如我们配置:

1
2
3
4
5
6
@Override
protected void configure(HttpSecurity http) throws Exception {
// 配置 httpSecurity
http.antMatcher("/admin/v1");

}

那么该HttpSecurity将只提供给以/admin/v1开头的所有URL。这要求我们针对不同的客户端指定统一的URL前缀。

举一反三只要HttpSecurity提供的功能都可以进行个性化定制。比如登录方式,角色体系等。

2.2 如何指定默认的HttpSecurity

我们可以通过在WebSecurityConfigurerAdapter实现上使用@Order注解来指定优先级,数值越大优先级越低,没有@Order注解将优先级最低。

2.3 如何配置不同的UserDetailsService

很多情况下我们希望普通用户和管理用户完全隔离,我们就需要多个UserDetailsService,你可以在下面的方法中对AuthenticationManagerBuilder进行具体的设置来配置UserDetailsService,同时也可以配置不同的密码策略。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 自行实现
return null ;
}
});
// 也可以设计特定的密码策略
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
auth.authenticationProvider(daoAuthenticationProvider);
}

2.4 最终的配置模板

上面的几个问题解决之后,我们基本上掌握了在一个应用中执行多种安全策略。配置模板如下:

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
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
* 多个策略配置
*
* @author felord.cn
* @see org.springframework.boot.autoconfigure.security.servlet.SpringBootWebSecurityConfiguration
* @since 14 :58 2019/10/15
*/
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true, jsr250Enabled = true, securedEnabled = true)
@EnableWebSecurity
@ConditionalOnClass(WebSecurityConfigurerAdapter.class)
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
public class CustomSpringBootWebSecurityConfiguration {

/**
* 后台接口安全策略. 默认配置
*/
@Configuration
@Order(1)
static class AdminConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//用户详情服务个性化
daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 自行实现
return null;
}
});
// 也可以设计特定的密码策略
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
auth.authenticationProvider(daoAuthenticationProvider);
}

@Override
public void configure(WebSecurity web) {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 根据需求自行定制
http.antMatcher("/admin/v1")
.sessionManagement(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());


}
}

/**
* app接口安全策略. 没有{@link Order}注解优先级比上面低
*/
@Configuration
static class AppConfigurerAdapter extends WebSecurityConfigurerAdapter {

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
DaoAuthenticationProvider daoAuthenticationProvider = new DaoAuthenticationProvider();
//用户详情服务个性化
daoAuthenticationProvider.setUserDetailsService(new UserDetailsService() {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 自行实现
return null;
}
});
// 也可以设计特定的密码策略
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
daoAuthenticationProvider.setPasswordEncoder(bCryptPasswordEncoder);
auth.authenticationProvider(daoAuthenticationProvider);
}

@Override
public void configure(WebSecurity web) {
super.configure(web);
}

@Override
protected void configure(HttpSecurity http) throws Exception {
// 根据需求自行定制
http.antMatcher("/app/v1")
.sessionManagement(Customizer.withDefaults())
.formLogin(Customizer.withDefaults());


}
}
}

3. 总结

今天我们解决了如何针对不同类型接口采取不同的安全策略的方法,希望对你有用

转载自@felord.cn

1. 前言

过滤器作为 Spring Security 的重中之重,我们需要了解其中的机制。这样我们才能根据业务需求的变化进行定制。今天来探讨一下 Spring Security 中的过滤器链机制

2. Spring Security 过滤器链

客户端(APP 和后台管理客户端)向应用程序发送请求,然后应用根据请求的 URI 的路径来确定该请求的过滤器链(Filter)以及最终的具体 Servlet 控制器(Controller)。

从上图我们可以看出 Spring Security 以一个单 Filter(FilterChainProxy) 存在于整个过滤器链中,而这个 FilterChainProxy 实际内部代理着众多的 Spring Security Filter 。这简直就是套娃啊!

2.1 过滤器链的形成过程

再多说一点 Filter 们的初始化过程,首先 Filter 们按照一定的顺序被 SecurityBuilder 的实现来组装为 SecurityFilterChain ,然后通过 WebSecurity 注入到 FilterChainProxy 中去,接着 FilterChainProxy 又在 WebSecurityConfiguration 中以 springSecurityFilterChain 的名称注册为 Spring Bean 。实际上还有一个隐藏层 DelegatingFilterProxy 代理了 springSecurityFilterChain 注入到最后整个 Servlet 过滤器链中。 简单画了个图:

事实上 Spring Security 的内置 Filter 对于 Spring IoC 容器来说都是不可见的。

Spring Security 允许有多 条过滤器链并行,Spring SecurityFilterChainProxy 可以代理多条过滤器链并根据不同的 URI 匹配策略进行分发。但是每个请求每次只能被分发到一条过滤器链。如下图所示:

实际每条过滤链 就是一个 SecurityFilterChain

4. 总结

今天我们通过对 Spring Security 中 过滤器链机制,对于深入学习 Spring Security 有着至关重要的意义。

转载自@felord.cn