Spring 50例常见错误(十三)

文章整理来源:Spring编程常见错误50例_spring_spring编程_bean_AOP_SpringCloud_SpringWeb_测试_事务_Data-极客时间

案例33:使用 Spring Security 没有自定义密码解析器

        在使用 Spring Security 时,忘记定义一个 PasswordEncoder,虽然程序能够启动成功,但在发送 http://localhost:8080/admin 请求,就会报错 java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"

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

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

         解析:系统会默认创建 DelegatingPasswordEncoder() 里面虽然包含了多个 PasswordEncoder ,而在 DelegatingPasswordEncoder 执行 matches() 方法中会根据 config 里所设置的密码的前缀提取对应的 PasswordEncoder ,但由于所设置的密码没有前缀,因此采用默认的 UnmappedIdPasswordEncoder 进行密码的匹配,而其匹配必会抛出异常

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
   PasswordEncoder delegate = this.idToPasswordEncoder.get(id);
   if (delegate == null) {
      // 没有前缀  则使用默认的 PasswordEncoder
      return this.defaultPasswordEncoderForMatches
         .matches(rawPassword, prefixEncodedPassword);
   }
   String encodedPassword = extractEncodedPassword(prefixEncodedPassword);

   return delegate.matches(rawPassword, encodedPassword);
}

---------------------------------------------------------

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 + "\"");
   }
}

        解决:1. 在 config 中设置密码时给定前缀,使用指定的 PasswordEncoder ,

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

                 2. 自定义一个 PasswordEncoder Bean

案例34:ROLE_ 前缀与角色

        定义多个角色权限,但用户 admin1 可以登录,而 admin2 设置了同样的角色却不可以登陆

@Configuration
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public PasswordEncoder passwordEncoder() {
      //同上案例,这里省略掉
    }

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

        解析:admin1 的添加最后对 Role 的处理,有前缀 ROLE_

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

        admin2 的角色设置,最终设置的方法其实就是 User#withUserDetails,没有前缀 ROLE_

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

        在校验的时候是通过 UsernamePasswordAuthenticationFilter 进行的,在判断角色时,则通过 UsernamePasswordAuthenticationToken 的父类方法 AbstractAuthenticationToken#getAuthorities 来获取用户角色。而判断是否具备某个角色时,使用的关键方法是 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;
}

        解决:在添加 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";
    }
    //省略其他非关键代码
})

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值