书接上回,之前写了vue3整合SpringSecurity实现登录认证。现在,接着之前写的那两个项目实现权限校验。
我本来是想着登录认证和权限校验放在一篇文章里的,但是上次写登录认证就写了非常多了,实在是有些写不动了,所以才分为了两篇文章。
本文适合有一定基础的人来看,如果你对springsecurity安全框架还不是很了解,建议你先去看一下我之前写过的spring security框架的快速入门:
springboot3整合SpringSecurity实现登录校验与权限认证(万字超详细讲解)_springboot3 springsecurity-CSDN博客
技术栈版本:vue3.3.11、springboot3.1.5、spring security6.x
之前的登录认证文章:
前后端分离,使用vue3整合SpringSecurity加JWT实现登录认证_springsecurity整合vue3-CSDN博客
在上次的文章中,只写到登录成功和退出之后就不写了,这次会加上权限校验。
首先,在原来数据库的基础上再新建:角色表、权限表、用户角色表、角色权限表四张表:
2、角色表
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
3、权限表
CREATE TABLE permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(50) NOT NULL UNIQUE,
description VARCHAR(255),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
4、用户角色表
CREATE TABLE user_roles (
id INT AUTO_INCREMENT PRIMARY KEY,
user_id INT NOT NULL,
role_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (role_id) REFERENCES roles(id)
);
5、角色权限表
CREATE TABLE role_permissions (
id INT AUTO_INCREMENT PRIMARY KEY,
role_id INT NOT NULL,
permission_id INT NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (role_id) REFERENCES roles(id),
FOREIGN KEY (permission_id) REFERENCES permissions(id)
);
现在,我们的数据库中共有5张表,分别创建相应的server、mapper和controller层。
接下来,再原来的登录认证的代码的基础上就可以来实现我们的权限校验了;
权限校验这方面主要体现在后端代码上,所以前端我只是进行一些简单的演示即可;
1、在我们的MyTUserDetail类中定义角色和权限的属性集合,并添加到UserDetails类的getAuthorities方法中(角色和权限我都使用Set定义,这样能够去重)
代码如下:
@Data
public class MyUserDetail implements Serializable, UserDetails {
private static final long serialVersionUID = 1L;
private Users Users;
// 角色
private Set<String> roles;
// 权限
private Set<String> permissions;
@JsonIgnore //json忽略
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
List<GrantedAuthority> list = new ArrayList<>();
// 如果角色不用空,则将角色添加到list中
if (!ObjectUtils.isEmpty(roles)){
roles.forEach(role->list.add(new SimpleGrantedAuthority(role)));
}
// 如果权限不用空,则将权限添加到list中
if (!ObjectUtils.isEmpty(permissions)){
permissions.forEach(permission->list.add(new SimpleGrantedAuthority(permission)));
}
return list;
}
@JsonIgnore
@Override
public String getPassword() {
return this.getUsers().getPassword();
}
@JsonIgnore
@Override
public String getUsername() {
return this.getUsers().getUsername();
}
@JsonIgnore
@Override
public boolean isAccountNonExpired() {
return this.getUsers().getStatus()==0;
}
@JsonIgnore
@Override
public boolean isAccountNonLocked() {
return this.getUsers().getStatus()==0;
}
@JsonIgnore
@Override
public boolean isCredentialsNonExpired() {
return this.getUsers().getStatus()==0;
}
@JsonIgnore
@Override
public boolean isEnabled() {
return this.getUsers().getStatus()==0;
}
}
2、在MyUserDetailServerImpl类的loadUserByUsername方法中查出登录用户的权限集合:
代码如下:
@Service
@Slf4j
public class MyUserDetailServerImpl implements MyUserDetailServer {
@Autowired
UsersMapper userService;
/**
* 返回一个账号所拥有的权限码集合
*/
// 角色权限表
@Autowired
IRolePermissionsService rolePermissionsService;
// 用户角色表
@Autowired
IUserRolesService userRolesService;
//权限表
@Autowired
IPermissionsService permissionsService;
// 角色表
@Autowired
IRolesService rolesService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
Users users = userService.selectOne(new LambdaQueryWrapper<Users>().
eq(username != null, Users::getUsername, username));
if (users == null) {
throw new UsernameNotFoundException("用户名不存在");
}
log.info("UserDetailServer中的user:=========>"+users);
MyUserDetail myTUserDetail=new MyUserDetail();
myTUserDetail.setUsers(users);
// 查询用户权限
// 根据用户id从用户角色表中获取角色id
List<UserRoles> roleIds = userRolesService.list(new LambdaQueryWrapper<UserRoles>()
.eq(UserRoles::getUserId,users.getId()));
List<Integer> rolesList = roleIds.stream().map(UserRoles::getRoleId).toList();
if (!(roleIds.size() >0)){
// 用户没有分配角色
return myTUserDetail;
}
Set<String> listPermission = new HashSet<>();
rolesList.forEach(roleId ->{
// 根据角色id从角色权限表中获取权限id
List<RolePermissions> rolePermissions = rolePermissionsService.list(new LambdaQueryWrapper<RolePermissions>().
eq(RolePermissions::getRoleId, roleId));
// 根据权限id从权限表中获取权限名称
rolePermissions.forEach(permissionsId->{
Permissions permissions = permissionsService.getById(permissionsId.getPermissionId());
listPermission.add(permissions.getName());
});
});
myTUserDetail.setPermissions( listPermission);
// 查询角色角色
Set<String> listRole = new HashSet<>();
roleIds.forEach(roleId ->{
Roles byId = rolesService.getById(roleId.getRoleId());
listRole.add(byId.getName());
});
myTUserDetail.setRoles(listRole);
log.info("UserDetailServer中的查完权限的myTUserDetail:=========>"+myTUserDetail);
return myTUserDetail;
}
}
我所实现的是标准的RBAC(基于用户、角色、权限的访问控制模型)。所以,在得到用户id的情况下、先根据用户角色表查出角色id(如果角色id的集合为空,说明用户没有分配任何角色,直接返回用户信息)、在根据角色权限表查询权限id,在根据权限表查出具体权限名称。
上面使用了Mybatis-plus的条件构造器和stream流的形式进行查询。
3、在JwtAuthenticationTokenFilter拦截器中,在查询到用户信息时,将用户的标识和用户拥有的权限一起放到SecurityContextHolder中,这样后面的过滤器在获取到用户信息的同时也能获取到用户所拥有的权限;
代码如下:
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisTemplate<String,String> redisTemplate;
@Autowired
private JwtUtil jwtUtil;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取请求头中的token
String token = request.getHeader("token");
System.out.println("前端的token信息=======>"+token);
//如果token为空直接放行,由于用户信息没有存放在SecurityContextHolder.getContext()中所以后面的过滤器依旧认证失败符合要求
if(!StringUtils.hasText(token)){
filterChain.doFilter(request,response);
return;
}
// 解析Jwt中的用户id
Integer userId = jwtUtil.getUsernameFromToken(token);
//从redis中获取用户信息
String redisUser = redisTemplate.opsForValue().get(String.valueOf(userId));
if(!StringUtils.hasText(redisUser)){
filterChain.doFilter(request,response);
return;
}
MyUserDetail myTUserDetail= JSON.parseObject(redisUser, MyUserDetail.class);
log.info("Jwt过滤器中MyUserDetail的值============>"+myTUserDetail.toString());
//将用户信息存放在SecurityContextHolder.getContext(),后面的过滤器就可以获得用户信息了。这表明当前这个用户是登录过的,后续的拦截器就不用再拦截了
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken=new UsernamePasswordAuthenticationToken(myTUserDetail,null,myTUserDetail.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken);
filterChain.doFilter(request,response);
}
}
在这里解释一下UsernamePasswordAuthenticationToken类:
UsernamePasswordAuthenticationToken
是Spring Security中用于表示基于用户名和密码的身份验证令牌的类。它主要有以下两个构造方法:
-
UsernamePasswordAuthenticationToken(Object principal, Object credentials)
principal
参数表示认证主体,通常是用户名或用户对象。在身份验证过程中,这通常是用来标识用户的信息,可以是用户名、邮箱等。credentials
参数表示凭据,通常是用户的密码或其他凭证信息。在身份验证过程中,这用于验证用户的身份。
-
UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities)
- 除了上述两个参数外,这个构造方法还接受一个授权权限集合(
authorities
参数)。这个集合表示用户所拥有的权限,通常是一个包含用户权限信息的集合。 GrantedAuthority
接口代表了用户的权限信息,可以通过该接口的实现类来表示用户具体的权限。
- 除了上述两个参数外,这个构造方法还接受一个授权权限集合(
这两个构造方法的作用是创建一个包含用户身份信息、凭据信息和权限信息的身份验证令牌,以便在Spring Security中进行身份验证和授权操作。通过这些构造方法,可以将用户的相关信息封装成一个完整的身份验证对象,方便在安全框架中进行处理和验证。
总之,UsernamePasswordAuthenticationToken
是在Spring Security中用于表示用户名密码身份验证信息的重要类,通过不同的构造方法可以满足不同场景下的需求
所以我们通过myTUserDetail.getAuthorities()方法完全可以将用户拥有的权限方法Security容器中,并供后续的拦截器获取用户信息和权限;
4、运行测试:
接下来我编写一个基于方法的权限校验,看我们编写的代码是否生效;
(基于方法的权限认证要在SecurityConfig类上加上@EnableMethodSecurity注解,表示开启了方法权限的使用;)
新建一个TestController,并在这个类中定义一个方法,用来测试:
@RestController
@RequestMapping("/test")
public class TestController {
@PreAuthorize("hasAnyAuthority('所有权限')")
@GetMapping("/hello")
public Result hello(){
System.out.println("test接口中的hello方法调用========================");
return Result.successData("hello");
}
}
在前端的Layout.vue页面中新增一个按钮,并绑定指定的方法用来测试;
代码如图:
const testHello = async() => {
let data:any= await api.get("/test/hello")
if(data.code===200){
ElMessage('有权限')
}
else{
ElMessage.error('没有权限')
}
}
现在,我们来测试看看这个方法能不能被调用到:
可以看到这个方法被正确的访问到了,这是必须的因为这个”张乔“用户有这个权限,那么我们改一下所需的权限看还能不能访问到;
点击前端按钮:
可以看到确实不能访问到了,这说明我们的代码是正确的;
我们权限校验的逻辑是:直接在登录时查询用户的权限,并放在我们自定义的实现了UserDetail的接口类中(MyUserDetail),用来表示登录用户的全部信息;
至此:我们前后端分离,使用vue3整合SpringSecurity实现登录认证和权限校验就已经全部的讲解完毕了,我还是会将前后端的源码放在码云上,有需要的童靴可以自行的下载:
码云地址:
Vue-Security: 前后端分离的Security
有什么疑问可以在评论区说,我看到了会回复的