龙空技术网

Spring Security 动手实践:职责分离——3.实现身份认证服务器

粥屋与包屋 228

前言:

眼前朋友们对“服务器有哪些认证”大致比较关心,你们都想要剖析一些“服务器有哪些认证”的相关资讯。那么小编也在网络上收集了一些有关“服务器有哪些认证””的相关知识,希望同学们能喜欢,咱们一起来了解一下吧!

从本文将开始编写示例的实现。第一个依赖项是身份认证服务器。即使它不是我们所关注的使用 Spring Security 的应用程序,我们也需要它来实现我们的最终结果。为了让您专注于实践中最重要的部分,我列出了实现的一些部分。我在整个示例中都提到了这些内容,并将其留给您作为练习来实现。

在我们的场景中,身份认证服务器连接到一个数据库,在该数据库中存储在请求身份认证事件期间生成的用户凭据和 OTPs。我们需要这个应用程序公开三个端点 ( 图 9 ):

/user/add -- 添加一个用户,用于测试我们后面的实现。/user/auth -- 通过用户的凭证对用户进行身份认证,并使用 OTP 发送短信。我们去掉了发送短信的部分,但你可以把它作为练习来做。/otp/check -- 验证 OTP 值是否为先前认证服务器为特定用户生成的值。

图 9

图 9 身份认证服务器的类设计控制器公开调用服务类中定义的逻辑的 REST 端点。这两个存储库是数据库的访问层。我们还编写了一个实用工具类来分离生成要通过 SMS 发送的 OTP 的验证码。

我们创建一个新项目并添加所需的依赖项,如下面的代码片段所示。

<dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-web</artifactId></dependency><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-security</artifactId></dependency><dependency>  <groupId>org.springframework.boot</groupId>  <artifactId>spring-boot-starter-data-jpa</artifactId></dependency><dependency>  <groupId>mysql</groupId>  <artifactId>mysql-connector-java</artifactId>  <scope>runtime</scope></dependency>

我们还需要确保为应用程序创建了数据库。因为我们存储用户凭据 ( 用户名和密码 ),所以需要一个表。我们还需要第二个表来存储与经过身份认证的用户相关联的 OTP 值 ( 图 10 )。

图 10

图 10 应用程序数据库有两个表。在第一个表中,应用程序存储用户凭证,而在第二个表中,应用程序存储生成的 OTP 验证码。

使用一个名为 spring 的数据库,并添加脚本来创建 schema.sql 文件中所需的两个表。 切记将 schema.sql 文件放置在项目的 resources 文件夹中,因为 Spring Boot 会在这里拾取它来执行脚本。 在下一个代码段中,您可以找到我的 schema.sql 文件的内容。 (如果您不喜欢使用 schema.sql 文件的方法,则可以随时手动创建数据库结构,也可以使用自己喜欢的任何其他方法。)

CREATE TABLE IF NOT EXISTS `spring`.`user` (    `username` VARCHAR(45) NULL,    `password` TEXT NULL,    PRIMARY KEY (`username`));CREATE TABLE IF NOT EXISTS `spring`.`otp` (    `username` VARCHAR(45) NOT NULL,    `code` VARCHAR(45) NULL,    PRIMARY KEY (`username`));

application.properties 文件中,我们提供了 Spring Boot 创建数据源所需的参数。 下一个代码片段显示了 application.properties 文件的内容:

spring.datasource.url=jdbc:mysql://localhost/springspring.datasource.username=rootspring.datasource.password=spring.datasource.initialization-mode=always

还为这个应用程序的依赖项添加了 Spring Security。我对身份认证服务器这样做的唯一原因是获得 BCryptPasswordEncoder ,我喜欢使用它来散列存储在数据库中的用户密码。为了使示例简短并与我们的目的相关,我没有在业务逻辑服务器和身份认证服务器之间实现身份认证。但是我想把这个留给您作为稍后的练习,在完成这个实际示例之后。对于我们在本文中讨论的实现,项目的配置类如清单 1 所示。

练习

更改本文中的应用程序,以验证业务逻辑服务器和身份认证服务器之间的请求:

通过使用对称密钥

使用非对称密钥对

为了解决这个练习,您可能会发现参考我们在前面文章中的示例(在过滤器链中已存在的过滤前添加过滤器)。

清单 1 身份认证服务器的配置类

