目录
概述
上一篇大致总结了有关spring security安全框架中所需要的几种过滤器。让我们对于spring security框架有了一定的认识。结合我们日常工作中的开发,还是需要实际的代码来演示一下框架的搭建。和之前shiro 框架整合spring一样,这次同样也是归纳总结下一个最小spring security系统的整合搭建。首先是引用依赖,整合spring security只需要在原来spring web的基础上再增加一个依赖,我使用的是2.3.2.RELEASE版本
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
总体结构
总体的组件图如上,看起来会比shiro的要简单一点,基本上配置都是统一的写在一个配置类中,再配合上一个我们自定义的账号实体类和实现账号认证功能的服务类这样一个security的大致雏形就具备了。
账号实体
首先我们先回顾一下之前提到的在spring security框架中Authentication的概念。单词的意思是认证,在框架中它主要有三个组件来组成,分别是
- principal
- credentials
- authorities
第一个你可以认为值账号主体,它主要就是在系统中来存储账号的主要信息,比如账号名之类的,在security中提供了UserDetails这样一个接口来做统一的标准载体,我们要做的就是让自己的实体类去实现这个接口。
第二个账号的身份信息凭证,类似于我们人的身份证,在系统中通常指的是密码。
第三个是账号的权限,指的是你在我们的系统中可以做些什么事情。
我们先在java工程中创建一个实体类并且实现UserDetails接口,这里我使用的是spring data jpa中的正向工程,可以将Java代码中的实体类映射成数据库中的表结构。
@Data
@Entity
@Table(name = "tb_security_user")
@org.hibernate.annotations.Table(appliesTo = "tb_security_user",comment = "spring_security用户表")
@EntityListeners(AuditingEntityListener.class)
public class SecurityUserPO implements UserDetails,Serializable {
@Override
public String getPassword() {
return userPassword;
}
@Override
public String getUsername() {
return userName;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return !locked;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
主要会实现接口中的这几个方法,对应的其实是系统中账号的几个属性,实际开发中其实并不是都会用到。
- getPassword,这个很简单就是返回我们实体类中的密码成员变量。
- getUsername,返回我们的账号名,什么是账号名,就是系统登录的时候用户输入的那个账号名,这个需要在系统中唯一。
- isAccountNonExpired,账号是否过期,比如某些系统中账号生成后存在有效期的,那就可以用到这个属性。
- isAccountNonLocked,账号是否被锁定,比如有些系统中密码输错一定次数,会冻结锁定账号,那么这个时候就可以用到这个属性。
- isCredentialsNonExpired,账号的凭证是否过期,我的理解是,有些时候账号的密码需要定期的去修改,那么需要一个属性去通知用户。
- isEnabled,账号是否可用,对于一些恶意的账号管理员可以禁用它们,阻止它们登录系统。
这里我只用到了获取账号密码和判断账号是否锁定。
另外比较重要的就是这个getAuthorities方法,它返回的是一个系统定义的权限集合的对象。和shiro中不同的是,shiro是将角色和权限分别保存在两个变量属性中,而在spring security中对于角色和权限的概念似乎没有那么的明确和清晰,它们是统一保存在一个变量属性中,那么是如何判断这个字符串是标记角色还是权限的呢?框架的默认角色字符前缀是ROLE_(不要漏掉这个下划线),这个默认前缀来源于实现SPEL表达式的SecurityExpressionRoot
public abstract class SecurityExpressionRoot implements SecurityExpressionOperations {
protected final Authentication authentication;
private AuthenticationTrustResolver trustResolver;
private RoleHierarchy roleHierarchy;
private Set<String> roles;
private String defaultRolePrefix = "ROLE_";
public final boolean permitAll = true;
public final boolean denyAll = false;
下面接着说获取账号所有权限集的这个方法,
@Transient
private Set<RolePO> allRoles;
@Transient
private Set<PermissionPO> allPermissions;
private static final String ROLE_PREFIX = "ROLE_";
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
allRoles.forEach(rolePO ->
grantedAuthorityList.add(new SimpleGrantedAuthority(ROLE_PREFIX + rolePO.getRoleName())));
allPermissions.forEach(permissionPO ->
grantedAuthorityList.add(new SimpleGrantedAuthority(permissionPO.getPermissionName())));
return grantedAuthorityList;
}
首先我们需要定义两个变量分别代表角色和权限,由于使用的式jpa,需要避免在运行更新数据库的表结构的时候将这个两个变量属性同步更新到数据库中。所以需要将上这个注解标识这个变量属性不需要持久化 。接着将所有的角色拼接上系统默认的前缀,封装成一个SimpleGrantedAuthority对象,由于这个接口本身返回的集合对象是所有继承GrantedAuthority的子类,所以集合可以直接接收这个SimpleGrantedAuthority对象。到此UserDetails实体的配置就完成了。
UserDetailService实现
和shiro类似在shiro中我们会创建一个自定义的realm用重写认证和授权的方法,在spring security中我们需要实现UserDetailService接口,这个接口中只有一个方法loadUserByUsername,通过账号名称载入账号信息。一般我们都是重写这个方法,所以我们创建一个类来实现它
@Slf4j
public class SelfUserDetailService implements UserDetailsService {
@Autowired
private UserDao userDao;
@Override
public UserDetails loadUserByUsername(String userName) throw UsernameNotFoundException {
SecurityUserPO userPO = userDao.queryUserByName(userName);
if (null == userPO){
log.warn("not found user:[{}]",userName);
throw new UsernameNotFoundException("账号不存在");
}
if (!userPO.isAccountNonLocked()){
log.warn("user has been locked :[{}]",userName);
throw new LockedException("账号被锁定");
}
Set<RolePO> rolePOSet = new HashSet<>(userDao.getAllRoles(userPO.getUserId()));
userPO.setAllRoles(rolePOSet);
Set<PermissionPO> permissionPOSet = new HashSet<>(userDao.getAllPermission(rolePOSet.stream()
.map(RolePO::getRoleId).collect(Collectors.toList())));
userPO.setAllPermissions(permissionPOSet);
Collection<? extends GrantedAuthority> authorities = userPO.getAuthorities();
return new User(userPO.getUsername(),userPO.getPassword(),authorities);
}
}
注意和shiro不同的是,spring security中不需要分成认证和授权两个方法,shiro的策略是将认证和授权分成两个步骤,首先对用户进行认证,之后当用户需要访问具体的资源的时候再调用这个授权的方法。而spring security是使用了之前提到的账号主体的概念,将账号的主体信息,凭证,权限集合封装在一起,然后在整套系统内使用。
在这个方法中首先通过这个账号名查询这个记录,然后可以按照自己的具体需求对这个记录做判断,不符合的记录抛出对应的异常。获取到合法的对象之后就可以获取到他的权限集合了,通过数据库查询分别获取到角色和权限,封装到上文提到的在账号实体中所增加的两个角色和权限两个字段,然后就可以调用之前getAuthorities方法获取封装号的权限集合,最后将账号名,密码,权限集合封装成spring security中的User对象返回。
小结
到此,spring security 的账号主体就设计好了,由于配置类当中还有许多的东西,放在一起整理未免篇幅过长,所以准备放在下一篇中一起整理。