公司老项目,之前是用前端进行资源鉴权,存在很大的风险和问题,最近安全漏洞检测告警好多次,准备对后端接口进行鉴权,于是进行改造。
大致设计:一个用户登录,先把他所有权限信息查出来放进redis,自定义注解、要进行权限控制的接口打上自己的权限注解,如果redis找得到权限信息就放行,找不到则返回无权限状态码:503
1、数据库表设计
2、自定义权限注解,方便对需要进行权限判断的接口打上标记
package com.yxkj.common.core.access;
import java.lang.annotation.*;
/**
* @author xiongxinyan
* @Description 每个接口方法上的权限控制注解
* @date 2023/4/17 9:42
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Documented // 在生成javac时显示该注解的信息
@Inherited
public @interface Access {
/**
* 用于配置具体接口的权限值
* 在数据库中添加对应的记录
* 用户登录时,将用户所有的权限列表放入redis中
* 用户访问接口时,将对应接口的值和redis中的匹配看是否有访问权限
* 用户退出登录时,清空redis中对应的权限缓存
*/
String value() default "";
}
3、自定义拦截器
package com.yxkj.common.core.access;
import com.alibaba.fastjson.JSONObject;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.yxkj.common.core.common.BussinessException;
import com.yxkj.common.core.common.GenericController;
import com.yxkj.common.core.config.RedisService;
import com.yxkj.common.core.config.SpringContextUtils;
import com.yxkj.common.core.entity.role.InterfacePermissionsVo;
import com.yxkj.common.core.entity.sys.SysPara;
import com.yxkj.common.core.mapper.InterfacePermissionsRoleMapper;
import com.yxkj.common.core.service.ISysParaService;
import com.yxkj.common.core.util.common.UserUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* @author xiongxinyan
* @Description 自定义权限拦截器Interceptor
* @date 2023/4/17 9:49
*/
@Component
public class AccessInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response1, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Method method = handlerMethod.getMethod();
Access access = method.getAnnotation(Access.class);
if (access == null) {
// 如果注解为null, 说明不需要拦截, 直接放过
return true;
}
RedisService redisService = getBean(RedisService.class, request);
String userId = UserUtils.getUserID();
String Access = "access:" + userId;
// 有注解,验证该用户是否有该接口权限
// 读取redis中缓存该用户权限进行判
String s = redisService.get(Access);
ISysParaService iSysParaService = SpringContextUtils.getBean(ISysParaService.class);
// Java过期时间
SysPara systokenTime = iSysParaService.getSysPara("Access_Token_Time");
if (null == s) {
// 为空则代表redis内不存在,从数据库获取
InterfacePermissionsRoleMapper interfacePermissionsRoleMapper = getBean(InterfacePermissionsRoleMapper.class, request);
List<InterfacePermissionsVo> interfacePermissionsVos = interfacePermissionsRoleMapper.selectInterfaceRole(userId);
if (interfacePermissionsVos.size() == 0) {
// 数据库不存在则塞一个默认值
redisService.put(Access, "0",(int) (Double.parseDouble(systokenTime.getParaValue()) * 60));
GenericController.getResponse().setStatus(503);
HttpServletResponse response = GenericController.getResponse();
response.setStatus(503);
throw new BussinessException(BussinessException.Code.E503);
} else {
// 若存在则判断是否存在该权限
StringBuilder accessAll = new StringBuilder();
for (InterfacePermissionsVo interfacePermissionsVo : interfacePermissionsVos) {
accessAll.append(interfacePermissionsVo.getInterfaceUrl()).append(":").append(interfacePermissionsVo.getInterfaceRequest()).append(",");
}
redisService.put(Access, accessAll.toString(),(int) (Double.parseDouble(systokenTime.getParaValue()) * 60));
//redis没有则读数据库存到redis中
// 返回是否存在的Boolean值
return accessAll.toString().contains(access.value());
}
}
// 不存在则返回503无权限状态码
if (!s.contains(access.value())) {
GenericController.getResponse().setStatus(503);
HttpServletResponse response = GenericController.getResponse();
response.setStatus(503);
throw new BussinessException(BussinessException.Code.E503);
}
return s.contains(access.value());
}
public <T> T getBean(Class<T> clazz, HttpServletRequest request) {
WebApplicationContext applicationContext = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
return applicationContext.getBean(clazz);
}
}
4、注册拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new AccessInterceptor()).addPathPatterns("/**");
}
5、修改权限过后清除redis缓存,解决缓存不一致问题
/**
* 删除redis下用户接口权限缓存
*/
private void deleteRedis() {
// 获取Redis中特定前缀
Set<String> keys = redisTemplate.keys("access:" + "*");
// 删除
redisTemplate.delete(keys);
}
6、对需要鉴权的接口加上注解
7、在数据库添加用户信息和接口权限即可
8、具体有无权限会出现如下页面
有权限:
无权限: