前后端分离,使用vue3整合SpringSecurity加JWT实现权限校验
我所实现的是标准的RBAC(基于用户、角色、权限的访问控制模型)。所以,在得到用户id的情况下、先根据用户角色表查出角色id(如果角色id的集合为空,说明用户没有分配任何角色,直接返回用户信息)、在根据角色权限表查询权限id,在根据权限表查出具体权限名称。3、在JwtAuthenticationTokenFilter拦截器中,在查询到用户信息时,将用户的标识和用户拥有的权限一起放到Security
书接上回,之前写了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("ZML_"+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;
}
}
Authentication 讨论了所有
Authentication
实现如何存储GrantedAuthority
对象的列表。这些对象代表已经授予委托人(principal)的权限。GrantedAuthority
对象由AuthenticationManager
插入到Authentication
对象中,随后由AccessDecisionManager
实例在做出授权决定时读取。
GrantedAuthority
接口只有一个方法。
String getAuthority();
这个方法被 AuthorizationManager
实例用来获取 GrantedAuthority
的一个精确的 String
表示。通过返回一个 String
表示,一个 GrantedAuthority
可以被大多数 AuthorizationManager
实现轻松 "读取"。如果 GrantedAuthority
不能被精确地表示为一个 String
,那么该 GrantedAuthority
被认为是 "复杂的",getAuthority()
必须返回 null
。
一个复杂的 GrantedAuthority
的例子是一个实现,它存储了一个适用于不同客户账号的操作和权限阈值的列表。将这种复杂的 GrantedAuthority
表示为一个 String
将是相当困难的。因此,getAuthority()
方法应该返回 null
。这向任何 AuthorizationManager
表明,它需要支持特定的 GrantedAuthority
实现来理解其内容。
Spring Security 包括一个具体的 GrantedAuthority
实现。SimpleGrantedAuthority
。这个实现允许任何用户指定的字符串被转换为 GrantedAuthority
。安全架构中包含的所有 AuthenticationProvider
实例都使用 SimpleGrantedAuthority
来填充 Authentication
对象。
默认情况下,基于角色的授权规则包括 ROLE_
作为前缀。这意味着,如果有一个授权规则要求 security context 的角色是 "USER",Spring Security 将默认寻找返回 "ROLE_USER" 的 GrantedAuthority#getAuthority
。
你可以用 GrantedAuthorityDefaults
来定制这个。GrantedAuthorityDefaults
的存在是为了允许自定义基于角色的授权规则所使用的前缀。
你可以通过暴露一个 GrantedAuthorityDefaults
Bean 来配置授权规则以使用不同的前缀,像这样:
@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() {
return new GrantedAuthorityDefaults("ZML_");
}
我们需要特别注意的一点是,在spring security中。我们的角色和权限是存储在一起的,没有分开存储 如:
参考来源:授权架构 :: Spring Security Reference
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容器中,并供后续的拦截器获取用户信息和权限;
自定义异常处理:
我们都知道spring security有两个很常见的异常。分别为未登录的401异常和没有权限的403异常,但是,在默认的spring security中这两个异常都是直接返回了一个HTTP的页面,特别丑;我们可以自定义这些异常的处理。并且返回给前端统一的数据格式;
自定义无权限403异常如下:
package com.scmpt.framework.security.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.scmpt.framework.core.web.response.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @Author 张乔
* @Date 2025/03/14 10:08
* @Description spring security自定义无权限处理器
* @Version 1.0
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
private final ObjectMapper objectMapper;
/**
* 处理无权限访问的情况。
*
* @param request HttpServletRequest 对象,包含了请求的信息
* @param response HttpServletResponse 对象,用于向客户端发送响应
* @param accessDeniedException AccessDeniedException 对象,包含了访问被拒绝的相关信息
* @throws IOException 如果在写入响应时发生I/O错误,则抛出此异常
*/
@Override
public void handle(HttpServletRequest request,
HttpServletResponse response,
AccessDeniedException accessDeniedException) throws IOException {
log.info("当前用户无权限");
// 返回json格式的错误信息
Result responseData = new Result(403,"当前用户没有权限",null);
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(responseData));
}
}
自定义登录失败401异常如下:
package com.scmpt.framework.security.config;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.scmpt.framework.core.web.response.Result;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import java.io.IOException;
/**
* @Author 张乔
* @Date 2025/03/14 10:08
* @Description spring security自定义未登录处理器
* @Version 1.0
*/
@Component
@Slf4j
@RequiredArgsConstructor
public class CustomAuthenticationEntryPoint implements AuthenticationEntryPoint {
private final ObjectMapper objectMapper;
/**
* 当用户未登录时,处理认证异常。
*
* @param request 请求对象
* @param response 响应对象
* @param authException 认证异常对象
* @throws IOException 如果写入响应时发生 I/O 错误
*/
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException {
log.info("当前用户未登录");
// 返回json格式的错误信息
Result responseData = new Result(401,"当前用户未登录",null);
response.setContentType("application/json");
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(responseData));
}
}
写好这两个异常之后,还需要注册进相应的spring security的责任链中。我们知道spring security就是通过责任链的形式来处理鉴权流程的;
在spring security的配置类中将这两个自定义异常注入进来:
private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
private final CustomAccessDeniedHandler customAccessDeniedHandler;
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http)throws Exception {
// 配置异常处理器 登录异常和鉴权异常
http.exceptionHandling(e -> e.accessDeniedHandler(customAccessDeniedHandler)
.authenticationEntryPoint(customAuthenticationEntryPoint)
);
return http.build();
}
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
有什么疑问可以在评论区说,我看到了会回复的
更多推荐
所有评论(0)