@Configurationpublic class ProjectConfig   extends WebSecurityConfigurerAdapter {    // 定义密码编码器以哈希存储在数据库中的密码  @Bean  public PasswordEncoder passwordEncoder() {    return new BCryptPasswordEncoder();  }          @Override  protected void configure(HttpSecurity http) throws Exception {    http.csrf().disable(); // 禁用 CSRF,这样我们就可以直接调用应用程序的所有端点    http.authorizeRequests()  //允许所有无需身份认证就可调用          .anyRequest().permitAll();  }}

配置类就绪后,我们可以继续定义到数据库的连接。因为我们使用 Spring Data JPA,所以我们需要编写 JPA 实体,然后是存储库,因为我们有两个表,所以我们定义了两个 JPA 实体和两个存储库接口。下面的清单显示了 User 实体的定义。它表示存储用户凭证的用户表。

清单 2 User 实体

@Entitypublic class User {  @Id  private String username;  private String password;  // Omitted getters and setters}

下一个清单展示了第二个实体 Otp。这个实体表示 otp 表,应用程序在其中存储为经过身份认证的用户生成的otp

清单 3 Otp 实体

@Entitypublic class Otp {  @Id  private String username;  private String code;  // Omitted getters and setters}

清单 4 展示了 User 实体的 Spring Data JPA 存储库。在这个接口中,我们定义了一个根据用户名检索用户的方法。在验证用户名和密码的第一步中,我们需要它。

清单 4 UserRepository 接口

public interface UserRepository extends JpaRepository<User, String> {  Optional<User> findUserByUsername(String username);}

清单 5 给出了用于 Otp 实体的 Spring Data JPA 存储库。在这个接口中,我们定义了一个根据用户名检索 OTP 的方法。我们需要在第二个验证步骤中使用这个方法,在这个步骤中,我们验证用户的 OTP

清单 5 OtpRepository 接口

public interface OtpRepository extends JpaRepository<Otp, String> {  Optional<Otp> findOtpByUsername(String username);}

存储库和实体就绪后,我们就可以处理应用程序的逻辑了。为此,我创建了一个称为 UserService 的服务类。如清单 6 所示,该服务依赖于存储库和密码编码器。因为我们使用这些对象来实现应用程序逻辑,所以我们需要自动装配它们。

清单 6 自动装配 UserService 类中的依赖项

@Service@Transactionalpublic class UserService {  @Autowired  private PasswordEncoder passwordEncoder;  @Autowired  private UserRepository userRepository;  @Autowired  private OtpRepository otpRepository;}

接下来,我们需要定义一个方法来添加用户。您可以在下面的清单中找到这个方法的定义。

清单 7 addUser() 方法的定义

@Service@Transactionalpublic class UserService {  // Omitted code  public void addUser(User user) {    user.setPassword(passwordEncoder.encode(user.getPassword()));    userRepository.save(user);  }}

业务逻辑服务器需要什么? 它需要一种发送用户名和密码以进行身份认证的方法。认证成功后,认证服务器会为用户生成一个 OTP,并通过短信发送。下面的清单显示了 auth() 方法的定义,该方法实现了这个逻辑。

清单 8 实现第一个步骤身份验证

@Service@Transactionalpublic class UserService {  // Omitted code  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())) {            //如果密码正确,则会生成一个新的 OTP           renewOtp(u);        } else {            //如果密码不正确或用户名不存在,则会抛出异常           throw new BadCredentialsException("Bad credentials.");        }    } else {        //如果密码不正确或用户名不存在,则会抛出异常       throw new BadCredentialsException("Bad credentials.");    }  }  private void renewOtp(User u) {      //为 OTP 生成一个随机值    String code = GenerateCodeUtil.generateCode();      //按用户名查找OTP    Optional<Otp> userOtp =otpRepository.findOtpByUsername(u.getUsername());    if (userOtp.isPresent()) {        //如果此用户名存在 OTP,则更新其值      Otp otp = userOtp.get();      otp.setCode(code);    } else {        //如果这个用户名的 OTP 不存在,则用生成的值创建一个新记录      Otp otp = new Otp();      otp.setUsername(u.getUsername());      otp.setCode(code);      otpRepository.save(otp);    }  }  // Omitted code}

下一个清单展示了 GenerateCodeUtil 类。我们在清单 8 中使用这个类来生成新的 OTP 值。

清单 9 生成 OTP

public final class GenerateCodeUtil {  private GenerateCodeUtil() {}  public static String generateCode() {    String code;    try {        // 创建一个生成随机 int 值的 SecureRandom 实例      SecureRandom random =         SecureRandom.getInstanceStrong();              //生成0到8,999之间的值。我们给每个生成的值加1000。这样,我们得到1000到9999(4位随机码)之间的值。      int c = random.nextInt(9000) + 1000;        //将int转换为String并返回它      code = String.valueOf(c);    } catch (NoSuchAlgorithmException e) {      throw new RuntimeException(           "Problem when generating the random code.");    }   return code;  }}

UserService 中需要的最后一个方法是验证用户的 OTP。您可以在下面的清单中找到此方法。

清单 10 验证 OTP

@Service@Transactionalpublic class UserService {  / Omitted code  public boolean check(Otp otpToValidate) {    Optional<Otp> userOtp =   //按用户名搜索 OTP      otpRepository.findOtpByUsername(         otpToValidate.getUsername());    if (userOtp.isPresent()) {        //如果数据库中存在 OTP,并且与从业务逻辑服务器接收到的 OTP 相同,则返回 true。      Otp otp = userOtp.get();      if (otpToValidate.getCode().equals(otp.getCode())) {         return true;      }    }      //否则,它将返回 false。     return false;  }  // Omitted code}

最后,在这个应用程序中,我们公开了控制器提供的逻辑。下面的清单定义了这个控制器。

清单 11 AuthController 类的定义

@RestControllerpublic class AuthController {  @Autowired  private UserService userService;  @PostMapping("/user/add")  public void addUser(@RequestBody User user) {    userService.addUser(user);  }  @PostMapping("/user/auth")  public void auth(@RequestBody User user) {    userService.auth(user);  }    //如果 OTP 有效,则 HTTP 响应将返回状态 200 OK;否则,状态为 200。 否则,状态值为 403 Forbidden。  @PostMapping("/otp/check")  public void check(@RequestBody Otp otp, HttpServletResponse response) {    if (userService.check(otp)) {      response.setStatus(HttpServletResponse.SC_OK);    } else {      response.setStatus(HttpServletResponse.SC_FORBIDDEN);    }  }}

有了这个设置,我们现在就有了身份认证服务器。让我们开始它,并确保端点按我们期望的方式工作。为了测试身份认证服务器的功能,我们需要:

通过调用 /user/add 端点向数据库添加新用户;通过检查数据库中的 users 表,验证是否正确添加了用户;调用第 1 步中添加的用户的 /user/auth 端点;验证应用程序是否生成了一个 OTP 并将其存储在 OTP 表中;使用步骤 3 中生成的 OTP 来验证 /otp/check 端点是否按预期工作.

我们首先将用户添加到身份认证服务器的数据库中。我们至少需要一个用户来进行身份认证。我们可以通过调用在身份认证服务器中创建的 /user/add 端点来添加用户。因为我们没有在身份认证服务器应用程序中配置端口,所以我们使用缺省端口,即 8080。这是调用:

curl -XPOST -H "content-type: application/json" -d "{\"username\":\"xiaohua\",\"password\":\"12345\"}" 

在使用前面代码片段提供的 curl 命令添加用户之后,我们检查数据库以验证添加的记录是否正确。在我的案例中,我可以看到以下细节:

Username: xiaohuaPassword: $2a$10$.bI9ix.Y0m70iZitP.RdSuwzSqgqPJKnKpRUBQPGhoRvHA.1INYmy

应用程序在将密码存储到数据库之前将其哈希,这是预期的行为。请记住,我们在身份认证服务器中特别为此使用了 BCryptPasswordEncoder

注意

记住,在我们密码实现文章的讨论中,BCryptPasswordEncoder 使用 bcrypt 作为哈希算法。使用 bcrypt,输出是基于 salt (盐) 生成的,这意味着您可以为相同的输入获得不同的输出。对于本例,相同密码的散列在您的情况下是不同的。你可以在 David Wong (Manning, 2020)的《真实世界密码学》第2章中找到关于哈希函数的更多细节和精彩讨论:。

我们有一个用户,所以让我们通过调用 /user/auth 端点来为该用户生成一个 OTP。下面的代码片段提供了您可以使用的 cURL 命令:

curl -XPOST -H "content-type: application/json" -d "{\"username\":\"xiaohua\",\"password\":\"12345\"}" http:/./localhost:8080/user/auth

在数据库的 otp 表中,应用程序生成并存储一个随机的四位数验证码。在我的例子中,它的值是 8527。

测试身份认证服务器的最后一步是调用 /otp/check 端点,并验证当 OTP 响应中返回一个 HTTP 200 OK 状态码,如果 OTP 错误则返回 403 Forbidden 状态码。下面的代码片段展示了正确的 OTP 值的测试,以及错误的OTP 值的测试。如果 OTP 值正确:

curl -v -XPOST -H "content-type: application/json" -d "{\"username\":\"xiaohua\",\"code\":\"8527\"}" http:/./localhost:8080/otp/check

响应状态:

...< HTTP/1.1 200...

如果OTP值错误:

curl -v -XPOST -H "content-type: application/json" -d "{\"username\":\"xiaohua\",\"code\":\"8888\"}" http:/./localhost:8080/otp/check

响应状态为:

...< HTTP/1.1 403...

我们刚刚证明了身份认证服务器组件是有效的 !现在,我们可以深入研究下一个组件——业务逻辑服务器,我们为它编写了当前实际示例的大部分 Spring Security 配置。

标签: #服务器有哪些认证