1.引言
在企业级Java Web项目的开发中,我们少不了对访问控制部分的代码的开发,比如登录,而这部分又往往涉及到应用的安全问题,所以SpringSecurity框架就应运而生了。
Spring Security是主要解决认证(Authenticate)和授权(Authorization)的框架。它提供了一组可以在Spring应用上下文中配置的Bean,充分利用了Spring IOC,DI 技术(控制反转Inversion of Control ,DI:Dependency Injection 依赖注入)和AOP(面向切面编程)功能,为应用系统提供声明式的安全访问控制功能,减少了为企业系统安全控制编写大量重复代码的工作。我们在使用过程中,只需简单的配置,自己编写逻辑部分。就可以让框架自动去实现验证功能。
2.使用
首先,我们在maven项目的pom.xml中引入依赖spring-boot-starter-security。
!注意:以上依赖项是带有自动配置的,一旦添加此依赖,整个项目中所有的访问,默认都是必须先登录才可以访问的,在浏览器输入任何此服务的URL,都会自动跳转到默认的登录页面。
其中默认的用户名是user
,默认的密码是启动项目时自动生成的随机密码,在服务器端的控制台可以看到此密码。
当登录后,会自动跳转到此前尝试访问的页面。Spring Security默认使用Session机制保存用户的登录状态,所以,重启服务后,登录状态会消失。在不重启的情况下,可以通过 /logout
访问“退出登录”页面,确定后也可以清除登录状态。
但是我们在实际开发中,如果频繁去使用自动生成的密码,是有很多的不方便之处的,对于我们测试也造成了很多障碍,于是我们需要添加自己的测试用户名和密码到数据库中,以便测试登录。这就必须来了解下Spring Security中的BCrypt 算法工具类。
关于BCrypt
在Spring Security中,内置了BCrypt算法的工具类,此工具类可以实现使用BCrypt算法对密码进行加密、验证密码的功能。
BCrypt算法使用了随机盐,所以,多次使用相同的原文进行加密,得到的密文都将是不同的,并且,使用的盐值会作为密文的一部分,也就不会影响验证密码了。
在Spring Security框架中,定义了PasswordEncoder
接口,表示“密码编码器”,并且使用BCryptPasswordEncoder
实现了此接口。
使用步骤
1.创建配置类,并显示配置Bean方法
·说完了上面的这些,我们来看下具体的使用步骤,这里拿我们要开发管理员的新增,以及登录功能,首先在config包下建一个Security对应的配置类:
@Configuration
public class SecurityConfiguration {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
这里显示的配置Bean方法,以方便我们后续获取PasswordEncoder对象。例如在Service层中,我们通过自动依赖注入,来获取到对象。
@Autowired
private PasswordEncoder passwordEncoder;
获取到对象之后,我们就可以对管理员用户输入的密码进行加密处理,例如我们现在处理添加管理员信息的业务:
public void addNew(AdminAddNewDTO adminAddNewDTO) {
// 日志
log.debug("开始处理【添加管理员】的业务,参数:{}", adminAddNewDTO);
// 从参数中获取尝试添加的管理员的用户名
String username = adminAddNewDTO.getUsername();
// 调用adminMapper对象的countByUsername()方法进行统计
int countByUsername = adminMapper.countByUsername(username);
// 判断统计结果是否大于0
if (countByUsername > 0) {
// 是:日志,抛出ServiceException
String message = "添加管理员失败,用户名【" + username + "】已经被占用!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_CONFLICT, message);
}
// 创建新的Admin对象
Admin admin = new Admin();
// 调用BeanUtils.copyProperties()方法将参数的属性值复制到以上Admin对象中
BeanUtils.copyProperties(adminAddNewDTO, admin);
// 补全Admin对象的属性值:loginCount >>> 0
admin.setLoginCount(0);
// 对密码进行加密处理
String rawPassword = admin.getPassword();
String encodedPassword = passwordEncoder.encode(rawPassword);
admin.setPassword(encodedPassword);
// 日志
log.debug("即将插入管理员数据:{}", admin);
// 调用adminMapper对象的insert()方法插入数据,并获取返回的受影响的行数
int rows = adminMapper.insert(admin);
// 判断受影响的行数是否不等于1
if (rows != 1) {
// 是:日志,抛出ServiceException
String message = "添加管理员失败,服务器忙,请稍后再次尝试!";
log.warn(message);
throw new ServiceException(ServiceCode.ERR_INSERT, message);
}
}
我们在加密之后,调用dao层将封装的信息添加到数据库后,这段密码实际的显示是以密文形式存在的。
注意:一旦在Spring容器中已经存在PasswordEncoder
对象,Spring Security会自动使用它,所以,会导致之前控制台默认的随机密码不可用(你提交的随机密码会被加密后再进行对比,而Spring Security默认的密码并不是密文,所以对比会失败)。
很多同学会问:这个密文有什么意义呢?它可以保证除了注册用户之外的其他人无法看到真正的密码。一旦加密后,它的破解过程是十分繁琐和困难的,几乎是不可能完成的任务,其次,我们还可以在加密的算法基础上,再加盐(salt)处理(关于加盐不懂的或者感兴趣的同学可以自行百度)。所以我们在做登录验证时,其实是通过密文二者之间进行比对的,而并不是将数据库中的密文破译后比对的。
登录
2.自定义实现类进行比对验证
使用Spring Security时,应该自定义类,实现UserDetailsService
接口,在此接口中,有UserDetails loadUserByUsername(String username)
方法,Spring Security会自动使用登录时输入的用户名来调用此方法,此方法返回的结果中应该包含与用户名匹配的相关信息,例如密码等,接下来,Spring Security会自动使用自动装配的密码编码器对密码进行验证。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
log.debug("根据用户名{}从数据库查询用户信息……", s);
// 调用AdminMapper对象,根据用户名(参数值)查询管理员信息
AdminLoginInfoVO loginInfo = adminMapper.getLoginInfoByUsername(s);
// 判断是否查询到有效结果
if (loginInfo == null) {
// 根据用户名没有找到任何管理员信息
String message = "登录失败,用户名不存在!";
log.warn(message);
throw new UsernameNotFoundException(message);
}
UserDetails userDetails = User.builder()
.username(loginInfo.getUsername())
.password(loginInfo.getPassword())
.accountExpired(false) // 账号是否已经过期
.accountLocked(false) // 账号是否已经锁定
.credentialsExpired(false) // 认证是否已经过期
.disabled(false) // 是否已经禁用
.authorities("这是临时使用的且无意义的权限值") // 权限,注意,此方法的参数值不可以为null
.build();
return userDetails;
}
}
}
这里解释一下上面这段代码,UserDetails是SpringSecurity中的一个接口,被认证信息,以及认证结果都是这个UserDetails里面的对象。里面定义了一个抽象方法loadByUsername(), 另外框架中还有一个User类:是UserDetails接口的官方写的实现类,构造方法有三个参数:username,password,authorities,我们需要向spring security提供一个UserDetails对象。
!!!!!!!!!!!!!!!!这个框架中的接口和类有点多,而且名字有点长,我们可以慢慢来消化,把知识点按照自己的理解去丰满起来,而不是这么抽象的理解!!!!!!!!!!!!!!!
我们先来看一下User的相关源码,注意这个User是框架官方写的实现类,别和我们自己命名的某些实体类搞混。以助于我们理解上面代码段的意义。首先我们看下图,可以看到User就是UserDetails的一个实现类,这个UserDetails就是一个抽象出来的接口,这个接口继承了Serializable可序列化接口,所以看到User也间接实现了这个接口,有SerialVersionUId,然后这个接口里面有一些抽象方法,我们看到User里面也定义了这些对应的常量,并重写了接口中的方法,返回自己的属性。
然后我们看到User这个类中有一个builder()方法,它返回的是一个UserBuilder()的对象,这个对象中有很多方法,返回值都是UserBuilder本身,所以可以通过链式表达去打点调用方法,最后调用build()方法,返回一个UserDetails
简单的功能先介绍到这里,下面会再加一篇文章来介绍JWT