前言
上节教程幽络源完成了项目中后端的JWT认证过滤器的配置与测试,通过JWT的过滤器,能够实现通过解析请求头中的JWT,来判断用户是否是属于这个系统的,解析不成功或者说请求头不带有token的请求都会被进行拦截,不让其访问接口。
本节概述
本章教程将在认证的基础上,完成权限的校验,所谓权限的校验,幽络源土拨鼠这里的理解就是,判断一个用户是否具备 模块、按钮、接口 的操作权限,若用户没有这些操作权限则不允许用户去访问。这个功能可以说在99%的系统中都应当是具备的。
启用全局方法安全注解
在多租户模块tenant的启动类上加上注解@EnableGlobalMethodSecurity(prePostEnabled = true),这是 Spring Security 对方法级安全控制的总开关注解,如图

修改测试接口
在之前我们创建了一个test接口来测试了认证,现在我们对这个接口加上Security提供的注解,来让这个接口实现必须通过权限校验才能访问,注解很简单,加上@PreAuthorize(“hasAuthority(‘company:manage’)”),其作用为只有当当前用户拥有权限标识 company:manage时,才允许调用该方法。否则抛出 AccessDeniedException异常。代码与图如下:
@GetMapping("/test")
@PreAuthorize("hasAuthority('company:manage')")
public String test(){
return "test";
}

修改UserDetails的实现对象
代码与图如下,可以看到相较于上节的认证教程,我们对这个UserDetailObject类做了两处增加和修改,一个是我们自定义了一个属性 List<String> permissions,这个属性主要是用于我们在认证时把查询出的用户拥有的所有权限存放到这里面。
但是对于Security来说,他是不认识这个 List<String> permissions的,他必须用自己指定的GrantedAuthority集合去进行权限的判断,因此就有了第二处修改,也就是重写他的getAuthorities方法,这个重写也容易理解,说白了就是将我们的permissions字符串集合转化为Security所需要的集合类型然后返回即可。
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserDetailObject implements UserDetails {
//用户信息
private User user;
//用户的权限信息-字符串形式
private List<String> permissions;
/**
* 实际Security需要通过这个方法来获取用户的权限信息,因此我们需要将将我们的字符串数组权限信息封装为Security所需要的权限集合
* @return 权限信息
*/
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
ArrayList<GrantedAuthority> grantedAuthorities = new ArrayList<>();
for (String permission : permissions) {
SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(permission);
grantedAuthorities.add(simpleGrantedAuthority);
}
return grantedAuthorities;
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
/**
* 是否不过期
* @return
*/
@Override
public boolean isAccountNonExpired() {
return true;
}
/**
* 是否不锁用户
* @return
*/
@Override
public boolean isAccountNonLocked() {
return true;
}
/**
* 凭着是否不过期
* @return
*/
@Override
public boolean isCredentialsNonExpired() {
return true;
}
/**
* 账户是否可用
* @return
*/
@Override
public boolean isEnabled() {
return true;
}
}

查询用户权限编写
上面我们为UserDetailObject对象添加了存放权限的字符串集合,现在就来开发查询用户权限的方法,并且在自定义认证处将查询出的权限也交给UserDetails对象。
涉及到数据库的查询了,因此这里我们要来开发相应的mapper,因为要查询的是权限,在PermissionMapper中添加抽象方法->根据用户ID查询拥有的权限字符串集合,代码与图如下
@Mapper
public interface PermissionMapper extends BaseMapper<Permission> {
/**
* 根据用户id查询对应的权限字符串
* @param userId 用户id
* @return
*/
List<String> selectPermissionByUserId(@Param("userId") Long userId);
}

然后在resource目录下建立mapper目录,对应PermissionMapper创建名为PermissionMapper.xml的文件,然后写该查询的具体的SQL,代码与图如下
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.tenant.mapper.PermissionMapper">
<!-- List<String> selectPermissionByUserId(@Param("userId") Integer userId); -->
<select id="selectPermissionByUserId" resultType="String">
select p.code
from tbs_user u
left join tbs_user_role ur on ur.user_id = u.id
left join tbs_role r on r.id = ur.role_id
left join tbs_role_permission rp on rp.role_id = r.id
left join tbs_permission p on p.id = rp.permission_id
where u.id=#{userId}
</select>
</mapper>

修改自定义认证
查询用户的权限方法有了,现在来修改自定义认证处的方法,将用户的权限查询出来让后存放到UserDetails中,代码与图如下
@Service
public class UserDetailServiceImpl implements UserDetailsService {
@Resource
private UserMapper userMapper;
@Resource
private PermissionMapper permissionMapper;
/**
* 实现自定义认证
* authenticationManager.authenticate内部在调用这里
* @param username
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户新消息
LambdaQueryWrapper<User> eq = new LambdaQueryWrapper<User>()
.eq(User::getUsername, username);
User user = userMapper.selectOne(eq);
if (!Objects.isNull(user)){
List<String> permissions = permissionMapper.selectPermissionByUserId(user.getId());
UserDetailObject userDetailObject = new UserDetailObject(user,permissions);
return userDetailObject;
}else{
throw new UsernameNotFoundException("无此用户");
}
}
}

修改登录方法
在登录方法中会调用认证,认证完成后会得到一个Authentication对象,此时由于我们已经修改了自定义认证,这里的Authentication对象便会包含用户权限信息,我们这里将Authentication对象中的权限信息一并生成到JWT中相应给前端,后续前端携带此JWT请求其他接口时,会通过我们的JWT过滤器来获取到其JWT中的权限信息,并将权限信息交给Security上下文。
如图,我们的登录方法中修改为如下

修改Token过滤器
你也可以称之为JWT过滤器,如图可以看到这里修改的其实不多,本质就是将前端传来的JWT解析,然后将权限信息封装为Security所需的GrantedAuthority集合,并将此集合赋值给Security的上下文对象之一UsernamePasswordAuthenticationToken。

过程搞清楚
修改了那么多,那这个执行流程是怎样的呢?Security本身的源码这里就不看了,否则太多了,我们就只搞清楚我们自己的代码来明白整个流程。
登录的过程
搞简单点,如图
当发起登录请求时,首先来到Controller的/auth/login接口
login接口接着调用LoginServiceImpl的登录方法login()
在login方法中,当执行到认证时,也就是authenticationManager.authenticate,便会来到我们自定义的认证方法loadUserByUsername,在自定义的方法中,我们查询了用户的信息和用户的权限进行返回,Security通过一些列的操作用Authentication接收到了,然后我们通过authenticate获取到里面的用户信息和权限信息,进一步通过JWT工具类将其存入JWT中响应给了前端。

认证搞清楚与测试
首先登录获取到JWT,如图,这里我首先用user1来测试认证,然后将JWT放到请求test接口的请求头中,如图可以看到这里响应了403,也就是被拒绝了,因为user1在数据库中他的角色是普通用户,并且不具备“company:manage”的权限。


再来测试超管用户,将超管进行登录,然后将响应的jwt放到请求中去请求test接口,如图可以看到超管用户是能够成功请求test接口的,至此权限的初步校验功能幽络源已完成。


本章源码链接
https://pan.quark.cn/s/24ec0b8ecee3
结语
如上为幽络源的7、幽络源微服务项目实战:SpringSecurity用户权限查询与校验的配置和测试教程,如有疑问或对微服务感兴趣可加入我们的QQ群询问与交流:307531422

