SpringSecurity默认用户名密码从哪来,为什么要写UserDetails...

1、创建一个普通的Spring boot项目

image-20210909203917251

image-20210909204138086

创建好项目后,直接启动,在控制台上会打印密码:

image-20210909204503353

此时在浏览器输入http://localhost:8080,会跳转到登录页面:

默认用户名为user,密码就是控制台打印的。

image-20210909204710417

这就说明spring security生效了!

2、自定义用户名密码

首先我们需要先了解,为什么会有默认的用户名和密码,这说明肯定是有一个自动配置类。

在idea中,双击shift键,输入UserDetailsServiceAutoConfiguration我们会发现:

@Bean
@ConditionalOnMissingBean(
    type = "org.springframework.security.oauth2.client.registration.ClientRegistrationRepository")
@Lazy
// InMemoryUserDetailsManager说明此时的用户信息是保存在内存中的,下次启动密码又会变化,但是我们重点不是这个
public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
                                                             ObjectProvider<PasswordEncoder> passwordEncoder) {
    // SecurityProperties中有一个User,在下一个代码解释中,我们看看这个User到底是个啥
    SecurityProperties.User user = properties.getUser();
    List<String> roles = user.getRoles();
    return new InMemoryUserDetailsManager(
        User.withUsername(user.getName()).password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
        .roles(StringUtils.toStringArray(roles)).build());
}

private String getOrDeducePassword(SecurityProperties.User user, PasswordEncoder encoder) {
    String password = user.getPassword();
    if (user.isPasswordGenerated()) {
        // 还记得控制台打印的那个密码吗?
        // Using generated security password: 8e45224d-58c8-4776-ba43-d3808def675e
        logger.info(String.format("%n%nUsing generated security password: %s%n", user.getPassword()));
    }
    if (encoder != null || PASSWORD_ALGORITHM_PATTERN.matcher(password).matches()) {
        return password;
    }
    return NOOP_PASSWORD_PREFIX + password;
}

我们点进User看看这个User为何方神圣:

// 它是SecurityProperties的一个静态内部类
public static class User {

    /**
	 * Default user name.
	*/
    private String name = "user"; // 默认的信息

    /**
	* Password for the default user name.
	*/
    private String password = UUID.randomUUID().toString();  // 默认的密码UUID

   ......

}

目前位置我们知道了默认的账号和密码是怎么来的了,但是怎么修改呢?

我们先通过配置,因为我们知道有SecurityProperties这个配置类了,那肯定能通过配置文件进行配置

application.yml中:

spring:
  security:
    user:
      name: butcher
      password: bb123

重启项目,我们发现控制台已经没有打印密码了

重新访问http://localhost:8080

image-20210909211114575

可登录成功,但这并不是我们想要的结果,我们希望这个用户名密码是我们动态设置的,而不是在配置文件中写死的。

返回到UserDetailsServiceAutoConfiguration,在类名上有这么一段注解

@ConditionalOnMissingBean(
    value = { AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class },
    type = { "org.springframework.security.oauth2.jwt.JwtDecoder",
            "org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector" })
// 说明如果我们自定义了UserDetailsService.class这个类并将它放置到IOC容器里面,这个默认配置就会失效,当然有其他的也会

那么我们看看UserDetailsService 是个啥?

public interface UserDetailsService {

	UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

}
// 省去了注解:注解的大致内容是通过username去数据库里查出完整的用户信息,那么完整的用户信息应该就是UserDetails了

我们在我们的service 层创建一个UserDetailsService的实现类。

/**
 * 实现了这个接口,默认的用户名密码自动配置就失效啦!
 */
@Service
public class MyUserDetailsService implements UserDetailsService {

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return null;
    }
}

这时我们的问题又冒出来了,那么完整的用户信息应该包含什么呢?

我们点开UserDetails这个类

// 首先它是个接口
// 类上注解的大意为:
// 出于安全目的,Spring Security不直接使用实现。它们只存储用户信息,这些信息随后被封装到Authentication对象中。
// 这允许将非安全相关的用户信息(如电子邮件地址、电话号码等)存储在方便的位置。
public interface UserDetails extends Serializable {

	/**
	 * 返回授予用户的权限集合,不能返回null
	 */
	Collection<? extends GrantedAuthority> getAuthorities();

	/**
	 * 用户的密码
	 */
	String getPassword();

	/**
	 * 返回用户名,用户名也不能为空
	 */
	String getUsername();

	/**
	 * 用户是否过期,没有过期就返回true
	 */
	boolean isAccountNonExpired();

	/**
	 * 用户是否被锁定,锁定返回true。
	 */
	boolean isAccountNonLocked();

	/**
	 * 用户凭证是否可用,可用返回true
	 */
	boolean isCredentialsNonExpired();

