首页 > 其他分享 >Spring Security 常见错误

Spring Security 常见错误

时间:2023-05-26 11:11:05浏览次数:56  
标签:return String 错误 ADMIN Spring PasswordEncoder new Security public

案例 1:遗忘 PasswordEncoder

当我们第一次尝试使用 Spring Security 时,我们经常会忘记定义一个 PasswordEncoder。因为这在 Spring Security 旧版本中是允许的。而一旦使用了新版本,则必须要提供一个 PasswordEncoder。这里我们可以先写一个反例来感受下:

https://www.java567.com,搜"spring")

首先我们在 Spring Boot 项目中直接开启 Spring Security:

 <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
 </dependency>
 ​

添加完这段依赖后,Spring Security 就已经生效了。然后我们配置下安全策略,如下:

 @Configuration
 public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
 //
 //   @Bean
 //   public PasswordEncoder passwordEncoder() {
 //       return new PasswordEncoder() {
 //           @Override
 //           public String encode(CharSequence charSequence) {
 //               return charSequence.toString();
 //           }
 //
 //           @Override
 //           public boolean matches(CharSequence charSequence, String //           s) {
 //               return Objects.equals(charSequence.toString(), s);
 //           }
 //       };
 //   }
 ​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("admin").password("pass").roles("ADMIN");
 ​
    }
 ​
    // 配置 URL 对应的访问权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/admin/**").hasRole("ADMIN")
                .anyRequest().authenticated()
                .and()
                .formLogin().loginProcessingUrl("/login").permitAll()
                .and().csrf().disable();
    }
 }
 ​

这里,我们故意“注释”掉 PasswordEncoder 类型 Bean 的定义。然后我们定义一个 SpringApplication 启动程序来启动服务,我们会发现启动成功了:

INFO 8628 --- [ restartedMain] c.s.p.web.security.example1.Application : Started Application in 3.637 seconds (JVM running for 4.499)

但是当我们发送一个请求时(例如 http://localhost:8080/admin ),就会报错java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null",具体错误堆栈信息如下:

 

所以,如果我们不按照最新版本的 Spring Security 教程操作,就很容易忘记 PasswordEncoder 这件事。那么为什么缺少它就会报错,它的作用又在哪?接下来我们具体解析下。

案例解析

我们可以反思下,为什么需要一个 PasswordEncoder。实际上,这是安全保护的范畴。

假设我们没有这样的一个东西,那么当用户输入登录密码之后,我们如何判断密码和内存或数据库中存储的密码是否一致呢?假设就是简单比较下是否相等,那么必然要求存储起来的密码是非加密的,这样其实就存在密码泄露的风险了。

反过来思考,为了安全,我们一般都会将密码加密存储起来。那么当用户输入密码时,我们就不是简单的字符串比较了。我们需要根据存储密码的加密算法来比较用户输入的密码和存储的密码是否一致。所以我们需要一个 PasswordEncoder 来满足这个需求。这就是为什么我们需要自定义一个 PasswordEncoder 的原因。

再看下它的两个关键方法 encode() 和 matches(),相信你就能理解它们的作用了。

思考下,假设我们默认提供一个出来并集成到 Spring Security 里面去,那么很可能隐藏错误,所以还是强制要求起来比较合适。

我们再从源码上看下 "no PasswordEncoder" 异常是如何被抛出的?当我们不指定PasswordEncoder去启动我们的案例程序时,我们实际指定了一个默认的PasswordEncoder,这点我们可以从构造器DaoAuthenticationProvider看出来:

 public DaoAuthenticationProvider() {
 setPasswordEncoder(PasswordEncoderFactories.createDelegatingPasswordEncoder());
 }
 ​

我们可以看下PasswordEncoderFactories.createDelegatingPasswordEncoder()的实现:

 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);
 }
 ​

我们可以换一个视角来看下这个DelegatingPasswordEncoder长什么样:

 

通过上图可以看出,其实它是多个内置的 PasswordEncoder 集成在了一起。

