您的位置:首页 > 其它

使用自定义过滤器-Filter--实现对权限的控制

2018-10-31 17:54 525 查看

提示:大牛大佬们就没必要垂阅了,如果很幸运的被大佬翻牌了,也希望能够给出指教。3Q

关于权限控制即包含功能权限+数据权限

我们使用的方式可谓多种多样:通过自定义注解编写AOP方式或是使用第三方提供好的框架如 shiro 或 springsecurity 等。这里总结的是我自己实际项目开发中使用的自定义过滤器的方式实现权限控制(认证服务会分开说)。

为什么要使用过滤器而不使用拦截器

说明,实际两者都是可以实现权限过滤与控制,并无较大的实际区别。
intercept(拦截器)是基于反射机制实现,应用功能比较全面,访问 action 上下文对象,获取 spring 容器中的对象,在 action 生命周期中可以调用多次。这些都是 filter 无法做到或者只能做一次的。但是 filter 的有点在于可以拦截几乎一切请求,这取决于我们的配置(以往有.xml文件时候的path配置如:/,现在springboot使用注解方式的配置 @WebFilter(urlPattens="/")道理相同)。而拦截器只限拦截 action 请求,基于这点我们选择适用性更强的 Filter。

本次是微服务项目 基于springcloud+springboot+springMVC+Mybatis-Plus等技术
简单说明拦截流程

就是请求进入后台服务器之前需要先去认证服务器进行认证。通过token认证后方可继续向下执行。
我们的 filter 就是在认证之后到后台服务之间;
拦截范围是 /* (全部请求)
下面是自定义的拦截器代码

@Component
@WebFilter(urlPatterns = "/*", filterName = "ruleFilter")
public class RuleFilter implements Filter {

// 注入缓存Redis
@Autowired
private RedisUtil redisUtil;

@Override
public void init(FilterConfig filterConfig) throws ServletException {
}

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException {
// 首先转换 request 与 response
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;

RuleService ruleService = SpringContextHolder.getBean(RuleService.class);

// 获取请求的URL
String servletPath = request.getServletPath();

// 获取请求方式 POST 需要使用流操作
String reqMethod = request.getMethod();

ServletRequest requestWrapper = new RuleFilterHttpServletRequestWrapper(request);

// 判断URL权限池是否包含 当前访问的URL 如果包含 则需要判断权限 如果不包含则不需要权限访问
boolean ruleURLList1 = redisUtil.hasKey("ruleURLList");

List<String> ruleURLList = new ArrayList<>();

if (ruleURLList1){

Object ruleURLs = redisUtil.get("ruleURLList");
if (null != ruleURLs){
ruleURLList = (List<String>) ruleURLs;
}
}

// 判断如果ruleURLList为空需要重新查询
if (!ruleURLList1 || 0 == ruleURLList.size()){

// 重新查询
List<String> strings = ruleService.finAllRuleUrl();

// 放入Redis缓存中
redisUtil.set("ruleURLList", strings, 60*60*24*7);

ruleURLList = strings;
}

if (ruleURLList.contains(servletPath)) {

// 需要验证访问权限 两种情况 GET 或者 POST 请求
if ("POST".equals(reqMethod)) {
PrintWriter out = null;
HttpServletResponse responseSecond = (HttpServletResponse) response;
responseSecond.setCharacterEncoding("UTF-8");
responseSecond.setContentType("application/json; charset=utf-8");

String body = HttpHelper.getBodyString(requestWrapper);

//如果是POST请求则需要获取 param 参数
String param = URLDecoder.decode(body, "utf-8");

//json串 转换为Map
if (param != null && param.contains("=")) {
param = param.split("=")[1];
}

Map paramMap = (Map) JSONUtils.parse(param);
String obj_adminId =  String.valueOf(paramMap.get("adminId"));
System.out.println("获取到的用户id为"+obj_adminId);

/**
* 调用判断用户是否拥有权限方法
* obj_adminId 操作用户的id(其它属性也可 是缓存中的 key 唯一标识)
* servletPath 请求路径(每个业务方法的path都不同 以此区分权限)
* redisUtil 缓存对象(这里有一点下面有将 即怎样在filter中注入我们的缓存类呢)
* ruleService 权限的业务层接口对象
*/
Boolean falg = criteriaUserRule(obj_adminId, servletPath, redisUtil, ruleService);

