自定义用户认证逻辑
在 Security 默认的机制中,不能自定义用户名,也不能自定义密码。本节将讲解开发人员在开发中如何获取自定义的用户名,并对用户进行校验。最后,对密码的加密与解密做一些探讨。
处理用户获取逻辑
读者看到这个标题,可能不是太明白,通俗一点地说,就是让后台程序可以接收前台的新用户名,并在后台进行逻辑处理。不论是进行运算还是数据库查询操作,重要的一步是,可以使得自己控制用户。首先,看 UserDetailsService 接口的代码。
public interface UserDetailsService {
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;
}
这个接口的实现类中,可以获取 Username,最终返回 UserDetails。那么什么是 UserDetails?先看一段代码。
public interface UserDetails extends Serializable {
Collection<? extends GrantedAuthority> getAuthorities();
String getPassword();
String getUsername();
boolean isAccountNonExpired();
boolean isAccountNonLocked();
boolean isCredentialsNonExpired();
boolean isEnabled();
}
从代码中可以发现,这是关于用户名以及密码的一些方法,其实 User.java 继承了 UserDetails 接口的实现类。下面通过程序说明,代码如下所示。
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger=LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("userName:"+username);
//根据用户名,可以查找用户信息,做一些操作
return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
重新登录,如图9.4所示。

登录后,可以看到控制台上输出的日志,如图9.5所示。

在图9.5中,可以看到日志上有新的用户名,这说明,通过实现 UserDetailsService 接口,就可以获取用户登录的用户名。
同时,在类上可以添加 @Component 注解,将本类注入 Spring 容器中。而且,在本类中可以通过 @Autowired 注解注入对 Username 进行操作的 Dao 或者 Controller,进行逻辑处理。
处理用户校验逻辑
当获取到了新的用户名,并不是马上就可以使用了,因为账户是否被禁用、账户是否被锁定、密码是否过期、账户是否过期等都还没有经过校验,登录认证并没有完成。
其中,UserDetails 封装了所有登录需要的信息。UserDetails 只列举了几个方法,但是每个方法的作用还没有具体说明。在这一小节将具体说明它们的作用,并进一步说明 Security 是怎么进行用户校验的。
//返回验证用户密码,无法返回则NULL
String getPassword();
//返回验证用户名,无法返回则NULL
String getUsername();
//账户是否过期,过期无法验证
boolean isAccountNonExpired();
//指定用户是否被锁定或者解锁,锁定的用户无法进行身份验证
boolean isAccountNonLocked();
//指示是否已过期的用户的凭据(密码),过期的凭据无法进行认证
boolean isCredentialsNonExpired();
//是否被禁用,禁用的用户不能身份验证
boolean isEnabled();
UserDetails 接口的功能已经介绍完了,但如何使用?下面通过代码实例来演示。
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger=LoggerFactory.getLogger(getClass());
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("userName:"+username);
return new User(username, "123456", true, true, true, false, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
在上面的代码中,不再使用前面有 3 个参数的 User 作为返回,而是使用有 7 个参数的 User 作为返回。下面看看 User 中的 7 个参数的代码清单。
User(String username, String password, boolean enabled,
boolean accountNonExpired, boolean credentialsNonExpired,
boolean accountNonLocked, Collection<? extends GrantedAuthority>authorities)
对于每个参数,可以对应 UserDetails 中的方法来看。代码写好了,验证一下是否可以通过登录认证。图9.6中显示没有通过认证,然后我们再回去看代码就理解了。因为代码中的 accountNonLocked 参数,在返回时被赋值为 false,所以没有通过认证。

代码演示结束,相信读者对这种用法也理解了。在实际的开发中,并不是这么简单地返回。这里直接返回 true 或者 false,都是可以控制的参数,根据业务需要,引入控制器或者 Dao 进行逻辑处理,最终将结果显示出来。
密码加密与解密
这里使用的类是 PasswordEnCoder,下面是源码。
package org.springframework.security.crypto.password;
public interface PasswordEncoder {
String encode(CharSequence var1);
boolean matches(CharSequence var1, String var2);
}
encode:用于加密,建议在用户注册时,调用一次,对密码进行加密。
matches:用于检查加密的密码与用户的密码是否匹配,它是通过 Spring 调用的 matches(CharSequence rawPassword, String encodedPassword),其中 rawPassword 是原始的密码,encodedPassword 是加密的密码。
使用的加密类配置文件,代码如下所示。
package com.cao.security.browser;
/**
* 覆盖掉security原有的配置
*/
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter{
@Override
protected void configure(HttpSecurity http) throws Exception {
//表单登录的一个安全认证环境
http.formLogin()
http.httpBasic()
.and()
.authorizeRequests() //请求授权
.anyRequest() //任何请求
.authenticated(); //都需要认证
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
处理加密与解密,代码如下所示。
package com.cao.security.browser;
@Component
public class MyUserDetailsService implements UserDetailsService {
private Logger logger=LoggerFactory.getLogger(getClass());
//做一次加密
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
logger.info("登录用户userName:"+username);
//根据用户名,可以查找用户信息,做一些操作
/**
* 简单地返回用户
* User(username, password, authorities),这个User实现了UserDetails
* return new User(username, "123456", AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
*/
/*
* 这里涉及更多的校验,需要使用更加复杂的User
* new User(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, authorities)
*/
String password=passwordEncoder.encode("123456");
logger.info("模拟的数据库密码password:"+password);
return new User(username, password, true, true, true, true, AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
最终的执行结果如图9.7所示。

每次用户登录的时候,即使被加密后的密码都不一样,但是解密后仍会是同一个密码。