当我们校验用户时,我们会通过下面的代码来匹配,参考DelegatingPasswordEncoder#matches:

 private PasswordEncoder defaultPasswordEncoderForMatches = new UnmappedIdPasswordEncoder();
 ​
 @Override
 public boolean matches(CharSequence rawPassword, String prefixEncodedPassword) {
    if (rawPassword == null && prefixEncodedPassword == null) {
      return true;
    }
    String id = extractId(prefixEncodedPassword);
    PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
    if (delegate == null) {
      return this.defaultPasswordEncoderForMatches
          .matches(rawPassword, prefixEncodedPassword);
    }
    String encodedPassword = extractEncodedPassword(prefixEncodedPassword);
 ​
    return delegate.matches(rawPassword, encodedPassword);
 }
 ​
 private String extractId(String prefixEncodedPassword) {
    if (prefixEncodedPassword == null) {
      return null;
    }
    //{
    int start = prefixEncodedPassword.indexOf(PREFIX);
    if (start != 0) {
      return null;
    }
    //}
    int end = prefixEncodedPassword.indexOf(SUFFIX, start);
    if (end < 0) {
      return null;
    }
    return prefixEncodedPassword.substring(start + 1, end);
 }
 ​

可以看出,假设我们的 prefixEncodedPassword 中含有 id,则根据 id 到 DelegatingPasswordEncoder 的 idToPasswordEncoder 找出合适的 Encoder;假设没有 id,则使用默认的UnmappedIdPasswordEncoder。我们来看下它的实现:

 private class UnmappedIdPasswordEncoder implements PasswordEncoder {
 ​
    @Override
    public String encode(CharSequence rawPassword) {
      throw new UnsupportedOperationException("encode is not supported");
    }
 ​
    @Override
    public boolean matches(CharSequence rawPassword,
      String prefixEncodedPassword) {
      String id = extractId(prefixEncodedPassword);
      throw new IllegalArgumentException("There is no PasswordEncoder mapped for the id \"" + id + "\"");
    }
 }
 ​

从上述代码可以看出,no PasswordEncoder for the id "null" 异常就是这样被 UnmappedIdPasswordEncoder 抛出的。那么这个可能含有 id 的 prefixEncodedPassword 是什么?其实它就是存储的密码,在我们的案例中由下面代码行中的 password() 指定:

 auth.inMemoryAuthentication()        .withUser("admin").password("pass").roles("ADMIN");
 ​

这里我们不妨测试下,修改下上述代码行,给密码指定一个加密方式,看看之前的异常还存在与否:

 auth.inMemoryAuthentication()        .withUser("admin").password("{MD5}pass").roles("ADMIN");
 ​

此时,以调试方式运行程序,你会发现,这个时候已经有了 id,且取出了合适的 PasswordEncoder。

 

说到这里,相信你已经知道问题的来龙去脉了。问题的根源还是在于我们需要一个PasswordEncoder,而当前案例没有给我们指定出来。

问题修正

那么通过分析,你肯定知道如何解决这个问题了,无非就是自定义一个 PasswordEncoder。具体修正代码你可以参考之前给出的代码,这里不再重复贴出。

另外,通过案例解析,相信你也想到了另外一种解决问题的方式,就是在存储的密码上做文章。具体到我们案例,可以采用下面的修正方式:

 auth.inMemoryAuthentication()        .withUser("admin").password("{noop}pass").roles("ADMIN");
 ​

然后定位到这个方式,实际上就等于指定 PasswordEncoder 为NoOpPasswordEncoder了,它的实现如下:

 public final class NoOpPasswordEncoder implements PasswordEncoder {
 ​
    public String encode(CharSequence rawPassword) {
      return rawPassword.toString();
    }
 ​
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
      return rawPassword.toString().equals(encodedPassword);
    }
 ​
  //省略部分非关键代码
 ​
 }
 ​

不过,这种修正方式比较麻烦,毕竟每个密码都加个前缀也不合适。所以综合比较来看,还是第一种修正方式更普适。当然如果你的需求是不同的用户有不同的加密,或许这种方式也是不错的。

https://www.java567.com,搜"spring")

 

案例 2:ROLE_ 前缀与角色

