这个是我毕设实现的关于RBAC权限控制部分,大致的流程是用户注册登陆后,产生一个token,token通过jwt封装用户id来进行用户身份识别。添加过滤器拦截请求,从请求中获取url,判断当前url是否能被当前用户访问(权限表内会将角色能够访问的url进行存储)。
一、什么是RBAC
基于角色的访问控制(RBAC)是一种访问控制策略,用于管理系统、应用程序或网络中的用户对资源的访问权限。RBAC 将用户分配给角色,然后将权限分配给角色,而不是直接将权限分配给个别用户。这种方式简化了权限管理,特别是在大型组织或系统中,可以更轻松地管理权限。
RBAC 的主要组成部分包括:
- 角色(Roles):角色是一组具有相似职责或权限需求的用户集合。例如,一个企业应用可能有角色如“管理员”、“普通用户”、“审计员”等。
- 权限(Permissions):权限是指用户或角色被允许执行的特定操作或访问资源的能力。例如,读取、写入、删除文件或访问特定功能。
- 用户(Users):用户是系统中的实体,可以被分配到一个或多个角色。
- 角色分配(Role Assignment):将用户分配给角色的过程。一旦分配了角色,用户就继承了与该角色相关联的权限。
- 权限授予(Permission Granting):将权限授予角色的过程。这确定了哪些操作或资源可供特定角色使用。
- 访问控制(Access Control):根据用户的角色来控制对资源的访问。这包括验证用户的身份,检查其所属的角色,然后根据角色的权限来决定是否允许访问特定资源。
使用 RBAC 可以带来以下优点:
简化权限管理:通过将权限与角色相关联,管理员可以更轻松地管理大量用户的权限,而不必为每个用户单独配置权限。
降低管理成本:RBAC 可以降低权限管理的复杂性和成本,因为权限只需分配给角色,而不是给每个用户分配。
提高安全性:RBAC 可以确保用户只能访问其所需的资源,因此可以减少潜在的安全漏洞。
总的来说,基于角色的访问控制(RBAC)是一种灵活且高效的访问控制策略,广泛用于各种规模和类型的组织和系统中。
二、基于RBAC的表设计
etao_user为用户表,etao_role为角色表。多对多关系,一个用户可以拥有多种角色权限,一个角色权限对应多个用户。(中间表为etao_user_role)
etao_role为角色表,etao_permisson为权限表。多对多关系,etao_role_permisson为中间表。
三、用户信息检验流程
分两种情况,已登录用户与未登录用户
1、未登陆用户直接登陆
springsecurity检验,通过后拿到用户基本信息和生成token与刷新token。
@Override
@Transactional(rollbackFor = Exception.class)
public ResponseResult login(String userName, String password) {
//AuthenticationManager authenticate进行用户认证
UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(userName,password);
Authentication authenticate = authenticationManager.authenticate(authenticationToken);
//如果认证没通过,给出对应的提示
if(Objects.isNull(authenticate)){
throw new RuntimeException("登录失败");
}
//如果认证通过了,使用userid生成一个jwt jwt存入ResponseResult返回
LoginUser loginUser = (LoginUser) authenticate.getPrincipal();
User user = loginUser.getUser();
Long userid = user.getUserId();
// long转tostring
Short userState = user.getUserState();
log.info("用户角色"+userState);
// 15分钟accessToken, 40分钟refreshToken
String jwt = JwtUtil.createJWT(userid.toString(),JwtUtil.TOKEN_TIMEOUT);
String reJwt = JwtUtil.createJWT(userName, JwtUtil.REFRESH_TOKEN_TIMEOUT);
Map<String,Object> map = new HashMap<>();
// TODO 双重token
map.put("accessToken",jwt);
map.put("refreshToken",reJwt);
map.put("id",userid.toString());
map.put("nickName",user.getNickName());
map.put("userName",userName);
// 用户上线
int update = userMapper.update(null, new UpdateWrapper<User>().eq("user_id",userid).set("user_tag",1).set("update_data",new Date()));
String tokenKey = "etao_token"+userid;
String refreshTokenKey = "etao_refreshToken"+userName;
log.info("#JWT"+jwt);
log.info("#REJET",reJwt);
// 存入 15分钟的 token
if (redisCache.exists(tokenKey)) {
redisCache.deleteObject(tokenKey);
}
if (redisCache.exists(refreshTokenKey)) {
redisCache.deleteObject(refreshTokenKey);
}
redisCache.setCacheObject(tokenKey,jwt,JwtUtil.TOKEN_TIMEOUT, TimeUnit.MILLISECONDS);
// 存入 40 分钟的 refreshToken
redisCache.setCacheObject(refreshTokenKey,reJwt,JwtUtil.REFRESH_TOKEN_TIMEOUT,TimeUnit.MILLISECONDS);
redisCache.setCacheObject("login:"+userid,loginUser);
if (userState == 1) {
map.put("permission", loginUser.getPermissionsList());
return new ResponseResult(200,"登录成功",map);
}
return new ResponseResult(200,"登录成功",map);
}
通过Authentication 返回的用户信息,组装成一个map返回给前端。token使用用户id进行生成,刷新token使用用户名称进行生成。
Authentication 通过UserDetailsService来获取用户的详细信息(包括用户名、密码和权限)自定义实现UserDetailsService,如下:
@Service
@Slf4j
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private UserMapper userMapper;
@Autowired
private UserRoleMapper userRoleMapper;
@Autowired
private RolePermissionMapper rolePermissionMapper;
@Autowired
private PermissionMapper permissionMapper;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//查询用户信息
LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
wrapper.eq(User::getUserName, username);
User user = userMapper.selectOne(wrapper);
//如果没有查询到
if (Objects.isNull(user)) {
throw new BusinessException(USER_EXCEPTION);
}
// 未审核用户禁止登录
if (user.getUserState() == -1) {
throw new BusinessException(USER_CHECK_EXCEPTION);
}
List<Integer> array = new ArrayList<>();
List<String> arrays = new ArrayList<>();
Integer roleId = userRoleMapper.selectOne(new LambdaQueryWrapper<UserRole>().eq(UserRole::getUserId, user.getUserId())).getRoleId();
List<RolePermission> rolePermissions = rolePermissionMapper.selectList(new QueryWrapper<RolePermission>().select("perm_id").eq("role_id", roleId));
rolePermissions.forEach(e -> {
array.add(e.getPermId());
});
List<Permission> permissions = permissionMapper.selectList(new LambdaQueryWrapper<Permission>().in(Permission::getId, array));
permissions.forEach(e -> {
arrays.add(e.getPermissionUrl());
});
System.out.println(arrays);
if (user.getUserState() == 1) {
List<Permission> menuTreeRecursion = getMenuTreeRecursion(permissions);
return new LoginUser(user,arrays,menuTreeRecursion);
}
return new LoginUser(user,arrays);
}
public static List<Permission> getMenuTreeRecursion(List<Permission> permissions){
/**
* 过滤分出父级菜单和子级菜单
*/
List<Permission> parentSysMenuList = permissions.stream().filter(sysMenu -> sysMenu.getParentId() == 0).collect(Collectors.toList());
List<Permission> childSysMenuList = permissions.stream().filter(sysMenu -> sysMenu.getParentId() > 0).collect(Collectors.toList());
/**
* 将子级目录菜单转换为map对象
*/
Map<Integer, List<Permission>> map = childSysMenuList.stream().collect(Collectors.toMap(Permission::getParentId,
// 此时的value 为集合,方便重复时操作
s -> {
List<Permission> childSysMenuMap = new ArrayList<>();
childSysMenuMap.add(s);
return childSysMenuMap;
},
// 重复时将现在的值全部加入到之前的值内
(List<Permission> value1, List<Permission> value2) -> {
value1.addAll(value2);
return value1;
}
));
/**
* 循环对比,父级菜单和子级菜单,相同则加入父级对象中
*/
parentSysMenuList.forEach(e -> {
List<Permission> childSysMenus = map.get(e.getId());
e.setChildSysMenu(childSysMenus);
});
/**
* 需要对返回结果集排序,前端要展示第一个菜单项,做重定向
*/
parentSysMenuList.sort(Comparator.comparing(Permission::getOrderNum));
System.out.println(parentSysMenuList);
return parentSysMenuList;
}
}
校验用户是否存在,并将查询到的用户信息封装成LoginUser类返回。LoginUser类信息如下:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class LoginUser implements UserDetails , Serializable {
private static final long serialVersionUID = -3210884885630038713L;
private User user;
//存储权限信息
private List<String> permissions;
private List<Permission> permissionsList;
public LoginUser(User user, List<String> permissions) {
this.user = user;
this.permissions = permissions;
}
//存储SpringSecurity所需要的权限信息的集合
@JSONField(serialize = false)
private List<GrantedAuthority> authorities;
public LoginUser(User user, List<String> arrays, List<Permission> menuTreeRecursion) {
this.user = user;
this.permissions = arrays;
this.permissionsList = menuTreeRecursion;
}
public List<String> getPermissions() {
return permissions;
}
// TODO 权限字段
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
if(authorities!=null){
return authorities;
}
if (permissions == null) {
authorities = new ArrayList<>();
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;
}
}
该类存放用户信息与权限信息
2、登陆后的用户
@Component
@Slf4j
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter {
@Autowired
private RedisCache redisCache;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
//获取token
String token = request.getHeader("token");
if (!StringUtils.hasText(token)) {
//放行
filterChain.doFilter(request, response);
return;
}
//解析token
String userid;
try {
Claims claims = JwtUtil.parseJWT(token);
userid = claims.getSubject();
} catch (Exception e) {
e.printStackTrace();
throw new SystemException(TOKEN_EXCEPTION);
}
//从redis中获取用户信息
String redisKey = "login:" + userid;
LoginUser loginUser = redisCache.getCacheObject(redisKey);
if (Objects.isNull(loginUser)){
throw new SystemException(LOGIN_EXCEPTION);
}
//存入SecurityContextHolder
// 普通用户 没有权限信息
if (loginUser.getAuthorities() == null){
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,null);
} else {
UsernamePasswordAuthenticationToken authenticationToken =
new UsernamePasswordAuthenticationToken(loginUser,null,loginUser.getAuthorities());
SecurityContextHolder.getContext().setAuthentication(authenticationToken);
}
//放行
filterChain.doFilter(request, response);
}
}
Objects.isNull(loginUser)判断用户登陆是否过期或用户是否已经是退出登录的状态
loginUser.getAuthorities(),用户存在权限就存入权限信息,不存在则只用存入基本信息。
四、用户权限验证
通过authentication获取权限信息,比对url判断是否有权限进行访问
@Slf4j
public class MyAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
HttpServletRequest request = ((FilterInvocation) object).getRequest();
String requestUrl = request.getRequestURI();
log.info("#MyAccessDecisionManager#requestUrl"+requestUrl);
// 遍历用户拥有的权限,与当前请求的 URL 进行匹配
for (GrantedAuthority authority : authentication.getAuthorities()) {
log.info("#MyAccessDecisionManager#authority.getAuthority()"+authority.getAuthority());
if (requestUrl.contains(authority.getAuthority())) {
// 如果匹配成功,直接返回,表示有权限访问
return;
}
}
// 如果没有匹配成功,表示没有权限访问,抛出 AccessDeniedException 异常
throw new AccessDeniedException("Access Denied");
}
@Override
public boolean supports(ConfigAttribute attribute) {
return true;
}
@Override
public boolean supports(Class<?> clazz) {
return true;
}
}
配置springsecurity,添加权限验证过滤器与token拦截过滤器
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Autowired
private JwtAuthenticationTokenFilter jwtAuthenticationTokenFilter;
@Autowired
private AuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private AccessDeniedHandler accessDeniedHandler;
@Override
public void configure(WebSecurity web) throws Exception {
//解决静态资源被拦截的问题
web.ignoring()
.antMatchers(HttpMethod.GET, "/v1.0/index/product/**")
// 登录,注册,验证邮箱,验证昵称放行
.antMatchers("/v1.0/user/checkNickName/**")
.antMatchers("/v1.0/user/register")
.antMatchers("/v1.0/user/login")
.antMatchers("/v1.0/user/sendMail")
.antMatchers("/v1.0/user/checkMail")
// 公告信息
.antMatchers("/v1.0/index/notice/**")
// 聊天放行
.antMatchers("/v1.0/chat/**")
// 刷新token放行
.antMatchers("/v1.0/user/refreshToken")
// 上传图片不需要Toekn检查
.antMatchers("/v1.0/deal/img")
.antMatchers("/v1.0/manage/register")
.antMatchers("/v1.0/user/logout/**")
.antMatchers("/v1.0/user/userDetail/**")
.antMatchers("/v1.0/manage/register")
.antMatchers("/v1.0/user/logout/**")
.antMatchers(HttpMethod.OPTIONS)
.antMatchers(HttpMethod.PATCH);
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
//关闭csrf
.csrf().disable()
//不通过Session获取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
// 对于商品信息获取接口放行
.antMatchers(HttpMethod.GET, "/v1.0/index/product/**").permitAll()
// 登录,注册,验证邮箱,验证昵称放行
.antMatchers("/v1.0/user/checkNickName/**").permitAll()
.antMatchers("/v1.0/user/register").anonymous()
.antMatchers("/v1.0/user/login").anonymous()
.antMatchers("/v1.0/user/logout/**").anonymous()
.antMatchers("/v1.0/user/sendMail").anonymous()
.antMatchers("/v1.0/user/checkMail").anonymous()
// 公告信息
.antMatchers("/v1.0/index/notice/**").permitAll()
// 聊天放行
.antMatchers("/v1.0/chat/**").permitAll()
// 刷新token放行
.antMatchers("/v1.0/user/refreshToken").anonymous()
// 上传图片不需要Toekn检查
.antMatchers("/v1.0/deal/img").anonymous()
.antMatchers("/v1.0/user/userDetail/**").permitAll()
.antMatchers(HttpMethod.OPTIONS).permitAll()
.antMatchers(HttpMethod.PATCH).permitAll()
// 除上面外的所有请求全部需要鉴权认证
.anyRequest().authenticated().accessDecisionManager(new MyAccessDecisionManager());
//添加过滤器
http.addFilterBefore(jwtAuthenticationTokenFilter, UsernamePasswordAuthenticationFilter.class);
//配置异常处理器
http.exceptionHandling()
//配置认证失败处理器
.authenticationEntryPoint(authenticationEntryPoint)
.accessDeniedHandler(accessDeniedHandler);
//允许跨域.
http.cors();
}
@Bean
public AccessDecisionManager accessDecisionManager() {
List<AccessDecisionVoter<?>> decisionVoters = Arrays.asList(
new MyAccessDecisionVoter(),
new RoleVoter(),
new AuthenticatedVoter()
);
return new AffirmativeBased(decisionVoters);
}
}
总结:整体逻辑并不难,需要再进行改进的:
- 内部密码校验可以自定义指定
- SecurityConfig太多冗余的,就是因为MyAccessDecisionVoter拦截器拦截了所有请求,
public void configure(WebSecurity web)
这个方法才能放行 - LoginUser类中不应该直接存User全部信息,应该再进行拆解