• Spring Security 的注册登录流程


    Spring Security 的注册登录流程

    数据库字段设计

    主要数据库字段要有:

    • 用户的 ID

    • 用户名称

    • 联系电话

    • 登录密码(非明文)

    UserDTO对象

      需要一个数据传输对象来将所有注册信息发送到我们的 Spring Boot 后端,该DTO对象应该要拥有所有我们以后创建User对象的所有字段内容:

    public class UserDto {
         private String userName;
       
         private String password;

         private String phone;
       
         // standard getters and setters
    }

    用户注册控制器

      登录页面上的“注册”链接会将用户带到注册页面。该页面的后端位于注册控制器中,并映射到 “/user/registration”,或者你可以使用 PostMan 来发送注册请求到后端,方便测试后端内容。

       @PostMapping("/user/registration")
       public String register(@RequestBody UserDTO userDTO) {
             if (userService.saveUserInfo(userDTO)) {
                  logger.info("用户注册成功");
                  return "注册成功";
              } else {
                  logger.error("用户注册失败");
                  return "注册失败";
              }
         }

      application/json 这个 Content-Type 作为响应头大家肯定不陌生。实际上,现在越来越多的人把它作为请求头,用来告诉服务端消息主体是序列化后的 JSON 字符串。

      当控制器收到请求 “/user/registration” 时,它将创建新的UserDTO对象,该对象将获取请求头Content-Type: application/json的输入流内容,在json_decode 成对象。

    定义相关字段验证

      需要使用正则表达式来验证注册的手机号是不是中国的号码以及各式是不是正确,其中一条正则表达式为:

          private static final String MOBILE_CM_AREA_REX = "^(13[0-9]{9}$|14[0-9]{9}|15[0-9]{9}$|17[0-9]{9}$|18[0-9]{9})$";

      正则表达式的编译

          private static final Pattern MOBILE_CM_AREA_REX_PATTERN = Pattern
              .compile(MOBILE_CM_AREA_REX);

      调用 Pattern 对象的 matcher 方法来获得一个 Matcher 对象,对输入字符串进行解释和匹配操作

          public static boolean isMobileCM(String mobile) {
             return MOBILE_CM_AREA_REX_PATTERN.matcher(mobile).matches();
       }

      还可以定义其他的验证,比如说用户名、密码格式之类的。

    注册前检查账号是否存在

      验证数据库中不存在该电子邮件帐户, 这是在验证表单之后执行的,也是在UserService的实现的帮助下完成。

      public boolean checkAccountByPhone(UserDTO userDTO) {
        boolean flags;
         ... // check account from database or other ways
         if (检查出账户已存在存在) {
                throw new UserAlreadyExistException(
                   "There is an account with that email address: "
                   +  userDTO.getPhone());
          }
           return flags;
      }

    保留注册数据并完成表单处理

      在控制器层中实现注册逻辑,成功后通知前端或者Postman注册结果。

    加载安全性登录的用户详细信息

      之前讨论的登录验证时使用的是硬编码凭据。让我们进行更改,并使用新注册的用户信息和凭据。我们将实现一个自定义UserDetailsService,以检查从持久性层登录的凭据。

    @Service
    @Transactional
    public class MyUserDetailsService implements UserDetailsService {
     
       @Autowired
       private UserRepository userRepository;
       
       @Override
       public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
           UserDTO userDTO = userRepository.selectOneByUsername(username);
           if (userDTO == null) {
               logger.warn("用户" + username + "不存在");
               throw new UsernameNotFoundException("用户" + username + "不存在");
          }
         
      userDTO.setAuthorities(AuthorityUtils.commaSeparatedStringToAuthorityList(userDTO.getRoles()));
           return userDTO;
      }
    }

      loadUserByUsername这个函数返回的是一个完全填充的用户记录UserDetail对象,为用户加载信息的最常见方法。UserDetails将用于构建Authentication存储在中的对象SecurityContextHolder。那这个函数什么时候被调用呢?(参考1参考2参考3

    1. 它通常由AuthenticationProvider实例调用,以认证用户。例如,提交用户名和密码后,将UserdetailsService被调用来查找该用户的密码以查看其是否正确。通常,它还将提供有关用户的其他信息,例如权限和你可能希望为已登录用户(例如电子邮件)访问的任何自定义字段。那是主要的使用模式。关于UserDetailsService经常会有一些困惑。它纯粹是用于用户数据的DAO层,除了将数据提供给框架内的其他组件外,不执行其他功能。特别是,它不对用户进行身份验证,这由AuthenticationManager完成。在许多情况下,如果您需要自定义身份验证过程,则直接实现AuthenticationProvider更有意义。

    2. 用户通过身份验证后,会将SecurityContext实例存储在会话中。根据应用程序的类型,可能需要制定一种策略来存储用户操作之间的SecurityContext。在典型的Web应用程序中,用户登录一次,然后通过其会话ID进行标识。服务器缓存持续时间会话的主体信息。在Spring Security中,请求之间存储SecurityContext 的责任落在SecurityContextPersistenceFilter,默认情况下,HTTP请求之间将上下文存储为HttpSession的属性。

    3. 如果你需要实现自定义UserDetailsService,则将取决于您的要求及其存储方式。通常,你将在与其他用户信息同时加载它们。你可能不会在过滤器中执行此操作。如上述参考手册中的引用所述,如果你·实际上要实现其他身份验证机制,则应直接实现AuthenticationProvider。你的应用程序中没有是强制性的要有UserDetailsService,可以将其视为某些内置功能使用的策略。

    启用新的身份验证提供程序

      为了能够在 Spring Security 配置新的用户服务,我们只需要添加一个引用到一个UserDetailsService内部认证管理元素,并添加了一个UserDetailsService的bean:

    @Autowired
    private MyUserDetailsService userDetailsService;

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

    添加对用户认证的自定义AuthenticationProvider

    @Autowired
       private AuthenticationProvider provider;

    @Override
       protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.authenticationProvider(provider).userDetailsService(vbUserDetailService);
      }

    使用BCrypt加密算法

      注册过程的关键部分- 密码编码 -基本上不以明文形式存储密码。

      在配置中将简单的BCryptPasswordEncoder定义为bean开始。

      @Bean
       public PasswordEncoder passwordEncoder() {
           return PasswordEncoderFactories.createDelegatingPasswordEncoder();
      }

      以下PasswordEncoderFactories会默认使用BCryptPasswordEncoder编码

    public static PasswordEncoder createDelegatingPasswordEncoder() {
      String encodingId = "bcrypt";
      Map<String, PasswordEncoder> encoders = new HashMap<>();
      encoders.put(encodingId, new BCryptPasswordEncoder());
      encoders.put("ldap", new org.springframework.security.crypto.password.LdapShaPasswordEncoder());
      encoders.put("MD4", new org.springframework.security.crypto.password.Md4PasswordEncoder());
      encoders.put("MD5", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("MD5"));
      encoders.put("noop", org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance());
      encoders.put("pbkdf2", new Pbkdf2PasswordEncoder());
      encoders.put("scrypt", new SCryptPasswordEncoder());
      encoders.put("SHA-1", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-1"));
      encoders.put("SHA-256", new org.springframework.security.crypto.password.MessageDigestPasswordEncoder("SHA-256"));
      encoders.put("sha256", new org.springframework.security.crypto.password.StandardPasswordEncoder());
      encoders.put("argon2", new Argon2PasswordEncoder());

      return new DelegatingPasswordEncoder(encodingId, encoders);
    }

      BCrypt 会在内部生成随机盐。因为这意味着每个调用都会有不同的结果,因此我们只需要对密码进行一次编码。注意,即使是相同密码明文,两次调用编码后得到的结果也不是一样的。

      BCrypt 把密码编码后通常长这样子:

      {bcrypt}$2a$10$BQ2AivawsVvTmnkzETQ6s.OAcHuafwsCJ9e6x0ScHybWlY7Xh1QlC

      BCrypt算法会生成长度为60的字符串,因此我们需要确保密码将存储在可以容纳该密码的列中。一个常见的错误是创建不同长度的列,然后在身份验证时收到“ 无效的用户名或密码”错误。

      注册时进行编码:

      user.setPassword(passwordEncoder.encode(userDTO.getPassword()));

      BCrypt算法会生成长度为60的字符串,因此我们需要确保密码将存储在可以容纳该密码的列中。一个常见的错误是创建不同长度的列,然后在身份验证时收到“ 无效的用户名或密码”错误。

      把密码编码器加入身份验证配置中

       @Override
       protected void configure(AuthenticationManagerBuilder auth) throws Exception {
           auth.authenticationProvider(provider).userDetailsService(vbUserDetailService)
                  .passwordEncoder(passwordEncoder());
      }

      那么自定义用户身份验证中,如何对前端返回的密码与数据库中的密码进行核验。把明文密码进行编码来.equals()?不是这样,前面说过,每个调用编码器都会有不同的结果,即使是对相同的明文密码。可以使用passwordEncoder 里面的matches方法来判断, 毕竟每次加密相同密码存进数据库的都不一样的。

          String encodePwd = passwordEncoder.encode(password); // 这里仅仅是为了调试的时候验证每次BCrypt编码器用的是随机盐
          String dbPwd = userInfo.getPassword();
          if (!passwordEncoder.matches(password, dbPwd)) {
               logger.warn("密码不正确");
               throw new BadCredentialsException("密码不正确");
          }

      这个matches方法会先对前端传来的进行相同方式加密的密码进行判空,然后检查是不是对应的编码格式。然后才对前端传来密码串和数据库中的密码串进行核对,检查明文密码是否与数据哈希密码匹配。具体一点就是说,matches的工作是先检查dbPwd的格式,使用的编码器类型,然后再由DelegatingPasswordEncoder转发给对用类型的BCryptPasswordEncoder来处理,它提取了数据库中的先前密码hash过的值中,取出当时hash所用的盐,然后再把password和这个盐进行编码,返回通过一样的盐hash出的字符串,最后在进行简单数组对比。matches方法返回true,表明匹配成功;反之,匹配失败,抛出认证失败异常。

    public boolean matches(CharSequence rawPassword, String encodedPassword) {
      if (encodedPassword == null || encodedPassword.length() == 0) {
        logger.warn("Empty encoded password");
        return false;
      }

      if (!BCRYPT_PATTERN.matcher(encodedPassword).matches()) {
        logger.warn("Encoded password does not look like BCrypt");
        return false;
      }

      return BCrypt.checkpw(rawPassword.toString(), encodedPassword);
    }

    上面代码中相关变量的举例说明:

    前端传来的密码password

    123

    前端编码后matches方法外的encodePwd

    {bcrypt}$2a$10$0wPZ/Gth9qcB6ALJ6XYMs.TffeGBkn/a7EJz0C9IGIVQRzfcek81i

    数据库中先前编码好的密码hash串dbPwd

    {bcrypt}$2a$10$BQ2AivawsVvTmnkzETQ6s.OAcHuafwsCJ9e6x0ScHybWlY7Xh1QlC

    (推测以上hash密码加粗部分为盐,盐的位置是有规律的)

  • 相关阅读:
    linux驱动摸索 --驱动框架初始化(结合韦东山视频教程)
    LWIP的移植笔记
    linux命令大全
    Linux中断(interrupt)子系统之一:中断系统基本原理
    Linux内核中的jiffies及其作用介绍及jiffies等相关函数详解
    Linux下PCI设备驱动程序开发 --- PCI驱动程序实现(三)
    VC++定义全局变量及extern用法
    [转]VS 2012环境下使用MFC进行OpenGL编程
    [转]在C++中容易出现的#error No Target Architecture
    实例详解:MFC坐标轴实现
  • 原文地址:https://www.cnblogs.com/magic-sea/p/12831278.html
Copyright © 2020-2023  润新知