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

您所在的位置:网站首页 csn1204默认密码 SpringSecurity默认用户名密码从哪来,为什么要写UserDetails...

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

2024-02-28 04:38| 来源: 网络整理| 查看: 265

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) { // SecurityProperties中有一个User,在下一个代码解释中,我们看看这个User到底是个啥 SecurityProperties.User user = properties.getUser(); List 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 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 Authorities; // 默认都为true 过期了咱再改,同时也方便测试 private boolean isAccountNonExpired = true; private boolean isAccountNonLocked = true; private boolean isCredentialsNonExpired = true; private boolean isEnabled = true; @Override public Collection 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; } }

此时就可以测试成功啦!



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3