我们再来看一个 Spring Security 中关于权限角色的案例,ROLE_ 前缀加还是不加?不过这里我们需要提供稍微复杂一些的功能,即模拟授权时的角色相关控制。所以我们需要完善下案例,这里我先提供一个接口,这个接口需要管理的操作权限:

 @RestController
 public class HelloWorldController {
    @RequestMapping(path = "admin", method = RequestMethod.GET)
    public String admin(){
          return "admin operation";
    };
 ​

然后我们使用 Spring Security 默认的内置授权来创建一个授权配置类:

 @Configuration
 public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {
 ​
    @Bean
    public PasswordEncoder passwordEncoder() {
      //同案例1,这里省略掉
    }
 ​
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("fujian").password("pass").roles("USER")
                .and()
                .withUser("admin1").password("pass").roles("ADMIN")
                .and()
                .withUser(new UserDetails() {
                    @Override
                    public Collection<? extends GrantedAuthority> getAuthorities() {
                        return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));
 ​
                    }
                    //省略其他非关键“实现”方法
                    public String getUsername() {
                        return "admin2";
                    }
 ​
                });
    }
 ​
    // 配置 URL 对应的访问权限
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    http.authorizeRequests()
              .antMatchers("/admin/**").hasRole("ADMIN")
              .anyRequest().authenticated()
              .and()
              .formLogin().loginProcessingUrl("/login").permitAll()
              .and().csrf().disable();
    }
 }
 ​

通过上述代码,我们添加了 3 个用户:

  1. 用户 fujian:角色为 USER

  2. 用户 admin1:角色为 ADMIN

  3. 用户 admin2:角色为 ADMIN

然后我们从浏览器访问我们的接口 http://localhost:8080/admin,使用上述 3 个用户登录,你会发现用户 admin1 可以登录,而 admin2 设置了同样的角色却不可以登陆,并且提示下面的错误:

 

如何理解这个现象?

案例解析

要了解这个案例出现的原因,其实是需要我们对 Spring 安全中的 Role 前缀有一个深入的认识。不过,在这之前,你可能想不到案例出错的罪魁祸首就是它,所以我们得先找到一些线索。

对比 admin1 和 admin2 用户的添加,你会发现,这仅仅是两种添加内置用户的风格而已。但是为什么前者可以正常工作,后者却不可以?本质就在于 Role 的设置风格,可参考下面的这两段关键代码:

 //admin1 的添加
 .withUser("admin").password("pass").roles("ADMIN")
 ​
 //admin2 的添加
 .withUser(new UserDetails() {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ADMIN"));
    }
    @Override
    public String getUsername() {
        return "admin2";
    }
    //省略其他非关键代码
 });
 ​

查看上面这两种添加方式,你会发现它们真的仅仅是两种风格而已,所以最终构建出用户的代码肯定是相同的。我们先来查看下 admin1 的添加最后对 Role 的处理(参考 User.UserBuilder#roles):

 public UserBuilder roles(String... roles) {
    List<GrantedAuthority> authorities = new ArrayList<>(
          roles.length);
    for (String role : roles) {
      Assert.isTrue(!role.startsWith("ROLE_"), () -> role
            + " cannot start with ROLE_ (it is automatically added)");
      //添加“ROLE_”前缀
      authorities.add(new SimpleGrantedAuthority("ROLE_" + role));
    }
    return authorities(authorities);
 }
 ​
 public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = new ArrayList<>(authorities);
    return this;
 }
 ​

可以看出,当 admin1 添加 ADMIN 角色时,实际添加进去的是 ROLE_ADMIN。但是我们再来看下 admin2 的角色设置,最终设置的方法其实就是 User#withUserDetails:

 public static UserBuilder withUserDetails(UserDetails userDetails) {
    return withUsername(userDetails.getUsername())
      //省略非关键代码
      .authorities(userDetails.getAuthorities())
      .credentialsExpired(!userDetails.isCredentialsNonExpired())
      .disabled(!userDetails.isEnabled());
 }
 ​
 public UserBuilder authorities(Collection<? extends GrantedAuthority> authorities) {
    this.authorities = new ArrayList<>(authorities);
    return this;
 }
 ​

所以,admin2 的添加,最终设置进的 Role 就是 ADMIN。

此时我们可以得出一个结论:通过上述两种方式设置的相同 Role(即 ADMIN),最后存储的 Role 却不相同,分别为 ROLE_ADMIN 和 ADMIN。那么为什么只有 ROLE_ADMIN 这种用户才能通过授权呢?这里我们不妨通过调试视图看下授权的调用栈,截图如下:

 