// 判断如果此集合包含当前servletPath则说明用户拥有此权限
if (falg) {
// 直接放行
chain.doFilter(requestWrapper, response);
} else {
//  抛出自定义异常
throw new NoRuleException(401, "没有访问权限");
}

} else if ("GET".equals(reqMethod)) {

//
String adminid = request.getParameter("adminId");

// 调用判断用户是否拥有权限方法
Boolean falg = criteriaUserRule(adminid, servletPath, redisUtil, ruleService);

// 判断如果此集合包含当前servletPath则说明用户拥有此权限
if (falg) {
// 直接放行
chain.doFilter(request, response);

} else {
// 不包含说明此用户无访问当前RUL的权限 拦截并改写响应response 前面已经设置响应json格式

throw new NoRuleException(401, "没有访问权限");
}
}

} else {
// 不需访问权限 直接放行
chain.doFilter(requestWrapper, response);
}

}

/**
* 判断REDIS缓存中 此用户id是否拥有当前权限
* 注意:如果当前Redis缓存中没有当前用户的key 实际我们应该重新查询当前用户key对应的RulesURLs 并设置时间重新放入Redis中及返回对应的Boolean
*/
public Boolean criteriaUserRule(String adminId, String url, RedisUtil redisUtil, RuleService ruleService){

// 判断内存是否拥有当前用户权限URL集合 如果拥有直接对比 如果未拥有或者已过期 则重新查询并放入
boolean fl = redisUtil.hasKey(adminId);
List<String> userRuleUrls = new ArrayList<>();

if (fl){
Object obj = redisUtil.get(adminId);
if (null == obj || "" == obj){
fl = false;
} else {
userRuleUrls = (List<String>)obj;
}
}

if (!fl){
// 重新查询 并放入缓存中
userRuleUrls = ruleService.findAllUrlByUserId(Integer.parseInt(adminId));

redisUtil.set(adminId, userRuleUrls, 60*60*24);

System.out.println("用户的全部权限URL是"+userRuleUrls);
}

// 判断如果此集合包含当前servletPath则说明用户拥有此权限
if (userRuleUrls.contains(url)) {

// 拥有操作权限
return true;
} else {
// 没有操作权限
return false;
}
}

@Override
public void destroy() {

}
}

博主在这里遇到了几个麻烦,也可能会有其他人遇到,所以拿出来说一下。

第一个问题就是关于怎样在 filter 中注入特定对象

如我们自己写的 RedisUtile (缓存操作对象) 或者我们需要进行数据库操作而使用的 UserService 等对象,如果我们使用 spring 提供的 @AutoWired 注解直接进行注入

public class RuleFilter implements Filter {
@AutoWired
private UserService userService;
}

使用此方式会发现,当我们使用 userService 对象时,此对象为 null 导致报空指针,无法继续向下进行操作。经过查询得知其实是在filter初始方法执行的时候,spring容器中还没有相关bean对象。如何解决呢?找了很多帖子、博客,有说在初始化方法中注入bean的,也有说先获取bean工厂,再获取bean。逻辑上这些都可行,但在博主的项目中却都行不通。最后,哈哈就是他
解决spring管理之外的类需要获取spring容器中的bean(非常感谢<作者:江南烟雨>)
通过对此文的拜读,解决了注入 spring 管理的 bean 如 UserService 注入到并不归 spring 管理的类 RedisUtil 中。代码如下:

@Component
@Lazy(false)
@Order(1)
public class SpringContextHolder implements ApplicationContextAware {

private static ApplicationContext applicationContext;

/**
* 实现ApplicationContextAware接口的context注入函数, 将其存入静态变量.
*/
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
SpringContextHolder.applicationContext = applicationContext; // NOSONAR
}

/**
* 取得存储在静态变量中的ApplicationContext.
*/
public static ApplicationContext getApplicationContext() {
checkApplicationContext();
return applicationContext;
}

/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
@SuppressWarnings("unchecked")
public static <T> T getBean(String name) {
checkApplicationContext();
return (T) applicationContext.getBean(name);
}

/**
* 从静态变量ApplicationContext中取得Bean, 自动转型为所赋值对象的类型.
*/
public static <T> T getBean(Class<T> clazz) {
checkApplicationContext();
return (T) applicationContext.getBean(clazz);
}

/**
* 清除applicationContext静态变量.
*/
public static void cleanApplicationContext() {
applicationContext = null;
}