	/**
	 * 用户是否启用了,启用了返回true
	 */
	boolean isEnabled();

}

既然和我们安全有关,那么我们在我们security包创建UserDetails的实现类。

public class MyUserDetails implements UserDetails {

    // 添加一些自己的属性,以便从外部设置值
    private String username;
    private String password;
    private Collection<? extends GrantedAuthority> Authorities;
    // 默认都为true 过期了咱再改,同时也方便测试
    private boolean isAccountNonExpired = true;
    private boolean isAccountNonLocked = true;
    private boolean isCredentialsNonExpired = true;
    private boolean isEnabled = true;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return this.Authorities;
    }

    @Override
    public String getPassword() {
        return this.password;
    }

    @Override
    public String getUsername() {
        return this.username;
    }

    @Override
    public boolean isAccountNonExpired() {
        return this.isAccountNonExpired;
    }

    @Override
    public boolean isAccountNonLocked() {
        return this.isAccountNonLocked;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return this.isCredentialsNonExpired;
    }

    @Override
    public boolean isEnabled() {
        return this.isEnabled;
    }

	省略了setter方法,请手动生成,或使用lombok生成
}

然后我们就可以在MyUserDetailsService中使用了!

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    // 假设这个MyUserDetails是我们从数据库中查出来的
    MyUserDetails myUserDetails = new MyUserDetails();
    myUserDetails.setUsername("tanxi");
    myUserDetails.setPassword("tx1234");
    return myUserDetails;
}

将之前我们再配置文件中配置的用户名和密码删除!

再重新运行项目!

这时候会报一个异常:There is no PasswordEncoder mapped for the id "null"

sercurity要求我们的密码一定是经过加密的,所以我们需要将密码进行加密。

我们需要一个PasswordEncoder类,双击shift查找:

public interface PasswordEncoder {

	/**
	 * 对原始密码进行编码。通常,一个好的编码算法应用SHA-1或更大的哈希值与一个8字节或更大的随机生成的salt相结合。
	 * 其中CharSequence是一个可读的字符序列,是个接口,很多类都实现了这个接口,例如String、CharArray等
	 */
	String encode(CharSequence rawPassword);

	/**
	 * 验证从存储器获得的编码密码在编码后是否与提交的原始密码匹配。如果密码匹配,则返回true;如果密码不匹配,则返回false。
	 * rawPassword是需要匹配的密码,encodedPassword是数据库中的密码
	 */
	boolean matches(CharSequence rawPassword, String encodedPassword);

	/**
	 * 如果为了更好的安全性,应再次对编码的密码进行编码,则返回true,否则返回false。默认实现总是返回false。
	 */
	default boolean upgradeEncoding(String encodedPassword) {
		return false;
	}

}

上面是一个接口,我们可以自定义加密的方式,自己实现一个加密!

// 不是必须加@Component注解,只是为了可以方便使用
@Component
public class MyPasswordEncoder implements PasswordEncoder {

    // 这是盐推荐8字节或更大的,这是我们从源码里知道的
    final String salt = "butchersoyoung";

    @Override
    public String encode(CharSequence rawPassword) {

        try {
            // 使用JDk自带的MD5加密
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            // CharSequence转为String,才能获取到字节数组,StandardCharsets.UTF_8是标准的字符集
            byte[] bytes = md5.digest((rawPassword.toString() + salt).getBytes(StandardCharsets.UTF_8));

            return new String(bytes,StandardCharsets.UTF_8);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        // rawPassword是我们要验证的原密码 ,encodedPassword这是我们加密后的数据库中的密码
        // 这个方法并不需要我们手动调用,而是由SpringSecurity来调用,我们写好规则就可以了~

        // 这里就没有做过多的验证了,只是为了说明密码是可以自己加密的,自己定匹配规则的
        if (rawPassword != null && encodedPassword != null){
            return encode(rawPassword).equals(encodedPassword);
        }else {
            return false;
        }
    }
}

注意:关于PasswordEncoder的实现类,Spring推荐我们使用BCryptPasswordEncoder,它使用了强哈希算法,怎么说都比我们自定义的加密要安全多~

自定义加密只是为了方便我们理解,原来就这么回事儿~

MyUserDetailsService中修改一下,解决我们上面遇到的密码未加密问题。

@Service
public class MyUserDetailsService implements UserDetailsService {

    @Autowired
    MyPasswordEncoder myPasswordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 假设这个MyUserDetails是我们从数据库中查出来的
        MyUserDetails myUserDetails = new MyUserDetails();
        myUserDetails.setUsername("tanxi");
        String encodePassword = myPasswordEncoder.encode("tx1234");
        myUserDetails.setPassword(encodePassword);
        return myUserDetails;
    }
}

此时就可以测试成功啦!

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值