创建好项目后,直接启动,在控制台上会打印密码:
此时在浏览器输入http://localhost:8080
,会跳转到登录页面:
默认用户名为user,密码就是控制台打印的。
这就说明spring security生效了!
首先我们需要先了解,为什么会有默认的用户名和密码,这说明肯定是有一个自动配置类。
在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
可登录成功,但这并不是我们想要的结果,我们希望这个用户名密码是我们动态设置的,而不是在配置文件中写死的。
返回到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;
}
}
此时就可以测试成功啦!