对于案例的代码,最终是通过 "UsernamePasswordAuthenticationFilter" 来完成授权的。而且从调用栈信息可以大致看出,授权的关键其实就是查找用户,然后校验权限。查找用户的方法可参考 InMemoryUserDetailsManager#loadUserByUsername,即根据用户名查找已添加的用户:

 public UserDetails loadUserByUsername(String username)
      throws UsernameNotFoundException {
    UserDetails user = users.get(username.toLowerCase());
 ​
    if (user == null) {
      throw new UsernameNotFoundException(username);
    }
 ​
    return new User(user.getUsername(), user.getPassword(), user.isEnabled(),
          user.isAccountNonExpired(), user.isCredentialsNonExpired(),
          user.isAccountNonLocked(), user.getAuthorities());
 }
 ​

完成账号是否过期、是否锁定等检查后,我们会把这个用户转化为下面的 Token(即 UsernamePasswordAuthenticationToken)供后续使用,关键信息如下:

 

最终在判断角色时,我们会通过 UsernamePasswordAuthenticationToken 的父类方法 AbstractAuthenticationToken#getAuthorities 来取到上述截图中的 ADMIN。而判断是否具备某个角色时,使用的关键方法是 SecurityExpressionRoot#hasAnyAuthorityName:

 private boolean hasAnyAuthorityName(String prefix, String... roles) {
    //通过 AbstractAuthenticationToken#getAuthorities 获取“role”
    Set<String> roleSet = getAuthoritySet();
 ​
    for (String role : roles) {
      String defaultedRole = getRoleWithDefaultPrefix(prefix, role);
      if (roleSet.contains(defaultedRole)) {
          return true;
      }
    }
 ​
    return false;
 }
 //尝试添加“prefix”,即“ROLE_”
 private static String getRoleWithDefaultPrefix(String defaultRolePrefix, String role) {
    if (role == null) {
      return role;
    }
    if (defaultRolePrefix == null || defaultRolePrefix.length() == 0) {
      return role;
    }
    if (role.startsWith(defaultRolePrefix)) {
      return role;
    }
    return defaultRolePrefix + role;
 }
 ​

在上述代码中,prefix 是 ROLE_(默认值,即 SecurityExpressionRoot#defaultRolePrefix),Roles 是待匹配的角色 ROLE_ADMIN,产生的 defaultedRole 是 ROLE_ADMIN,而我们的 role-set 是从 UsernamePasswordAuthenticationToken 中获取到 ADMIN,所以最终判断的结果是 false。

最终这个结果反映给上层来决定是否通过授权,可参考 WebExpressionVoter#vote:

 public int vote(Authentication authentication, FilterInvocation fi,
      Collection<ConfigAttribute> attributes) {
    //省略非关键代码
    return ExpressionUtils.evaluateAsBoolean(weca.getAuthorizeExpression(), ctx) ? ACCESS_GRANTED
          : ACCESS_DENIED;
 }
 ​

很明显,当是否含有某个角色(表达式 Expression:hasRole('ROLE_ADMIN'))的判断结果为 false 时,返回的结果是 ACCESS_DENIED。

问题修正

针对这个案例,有了源码的剖析,可以看出: ROLE_ 前缀在 Spring Security 前缀中非常重要。 而要解决这个问题,也非常简单,我们直接在添加 admin2 时,给角色添加上 ROLE_ 前缀即可:

 //admin2 的添加
 .withUser(new UserDetails() {
    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Arrays.asList(new SimpleGrantedAuthority("ROLE_ADMIN"));
    }
    @Override
    public String getUsername() {
        return "admin2";
    }
    //省略其他非关键代码
 })
 ​

参考上述代码,我们给 Role 添加了前缀,重新运行程序后,结果符合预期。

反思这个案例,我们可以总结出:有时候,不同的 API 提供了不同的设置 Role 的方式,但是我们一定要注意是否需要添加 ROLE_ 这个前缀。而如何判断,这里我也没有更好的办法,只能通过经验或者查看源码来核实了。

重点回顾

最后我们梳理下课程中所提及的重点。

  1. PasswordEncoder

在新版本的 Spring Security 中,你一定不要忘记指定一个PasswordEncoder,因为出于安全考虑,我们肯定是要对密码加密的。至于如何指定,其实有多种方式。常见的方式是自定义一个PasswordEncoder类型的Bean。还有一种不常见的方式是通过存储密码时加上加密方法的前缀来指定,例如密码原来是password123,指定前缀后可能是 {MD5}password123。我们可以根据需求来采取不同的解决方案。

  1. Role

在使用角色相关的授权功能时,你一定要注意这个角色是不是加了前缀 ROLE_。

