Spring Security 的注册登录流程

Spring Security 的注册登录流程

数据库字段设计

主要数据库字段要有:文章来源地址https://www.yii666.com/article/332454.html网址:yii666.com

  • 用户的 ID

  • 用户名称

  • 联系电话

  • 登录密码(非明文)文章来源地址:https://www.yii666.com/article/332454.html

UserDTO对象

  需要一个数据传输对象来将所有注册信息发送到我们的 Spring Boot 后端,该DTO对象应该要拥有所有我们以后创建User对象的所有字段内容:文章地址https://www.yii666.com/article/332454.html

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注册结果。网址:yii666.com<

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

  之前讨论的登录验证时使用的是硬编码凭据。让我们进行更改,并使用新注册的用户信息和凭据。我们将实现一个自定义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密码加粗部分为盐,盐的位置是有规律的)

版权声明:本文内容来源于网络,版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。文本页已经标记具体来源原文地址,请点击原文查看来源网址,站内文章以及资源内容站长不承诺其正确性,如侵犯了您的权益,请联系站长如有侵权请联系站长,将立刻删除

Spring Security 的注册登录流程-相关文章

  1. BCrypt加密算法

    用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。BCrypt算

  2. bcrypt 加密算法

    MD5 的特性MD5 是一种加密算法,在调用这个算法的时候,提供一个密码的明文, 调用的结果,得到一个 32 位长度的密文;MD5 算法的特性:相同的字符串,如果多次调用 md5 算法,得到的结果,完全一样;MD5 算法,无法被逆向解密;但是,基于 md5 算法的第二个特性,我们可以

  3. 密码学系列之:bcrypt加密算法详解

    目录简介bcrypt的工作原理bcrypt算法实现bcrypt hash的结构hash的历史简介今天要给大家介绍的一种加密算法叫做bcrypt, bcrypt是由Niels Provos和David Mazières设计的密码哈希函数,他是基于Blowfish密码而来的,并于1999年在USENIX上提出。除了加盐来抵御rainbow table 攻击之外,bcrypt的一个非常

  4. Bcrypt加密算法简介

    用户表的密码通常使用MD5等不可逆算法加密后存储,为防止彩虹表破解更会先使用一个特定的字符串(如域名)加密,然后再使用一个随机的salt(盐值)加密。 特定字符串是程序代码中固定的,salt是每个密码单独随机,一般给用户表加一个字段单独存储,比较麻烦。 BCry

  5. phpass类加密算法

    客户说用md5加密容易被破解,要求使用bcrypt加密,经搜索发现password_hash函数可以轻松对密码实现加盐加密,比md5更安全,缺点是运行慢。phpass是一个开源类库,它可以让我们更方便使用bcrypt加密算法。下载地址:http://www.openwall.com/phpass/使用:// 引入类文件require \\\'PasswordHash.p

  6. [Node.js] 3、搭建hexo博客

    目录一、安装新版本的nodejs和npm二、安装hexo三、安装hexo-admin并配置四、nginx配置五、增加tag六、后台启动七、体验一、安装新版本的nodejs和npm安装n模块:升级node.js到最新稳定版二、安装hexonote: 参考github,不要去其官网: https://github.com/hexojs/hexo安装HexoSetup your blog安装Cactus主

  7. PHP版本对比【转】

    其他历史http://www.cnblogs.com/yjf512/p/3588466.htmlphp5.3改动:1、realpath() 现在是完全与平台无关的. 结果是非法的相对路径比如FILE. \\\"/../x\\\" 将不会工作.2、call_user_func() 系列函数即使被调用者是一个父类也使用 $this.3、数组函数 natsort(), natcasesort(), usort(), uasort(), uksort(), array_flip(), 和 ar

  8. spring security使用哈希加密的密码

    之前我们都是使用MD5 Md5PasswordEncoder 或者SHA ShaPasswordEncoder 的哈希算法进行密码加密,在spring security中依然使用只要指定使用自定义加密算法就行,现在推荐spring使用的BCrypt BCryptPasswordEncoder,一种基于随机生成salt的根据强大的哈希加密算法。首先我们使用spring提供的加密方

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

微信图片_20190322181744_03.jpg

微信扫一扫打赏

请作者喝杯咖啡吧~

支付宝扫一扫领取红包,优惠每天领

二维码1

zhifubaohongbao.png

二维码2

zhifubaohongbao2.png