百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 热门文章 > 正文

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

bigegpt 2024-09-27 00:34 3 浏览

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

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

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


图 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 应用程序数据库有两个表。在第一个表中,应用程序存储用户凭证,而在第二个表中,应用程序存储生成的 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/spring
spring.datasource.username=root
spring.datasource.password=
spring.datasource.initialization-mode=always

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

练习

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

通过使用对称密钥

使用非对称密钥对

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

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

@Configuration
public 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 实体

@Entity
public class User {

  @Id
  private String username;
  private String password;

  // Omitted getters and setters
}

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

清单 3 Otp 实体

@Entity
public 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
@Transactional
public class UserService {

  @Autowired
  private PasswordEncoder passwordEncoder;

  @Autowired
  private UserRepository userRepository;

  @Autowired
  private OtpRepository otpRepository;

}

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

清单 7 addUser() 方法的定义

@Service
@Transactional
public class UserService {

  // Omitted code

  public void addUser(User user) {
    user.setPassword(passwordEncoder.encode(user.getPassword()));
    userRepository.save(user);
  }
}

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

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

@Service
@Transactional
public 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
@Transactional
public 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 类的定义

@RestController
public 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);
    }
  }
}

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

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

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

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

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

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

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

注意

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

我们有一个用户,所以让我们通过调用 /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 配置。



相关推荐

有些人能留在你的心里,但不能留在你生活里。

有时候,你必须要明白,有些人能留在你的心里,但不能留在你生活里。Sometimes,youhavetorealize,Somepeoplecanstayinyourheart,...

Python学不会来打我(34)python函数爬取百度图片_附源码

随着人工智能和大数据的发展,图像数据的获取变得越来越重要。作为Python初学者,掌握如何从网页中抓取图片并保存到本地是一项非常实用的技能。本文将手把手教你使用Python函数编写一个简单的百度图片...

软网推荐:图像变变变 一“软”见分晓

当我们仅需要改变一些图片的分辨率、裁减尺寸、添加水印、标注文本、更改图片颜色,或将一种图片转换为另一种格式时,总比较讨厌使用一些大型的图像处理软件,尤其是当尚未安装此类软件时,更是如此。实际上,只需一...

首款WP8.1图片搜索应用,搜照片得资料

首款WP8.1图片搜索应用,搜照片得资料出处:IT之家原创(天际)2014-11-1114:32:15评论WP之家报道,《反向图片搜索》(ReverseImageSearch)是Window...

分享一组美图(图片来自头条)(头条美女头像)

...

盗墓笔记电视剧精美海报 盗墓笔记电视剧全集高清种子下载

出身“老九门”世家的吴邪,因身为考古学家的父母在某次保护国家文物行动时被国外盗墓团伙杀害,吴家为保护吴邪安全将他送去德国读书,因而吴邪对“考古”事业有着与生俱来的兴趣。在一次护宝过程中他偶然获得一张...

微软调整Win11 24H2装机策略:6月起36款预装应用改为完整版

IT之家7月16日消息,微软公司今天(7月16日)发布公告,表示自今年6月更新开始,已默认更新Windows1124H2和WindowsServer2025系统中预装...

谷歌手把手教你成为谣言终结者 | 域外

刺猬公社出品,必属原创,严禁转载。合作事宜,请联系微信号:yunlugongby贾宸琰编译、整理11月23日,由谷歌新闻实验室(GoogleNewsLab)联合Bellingcat、DigD...

NAS 部署网盘资源搜索神器:全网资源一键搜,免费看剧听歌超爽!

还在为找不到想看的电影、电视剧、音乐而烦恼?还在各个网盘之间来回切换,浪费大量时间?今天就教你如何在NAS上部署aipan-netdisk-search,一款强大的网盘资源搜索神器,让你全网资源...

使用 Docker Compose 简化 INFINI Console 与 Easysearch 环境搭建

前言回顾在上一篇文章《搭建持久化的INFINIConsole与Easysearch容器环境》中,我们详细介绍了如何使用基础的dockerrun命令,手动启动和配置INFINICon...

为庆祝杜特尔特到访,这个国家宣布全国放假?

(观察者网讯)近日,一篇流传甚广的脸书推文称,为庆祝杜特尔特去年访问印度,印度宣布全国放假,并举办了街头集会以示欢迎。菲媒对此做出澄清,这则消息其实是“假新闻”。据《菲律宾世界日报》2日报道,该贴子...

一课译词:毛骨悚然(毛骨悚然的意思是?)

PhotobyMoosePhotosfromPexels“毛骨悚然”,汉语成语,意思是毛发竖起,脊梁骨发冷;形容恐惧惊骇的样子(withone'shairstandingonend...

Bing Overtakes Google in China&#39;s PC Search Market, Fueled by AI and Microsoft Ecosystem

ScreenshotofBingChinahomepageTMTPOST--Inastunningturnintheglobalsearchenginerace,Mic...

找图不求人!6个以图搜图的识图网站推荐

【本文由小黑盒作者@crystalz于03月08日发布,转载请标明出处!】前言以图搜图,专业说法叫“反向图片搜索引擎”,是专门用来搜索相似图片、原始图片或图片来源的方法。常用来寻找现有图片的原始发布出...

浏览器功能和“油管”有什么关联?为什么要下载

现在有没有一款插件可以实现全部的功能,同时占用又小呢,主题主要是网站的一个外观,而且插件则主要是实现wordpress网站的一些功能,它不仅仅可以定制网站的外观,还可以实现很多插件的功能,搭载chro...