Spring Security专栏(如何使用高级主题保护we

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

写在前面

今天我们一起来学习如何使用高级主题保护web应用,何为高级主题,即包括过滤器、CSRF 保护、CORS 以及全局方法,这些都是非常实用的功能特性,今天这个算是对以前的学习进行下总结吧。

本次分为两部分来说。那么作为阶段性的总结,今天的内容将利用这些功能特性构建在安全领域中的一种典型的认证机制,即多因素认证(Multi-Factor Authentication,MFA)机制。

下面我们通过一些案例分享下

案例设计和初始化

在今天的案例中,我们构建多因素认证的思路并不是采用第三方成熟的解决方案,而是基于 Spring Security 的功能特性来自己设计并实现一个简单而完整的认证机制。

开头说到多因素认证:多因素认证是一种安全访问控制的方法,基本的设计理念在于用户想要访问最终的资源,至少需要通过两种以上的认证机制。

那么,我们如何实现多种认证机制呢?一种常见的做法是分成两个步骤,第一步通过用户名和密码获取一个认证码(Authentication Code),第二步基于用户名和这个认证码进行安全访问。基于这种多因素认证的基本执行流程如下图所示:

image.png

系统初始化

为了实现多因素认证,我们需要构建一个独立的认证服务 Auth-Service,该服务同时提供了基于用户名+密码以及用户名+认证码的认证形式。当然,实现认证的前提是构建用户体系,因此我们需要提供如下所示的 User 实体类:

1
2
3
4
5
6
7
8
9
10
java复制代码@Entity
public class User {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
   
    private String username;
    private String password;
}

可以看到,User 对象中包含了用户名 Username 和密码 Password 的定义。同样的,在如下所示的代表认证码的 AuthCode 对象中包含了用户名 Username 和具体的认证码 Code 字段的定义:

1
2
3
4
5
6
7
8
9
10
java复制代码@Entity
public class AuthCode {
 
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
   
    private String username;
    private String code;  
}

基于 User 和 AuthCode 实体对象,我们也给出创建数据库表的对应 SQL 定义,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
java复制代码CREATE TABLE IF NOT EXISTS `spring_security_demo`.`user` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(45) NULL,
    `password` TEXT NULL,
    PRIMARY KEY (`id`));
 
CREATE TABLE IF NOT EXISTS `spring_security_demo`.`auth_code` (
    `id` INT NOT NULL AUTO_INCREMENT,
    `username` VARCHAR(45) NOT NULL,
    `code` VARCHAR(45) NULL,
    PRIMARY KEY (`id`));
)

有了认证服务,接下来我们需要构建一个业务服务 Business-Service,该业务服务通过集成认证服务,完成具体的认证操作,并返回访问令牌(Token)给到客户端系统。因此,从依赖关系上讲,Business-Service 会调用 Auth-Service,如下图所示:

image.png

接下来,我们分别从这两个服务入手,实现多因素认证机制。

实现多因素认证机制

对于多因素认证机制而言,实现认证服务是基础,但难度并不大,我们往下看。

实现认证服务

从表现形式上看,认证服务也是一个 Web 服务,所以内部需要通过构建 Controller 层组件实现 HTTP 端点的暴露。为此,我们构建了如下所示的 AuthController:

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
java复制代码@RestController
public class AuthController {
 
    @Autowired
    private UserService userService;
 
    //添加User
    @PostMapping("/user/add")
    public void addUser(@RequestBody User user) {
        userService.addUser(user);
    }
 
    //通过用户名+密码对用户进行首次认证
    @PostMapping("/user/auth")
    public void auth(@RequestBody User user) {
        userService.auth(user);
    }
 
    //通过用户名+认证码进行二次认证
    @PostMapping("/authcode/check")
    public void check(@RequestBody AuthCode authCode, HttpServletResponse response) {
        if (userService.check(authCode)) {
            response.setStatus(HttpServletResponse.SC_OK);
        } else {
            response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        }
    }
}

可以看到,这里除了一个添加用户信息的 HTTP 端点之外,我们分别实现了通过用户名+密码对用户进行首次认证的”/user/auth”端点,以及通过用户名+认证码进行二次认证的”/authcode/check”端点。

这两个核心端点背后的实现逻辑都位于 UserService 中,我们先来看其中的 auth() 方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
java复制代码public void auth(User user) {
        Optional<User> o =
                userRepository.findUserByUsername(user.getUsername());
 
        if(o.isPresent()) {
            User u = o.get();
            if (passwordEncoder.matches(user.getPassword(), u.getPassword())) {
                 //生成或刷新认证码
                generateOrRenewAutoCode(u);
            } else {
                throw new BadCredentialsException("Bad credentials.");
            }
        } else {
            throw new BadCredentialsException("Bad credentials.");
        }
}

上述代码中的关键流程就是在完成用户密码匹配之后的刷新认证码流程,负责实现该流程的 generateOrRenewAutoCode() 方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码private void generateOrRenewAutoCode (User u) {
        String generatedCode = GenerateCodeUtil.generateCode();
 
        Optional<AuthCode> autoCode = autoCodeRepository.findAuthCodeByUsername(u.getUsername());
        if (autoCode.isPresent()) {//如果存在认证码,则刷新该认证码
            AuthCode code = autoCode.get();
            code.setCode(generatedCode);
        } else {//如果没有找到认证码,则生成并保存一个新的认证码
            AuthCode code = new AuthCode();
            code.setUsername(u.getUsername());
            code.setCode(generatedCode);
            autoCodeRepository.save(code);
        }
}

上述方法的流程也很明确,首先通过调用工具类 GenerateCodeUtil 的 generateCode() 方法生成一个认证码,然后根据当前数据库中的状态决定是否对已有的认证码进行刷新,或者直接生成一个新的认证码并保存。因此,每次调用 UserService 的 auth() 方法就相当于对用户的认证码进行了动态重置。

一旦用户获取了认证码,并通过该认证码访问系统,认证服务就可以对该认证码进行校验,从而确定其是否有效。对认证码进行验证的方法如下所示:

1
2
3
4
5
6
7
8
9
10
11
java复制代码public boolean check(AuthCode authCodeToValidate) {
        Optional<AuthCode> authCode = autoCodeRepository.findAuthCodeByUsername(authCodeToValidate.getUsername());
        if (authCode.isPresent()) {
            AuthCode authCodeInStore = authCode.get();
            if (authCodeToValidate.getCode().equals(authCodeInStore.getCode())) {
                return true;
            }
        }
 
        return false;
}

这里的逻辑也很简单,就是把从数据库中获取的认证码与用户传入的认证码进行比对。

至此,认证服务的核心功能已经构建完毕

下期我们来看业务服务的实现过程。我们还是要一点点学,这样才能消化得了。

下期再见 加油!!!

弦外之音

感谢你的阅读,如果你感觉学到了东西,您可以点赞,关注。也欢迎有问题我们下面评论交流

加油! 我们下期再见!

给大家分享几个我前面写的几篇骚操作

copy对象,这个操作有点骚!

干货!SpringBoot利用监听事件,实现异步操作

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

0%