一、权限系统的作用
例如一个学校图书馆的管理系统,如果是普通学生登录就能看到借书还书相关的功能,不可能让他看到并且去使用添加书籍信息,删除书籍信息等功能。但是如果是一个图书馆管理员的账号登录了,应该就能看到并使用添加书籍信息,删除书籍信息等功能。
总结起来就是不同的用户可以使用不同的功能。这就是权限系统要去实现的效果。
我们不能只依赖前端去判断用户的权限来选择显示哪些菜单哪些按钮。因为如果只是这样,如果有人知道了对应功能的接口地址就可以不通过前端,直接去发送请求来实现相关功能操作。
所以我们还需要在后台进行用户权限的判断,判断当前用户是否有相应的权限,必须具有所需权限才能进行相应的操作。
二、授权基本流程
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在
FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的
权限信息。判断当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
然后设置我们的资源所需要的权限即可。
三、授权基本实现
3.1限制访问资源所需权限
SpringSecurity为我们提供了基于注解的权限控制方案,这也是我们项目中主要采用的方式。我们可以使用注解去指定访问对应的资源所需的权限。
但是要使用它我们需要先开启相关配置。
1.开启配置:
(com.sangeng.config.SecurityConfig)
@EnableGlobalMethodSecurity(prePostEnabled = true)
2.然后就可以使用对应的注解。@PreAuthorize
(com.sangeng.controller.HelloController)
@RequestMapping("hello")
@PreAuthorize("hasAuthority('user')")
public String hello(){
return "hello world";
}
3.2封装权限信息
我们前面在写UserDetailsServiceImpl的时候说过,在查询出用户后还要获取对应的权限信息,封装到UserDetails中返回。
我们先直接把权限信息写死封装到UserDetails中进行测试。
我们之前定义了UserDetails的实现类LoginUser,想要让其能封装权限信息就要对其进行修改
(com.sangeng.domain.LoginUser)
@Data
@NoArgsConstructor
public class LoginUser implements UserDetails {
private User user;//封装用户信息
private List<String> permissions;//存储权限信息
public LoginUser(User user, List<String> list) {
this.user = user;
this.permissions = list;
}
//获取权限
@JSONField(serialize = false) //忽略
private List<SimpleGrantedAuthority> authorities;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if (authorities != null){
return authorities;
}
把permissions中字符串类型的权限信息转换成GrantedAuthority对象存入authorities
authorities = permissions.stream()
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return authorities;
}
//获取密码
@Override
public String getPassword() {
return user.getPassword();
}
//获取用户名
@Override
public String getUsername() {
return user.getUserName();
}
//账户是否未过期
@Override
public boolean isAccountNonExpired() {
return true;
}
//账户是否未锁定
@Override
public boolean isAccountNonLocked() {
return true;
}
//密码是否未过期
@Override
public boolean isCredentialsNonExpired() {
return true;
}
//账户是否可用
@Override
public boolean isEnabled() {
return true;
}
}
LoginUser修改完后我们就可以在UserDetailsServiceImpl中去把权限信息封装到LoginUser中了。我们写死权限进行测试,后面我们再从数据库中查询权限信息。
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
//如果没有该用户就抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO: 查询权限信息封装到LoginUser中
ArrayList<String> list = new ArrayList<>();
list.add("user");
// 将用户信息封装到UserDetails实现类中
return new LoginUser(user,list);
}
}
在SpringSecurity中,会使用默认的FilterSecurityInterceptor来进行权限校验。在
FilterSecurityInterceptor中会从SecurityContextHolder获取其中的Authentication,然后获取其中的
权限信息。当前用户是否拥有访问当前资源所需的权限。
所以我们在项目中只需要把当前登录用户的权限信息也存入Authentication。
@Component
//OncePerRequestFilter特点是在处理单个HTTP请求时确保过滤器的 doFilterInternal 方法只被调用一次
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//1.在请求头中获取token
String token = request.getHeader("token");
//此处需要判断token是否为空
if (!StringUtils.hasText(token)){
//没有token放行 此时的SecurityContextHolder没有用户信息 会被后面的过滤器拦截
filterChain.doFilter(request,response);
return;
}
//2.解析token获取用户id
String subject;
try {
Claims claims = JwtUtil.parseJWT(token);
subject = claims.getSubject();
} catch (Exception e) {
//解析失败
throw new RuntimeException("token非法");
}
//3.在redis中获取用户信息 注意:redis中的key是login:+userId
String redisKey = "login:" + subject;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
//此处需要判断loginUser是否为空
if (Objects.isNull(loginUser)){
throw new RuntimeException("用户未登录");
}
//4.将获取到的用户信息存入SecurityContextHolder 参数(用户信息,,权限信息)
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
//5.放行
filterChain.doFilter(request,response);
}
}
测试 如下:
四、从数据库查询权限信息
4.1RBAC权限模型
RBAC权限模型(Role-Based Access Control)即:基于角色的权限控制。这是目前最常被开发者使用也是相对易用、通用权限模型。
4.2准备工作
4.2.1建表语句:
CREATE TABLE `sys_menu` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '菜单ID',
`menu_name` varchar(50) NOT NULL COMMENT '菜单名称',
`parent_id` bigint DEFAULT '0' COMMENT '父菜单ID',
`order_num` int DEFAULT '0' COMMENT '显示顺序',
`path` varchar(200) DEFAULT '' COMMENT '路由地址',
`component` varchar(255) DEFAULT NULL COMMENT '组件路径',
`is_frame` int DEFAULT '1' COMMENT '是否为外链(0是 1否)',
`menu_type` char(1) DEFAULT '' COMMENT '菜单类型(M目录 C菜单 F按钮)',
`visible` char(1) DEFAULT '0' COMMENT '菜单状态(0显示 1隐藏)',
`status` char(1) DEFAULT '0' COMMENT '菜单状态(0正常 1停用)',
`perms` varchar(100) DEFAULT NULL COMMENT '权限标识',
`icon` varchar(100) DEFAULT '#' COMMENT '菜单图标',
`create_by` bigint DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT '' COMMENT '备注',
`del_flag` char(1) DEFAULT '0',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2034 DEFAULT CHARSET=utf8mb3 COMMENT='菜单权限表'
CREATE TABLE `sys_role` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '角色ID',
`role_name` varchar(30) NOT NULL COMMENT '角色名称',
`role_key` varchar(100) NOT NULL COMMENT '角色权限字符串',
`role_sort` int NOT NULL COMMENT '显示顺序',
`status` char(1) NOT NULL COMMENT '角色状态(0正常 1停用)',
`del_flag` char(1) DEFAULT '0' COMMENT '删除标志(0代表存在 1代表删除)',
`create_by` bigint DEFAULT NULL COMMENT '创建者',
`create_time` datetime DEFAULT NULL COMMENT '创建时间',
`update_by` bigint DEFAULT NULL COMMENT '更新者',
`update_time` datetime DEFAULT NULL COMMENT '更新时间',
`remark` varchar(500) DEFAULT NULL COMMENT '备注',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=13 DEFAULT CHARSET=utf8mb3 COMMENT='角色信息表'
CREATE TABLE `sys_role_menu` (
`role_id` bigint NOT NULL COMMENT '角色ID',
`menu_id` bigint NOT NULL COMMENT '菜单ID',
PRIMARY KEY (`role_id`,`menu_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='角色和菜单关联表'
CREATE TABLE `sys_user_role` (
`user_id` bigint NOT NULL COMMENT '用户ID',
`role_id` bigint NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb3 COMMENT='用户和角色关联表'
4.2.2实体类
(com.sangeng.domain)
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("sys_menu")
public class Menu implements Serializable {
private static final long serialVersionUID = 1L;
/**
* 菜单ID
*/
@TableId(value = "id", type = IdType.AUTO)
private Long id;
/**
* 菜单名称
*/
private String menuName;
/**
* 父菜单ID
*/
private Long parentId;
/**
* 显示顺序
*/
private Integer orderNum;
/**
* 路由地址
*/
private String path;
/**
* 组件路径
*/
private String component;
/**
* 是否为外链(0是 1否)
*/
private Integer isFrame;
/**
* 菜单类型(M目录 C菜单 F按钮)
*/
private String menuType;
/**
* 菜单状态(0显示 1隐藏)
*/
private String visible;
/**
* 菜单状态(0正常 1停用)
*/
private String status;
/**
* 权限标识
*/
private String perms;
/**
* 菜单图标
*/
private String icon;
/**
* 创建者
*/
private Long createBy;
/**
* 创建时间
*/
private LocalDateTime createTime;
/**
* 更新者
*/
private Long updateBy;
/**
* 更新时间
*/
private LocalDateTime updateTime;
/**
* 备注
*/
private String remark;
private String delFlag;
}
4.2.3代码实现
(com.sangeng.mapper)
public interface MenuMapper extends BaseMapper<Menu> {
List<String> selectPermsByUserId (Long userId);
}
(classpath:mapper)
<?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.example.securitytest.mapper.MenuMapper">
<select id="selectPermsByUserId" resultType="java.lang.String">
select
distinct perms
from sys_user_role sur
left join sys_role sr on sur.role_id = sr.id
left join sys_role_menu srm on sur.role_id = srm.role_id
left join sys_menu sm on srm.menu_id = sm.id
where user_id = #{userId} and sr.status = 0 and sm.status = 0
</select>
</mapper>
然后我们可以在UserDetailsServiceImpl中去调用该mapper的方法查询权限信息封装到LoginUser对象中即可
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
UserMapper userMapper;
@Autowired
MenuMapper menuMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 根据用户名查询用户信息
LambdaQueryWrapper wrapper = new LambdaQueryWrapper<User>().eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
//如果没有该用户就抛出异常
if (Objects.isNull(user)) {
throw new RuntimeException("用户名或密码错误");
}
//TODO: 查询权限信息封装到LoginUser中
// ArrayList<String> list = new ArrayList<>();
// list.add("user");
List<String> list = menuMapper.selectPermsByUserId(user.getId());
// 将用户信息封装到UserDetails实现类中
return new LoginUser(user,list);
}
}
注意🍪此处需要注意修改HelloController中的权限 之前写的是@PreAuthorize("hasAuthority('user')") 要改为数据库中对应有的权限