虽然 Spring 在很多角色的设置上,已经尽量尝试加了前缀,但是仍然有许多接口是可以随意设置角色的。所以有时候你没意识到这个问题去随意设置的话,在授权检验时就会出现角色控制不能生效的情况。从另外一个角度看,当你的角色设置失败时,你一定要关注下是不是忘记加前缀了。

https://www.java567.com,搜"spring")

标签:return,String,错误,ADMIN,Spring,PasswordEncoder,new,Security,public
From: https://www.cnblogs.com/web-666/p/17434204.html

相关文章

  • idea显示springboot多服务启动界面service
    如果是多模块的微服务,idea提供了一个可以多服务启动的界面services,如果你的项目里没看到这个界面:那么你需要在顶级的maven工程中找到这个配置,然后找到componentname="RunDashboard"这个节点整个替换掉:<componentname="RunDashboard"><optionname="configurationTypes">......
  • SpringBoot2.0实现SpringCloud config自动刷新之坑点
    在使用rabbitmq之后并不能实现客户端的配置自动刷新,原因是我参考的资料都是springboot1.x的,Springboot2.0的改动较大,之前1.0的/bus/refresh全部整合到actuador里面了,所以之前1.x的management.security.enabled全部失效,不适用于2.0适用于2.0的配置是这样的:management:endpoin......
  • Springboot+Vue集成个人中心、修改头像、数据联动、修改密码
    源码:https://gitee.com/xqnode/pure-design/tree/master学习视频:https://www.bilibili.com/video/BV1U44y1W77D开始讲解个人信息的下拉菜单:<el-dropdownstyle="width:150px;cursor:pointer;text-align:right"><divstyle="display:inline-block">......
  • Springboot集成百度地图实现定位打卡功能
    打卡sign表sqlCREATETABLE`sign`(`id`int(11)NOTNULLAUTO_INCREMENT,`user`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULTNULLCOMMENT'用户名称',`location`varchar(255)COLLATEutf8mb4_unicode_ciDEFAULTNULLCOMMENT'打卡位置',`......
  • SpringBoot集成swagger-ui以及swagger分组显示
    文章目录1.swagger配置类2.使用swagger3.额外的学习经历大家好,这篇文章展示下如何在springboot项目中集成swagger-ui。有人说,这都是老生常谈,网上的例子数不胜数。确实swagger诞生至今已经很久了,但是在使用过程中我遇到一个问题,下面给大家分享下我的使用心得吧。1.swagger配置类第......
  • Springboot集成支付宝沙箱支付补充版本(退款功能)
    接上一次讲解:B站视频讲解:https://www.bilibili.com/video/BV1w44y1379q/这次的讲解涉及到完整的支付流程,大家请仔细查看!包括:支付宝沙箱支付+异步通知+退款功能正式版本的sdk通用版本SDK文档:https://opendocs.alipay.com/open/02np94<dependency><groupId>com.alipay.sdk......
  • Spring Web 参数验证常见错误
    案例1:对象参数校验失效在构建Web服务时,我们一般都会对一个HTTP请求的Body内容进行校验,例如我们来看这样一个案例及对应代码。当开发一个学籍管理系统时,我们会提供了一个API接口去添加学生的相关信息,其对象定义参考下面的代码:(https://www.java567.com,搜"spring") importlo......
  • spring boot框架JAVA语言实现的货运系统(司机APP端+货主APP端)
    技术架构:springboot、mybatis、redis、vue、element-ui  开发语言:java、vue、uniapp开发工具:idea、vscode、hbuilder+  前端框架:vue  后端框架:springboot  数据库:mysql  移动端:uniapp混合开发+原生插件后台管理端功能:权限设置:角色设置、人员设置......
  • 在命令提示符窗口输入docker ps提示链接错误
    一、前言  在命令行窗口执行dockerps命令的时候,报错了 二、处理方法  提示这个错误可能是dockerdaemon没有打开    发现docker服务没有打开,把docker服务打开试试    然后进入到“C:\ProgramFiles\Docker\Docker”目录下,执行DockerCli.exe-SwitchDaem......
  • Springboot+Mybatisplus+ClickHouse集成
    核心依赖引入<dependency><groupId>ru.yandex.clickhouse</groupId><artifactId>clickhouse-jdbc</artifactId><version>0.1.53</version></dependency><!--Mybati......