private static void checkApplicationContext() {
if (applicationContext == null) {
throw new IllegalStateException("applicaitonContext未注入,请在applicationContext.xml中定义SpringContextHolder");
}
}
}

使用此工具类之后就可以通过下面这种方式在 filter 中使用 userService 等 spring 管理的对象

@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain){
// 这里是为了具体说明
UserService userService = SpringContextHolder.getBean(UserService .class);
}
第二个问题是现在很多项目前后端分离开发(交互数据基本都为json,POST请求居多)

这种情况下就涉及一个流转换的问题,因为以往我们参数如果在get请求中会通过请求头携带传递,这样我们在 filter 中获取相关参数,然后判断是否放行等没有问题。
但是如果是在 post 请求,那么参数等都在请求体中,会以流的方式进入后台接收的 filter ,我们获取相关参数后,进行判断,那么问题来了。流 只要被读取过就会被消耗了(即消失了没有了)我们继续执行放行的 dofilter 方法中的 request 中就无法获取参数了。这里我们通过一个辅助类+一个继承了 HttpServletRequestWrapper 的类来解决:
感谢方法提供者:<代码老中医> 很详细,作为新手很容易看懂
通过这两个类 我们不仅可以获取请求中的各项参数,还能将读取后的流重新组织为新的流传递给下一层,保证了程序的正常正确运行。当然为了防止连接失效,本文也会贴上我自己的代码,如下:
辅助类

public class HttpHelper {
/**
* 获取请求Body
* @param request
* @return
*/
public static String getBodyString(ServletRequest request) {
StringBuilder sb = new StringBuilder();
InputStream inputStream = null;
BufferedReader reader = null;
try {
// 重新组织输入流 将request中获取到的流中的信息重新写入到新的流中 保证下一层的request流不为null
inputStream = request.getInputStream();
reader = new BufferedReader(new InputStreamReader(inputStream, Charset.forName("UTF-8")));
String line = "";
while ((line = reader.readLine()) != null) {
sb.append(line);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
if (inputStream != null) {
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (reader != null) {
try {
reader.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return sb.toString();
}
}
继承 Wrapper 的类

public class RuleFilterHttpServletRequestWrapper extends HttpServletRequestWrapper {

private final byte[] body;

public RuleFilterHttpServletRequestWrapper(HttpServletRequest request) throws IOException {
super(request);
// 首先读取输入流 获取到Request对象及流中的信息
Enumeration e = request.getHeaderNames()   ;
while(e.hasMoreElements()){
String name = (String) e.nextElement();
String value = request.getHeader(name);
System.out.println(name+" = "+value);

}
// 借助辅助类 HttpHelper 重新组织输入流 使保持与之前流相同 输入到控制层中
body = HttpHelper.getBodyString(request).getBytes(Charset.forName("UTF-8"));
}

@Override
public BufferedReader getReader() throws IOException {
return new BufferedReader(new InputStreamReader(getInputStream()));
}

@Override
public ServletInputStream getInputStream() throws IOException {

final ByteArrayInputStream bais = new ByteArrayInputStream(body);

return new ServletInputStream() {
@Override
public boolean isFinished() {
return false;
}

@Override
public boolean isReady() {
return false;
}

@Override
public void setReadListener(ReadListener listener) {

}

@Override
public int read() throws IOException {
return bais.read();
}
};
}

@Override
public String getHeader(String name) {
return super.getHeader(name);
}

@Override
public Enumeration<String> getHeaderNames() {
return super.getHeaderNames();
}

@Override
public Enumeration<String> getHeaders(String name) {
return super.getHeaders(name);
}
}

这里如果有同学找不到辅助类中那个 JOSNHelp 类,请不要着急,这个类就是一个Json 与 Object 转换的类,我们可以使用其它包提供的类,或者自己编写的类同样可以。比如博主就是用的 JSONUtils (并且其中的parse方法相差无几)

我的权限表其中一个字段 url 就是每个方法的访问 URL ,设计初衷也是想通过这种方式控制权限,这种方式适用性更好一些,后期维护成本也相对较低。

到这里大概说完了,好像有遗漏的,刚写博客,求原谅 后续会补充。
这里感谢大家的支持,java 之所以强,离不开大家无私的奉献,作为一个新手会再接再厉的,随时欢迎交流。

阅读更多
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: 
相关文章推荐