阶段一模块三

作业要求:

手写 MVC 框架基础上增加如下功能:

  1. 定义注解 @Security(有 value 属性,接收 String 数组),该注解用于添加在 Controller 类或者 Handler 方法上,表明哪些用户拥有访问该 Handler 方法的权限(注解配置用户名)

  2. 访问 Handler 时,用户名直接以参数名 username 紧跟在请求的 url 后面即可,比如 http://localhost:8080/demo/handle01?username=zhangsan

  3. 程序要进行验证,有访问权限则放行,没有访问权限在页面上输出

注意:自己造几个用户以及 url,上交作业时,文档提供哪个用户有哪个 url 的访问权限

分析:

参照着 Spring MVC 框架,很多同学第一反应都应该是拦截器 Interceptor,同样,我的第一反应也是实现拦截器,然后实现权限控制。然而,当我在参照着课程将手写 MVC 框架实现后,我发现调用 handler 方法的时候本身就是通过反射调用的,而权限校验恰好可以直接添加在这之前,这无疑比实现拦截器要简单快捷很多。

作业要求分析之后,其实就是获取请求中的 username 属性,然后按照 Controller 以及 Handler 方法上的 @Security 注解中的 value 数组进行比较,如果当前用户在数组中,则可以调用 Handler 方法,否则直接输出没有权限访问。

权限:

  1. Controller

    • DemoController

      映射请求路径:"/demo"

      权限:{"first", "second", "third"}

  2. Method

    • query()

      映射请求路径:"/query"

      权限:

    • queryArr()

      映射请求路径:"/queryArr"

      权限:

    • querySecurity()

      映射请求路径:"/querySecurity"

      权限:{"first"}

请求:

  1. http://localhost:8080 404
  2. http://localhost:8080/demo/query 没有权限
  3. http://localhost:8080/demo/query?username=first&name=xxx 正常执行
  4. http://localhost:8080/demo/query?username=second&name=xxx 正常执行
  5. http://localhost:8080/demo/query?username=first&name=xxx&name=vvv 正常执行
  6. http://localhost:8080/demo/query?username=fourth&name=xxx 没有权限
  7. http://localhost:8080/demo/queryArr?username=second&name=xxx&name=vvv 正常执行
  8. http://localhost:8080/demo/querySecurity?username=second&name=xxx 没有权限
  9. http://localhost:8080/demo/querySecurity?username=first&name=xxx 正常执行

代码:

注解部分:
  1. Security

    package cn.worstone.framework.annotation;
    
    import java.lang.annotation.*;
    
    @Documented
    @Target({ElementType.TYPE, ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    public @interface Security {
        String[] value();
    }
    
    
代码修改部分:
  1. DispatcherServlet

    package cn.worstone.framework.servlet;
    
    import cn.worstone.framework.annotation.*;
    import cn.worstone.framework.bean.Handler;
    import org.apache.commons.lang3.StringUtils;
    
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import javax.servlet.http.HttpServlet;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import javax.servlet.http.HttpSession;
    import java.io.File;
    import java.io.IOException;
    import java.io.InputStream;
    import java.lang.reflect.Field;
    import java.lang.reflect.Method;
    import java.lang.reflect.Parameter;
    import java.util.*;
    import java.util.regex.Matcher;
    import java.util.regex.Pattern;
    
    public class DispatcherServlet extends HttpServlet {
        private Properties properties = new Properties();
        private List<String> classNames = new ArrayList<>();
        private Map<String, Object> beansMap = new HashMap<>();
        private List<Handler> handlers = new ArrayList<>();
        private List<Class> commonsType = new ArrayList<>();
    
        {
            commonsType.add(HttpServletRequest.class);
            commonsType.add(HttpServletResponse.class);
            commonsType.add(HttpSession.class);
        }
    
        @Override
        protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            doPost(req, resp);
        }
    
        @Override
        protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
            // 处理请求
            // 根据请求 Url, 获取对应的 Handler, 获取其中的方法, 然后进行调用
            Handler handler = getHandler(req);
    
            if (handler == null) {
                resp.getWriter().write("404 NOT FOUND !");
                return;
            }
    
            // 权限校验
            if (!checkSecurity(req, resp, handler)) return;
    
            // 参数绑定
            // 构建参数数组, 数组长度与调用方法参数类型数组的长度一致
            Method method = handler.getMethod();
            Parameter[] parameters = method.getParameters();
            Object[] args = new Object[parameters.length];
    
            // 构建方法参数, 需要保证参数的顺序
            Map<String, String[]> parameterMap = req.getParameterMap();
            for (int i = 0; i < parameters.length; i++) {
                Parameter parameter = parameters[i];
                // 处理公共参数
                if (commonsType.contains(parameter.getType())) {
                    if (parameter.getType() == HttpServletRequest.class) {
                        args[i] = req;
                        continue;
                    }
                    if (parameter.getType() == HttpServletResponse.class) {
                        args[i] = resp;
                        continue;
                    }
                    if (parameter.getType() == HttpSession.class) {
                        HttpSession session = req.getSession();
                        args[i] = session;
                        continue;
                    }
                }
    
                String[] values = parameterMap.get(parameter.getName());
                // 没有找到当前参数
                if (values == null) continue;
                // 这里没有进行参数类型转换
                if (parameter.getType().isArray()) {
                    // 如果参数类型为数组, 则直接赋值为 values
                    args[i] = values;
                } else {
                    // 如果参数不为数组
                    if (values.length == 1) {
                        // 数组中只有 1 个元素
                        args[i] = values[0];
                    } else {
                        // 数组中有多个元素
                        args[i] = StringUtils.join(values, ",");
                    }
                }
            }
    
            // 调用 handler 中的方法
            try {
                handler.getMethod().invoke(handler.getController(), args);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        @Override
        public void init(ServletConfig config) throws ServletException {
            // 加载配置文件
            String contextConfigLocation = config.getInitParameter("contextConfigLocation");
            doLoadingConfig(contextConfigLocation);
    
            // 扫描注解
            doScan(properties.getProperty("scanPackage"));
    
            // 初始化 Bean 对象
            doInstance();
    
            // 实现依赖注入
            doAutowired();
    
            // 构造处理器映射器
            initHandlerMapping();
    
            System.out.println("MVC Framework init finished...");
        }
    
        private void doLoadingConfig(String contextConfigLocation) {
            InputStream inputStream = this.getClass().getClassLoader().getResourceAsStream(contextConfigLocation);
            try {
                properties.load(inputStream);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    
        private void doScan(String packagePath) {
            // 没有处理 classpath*:
            String path = Thread.currentThread().getContextClassLoader().getResource("").getPath() + packagePath.replaceAll("\\.", "/");
            File packageFolder = new File(path);
            File[] files = packageFolder.listFiles();
            for (File file : files) {
                if (file.isDirectory()) {
                    // 目录
                    doScan(packagePath + "." + file.getName());
                } else {
                    // 文件
                    if (file.getName().endsWith(".class")) {
                        String className = packagePath + "." + file.getName().replaceAll(".class", "");
                        classNames.add(className);
                    }
                }
            }
        }
    
        private void doInstance() {
            if (classNames.size() == 0) return;
            try {
                for (String className : classNames) {
                    // 获取 Class 对象
                    Class<?> aClass = Class.forName(className);
                    // 扫描 @Controller 注解以及 @Service 注解
                    if (aClass.isAnnotationPresent(Controller.class)) {
                        // 处理 @Controller
                        // 直接将类首字母小写作为 bean 名称
                        Object bean = aClass.getDeclaredConstructor().newInstance();
                        String name = lowerFirstCharacter(aClass.getSimpleName());
                        beansMap.put(name, bean);
                    } else if (aClass.isAnnotationPresent(Service.class)) {
                        // 处理 @Service
                        Object bean = aClass.getDeclaredConstructor().newInstance();
                        Service annotation = aClass.getAnnotation(Service.class);
                        // 获取注解 value 属性值
                        String name = annotation.value();
                        if ("".equals(name)) {
                            // value 属性值为 ""
                            // 如果没有指定 Service 名称, 则默认以其类名首字母小写作为名称
                            name = lowerFirstCharacter(aClass.getSimpleName());
                        }
                        beansMap.put(name, bean);
    
                        // Service 一般是面向接口开发, 为了后续的依赖注入, 这里根据接口类型存入一份
                        Class<?>[] interfaces = aClass.getInterfaces();
                        for (Class<?> anInterface : interfaces) {
                            beansMap.put(anInterface.getName(), bean);
                        }
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    
        private String lowerFirstCharacter(String simpleName) {
            char[] chars = simpleName.toCharArray();
            // 判断其首字母是否大写
            if ('A' <= chars[0] && chars[0] <= 'Z') {
                chars[0] += 32;
            }
            return String.valueOf(chars);
        }
    
        private void doAutowired() {
            if (beansMap.isEmpty()) return;
    
            // 判断对象的字段是否存在 @Autowired 注解
            for (String key : beansMap.keySet()) {
                Object bean = beansMap.get(key);
                Field[] fields = bean.getClass().getDeclaredFields();
                for (Field field : fields) {
                    if (field.isAnnotationPresent(Autowired.class)) {
                        // 当前字段存在 @Autowired 注解
                        Autowired annotation = field.getAnnotation(Autowired.class);
                        String name = annotation.value();
                        // 在 Spring 中这里判断的是 @Qualifier 注解
                        if ("".equals(name.trim())) {
                            // 如果没有指定具体名称, 则直接使用类型名
                            name = field.getType().getName();
                        }
    
                        // 开启暴力访问
                        field.setAccessible(true);
                        try {
                            // 属性赋值
                            // 判断当前的 name 对应的 bean 对象是否存在
                            Object autowired = beansMap.get(name);
                            if (autowired == null) {
                                throw new RuntimeException("The bean is not found. name: " + name);
                            }
                            field.set(bean, autowired);
                        } catch (IllegalAccessException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    
        private void initHandlerMapping() {
            if (beansMap.isEmpty()) return;
            for (String key : beansMap.keySet()) {
                Object bean = beansMap.get(key);
                Class<?> aClass = bean.getClass();
    
                if (aClass.isAnnotationPresent(Controller.class)) {
                    String baseUrl = "";
                    if (aClass.isAnnotationPresent(RequestMapping.class)) {
                        RequestMapping annotation = aClass.getAnnotation(RequestMapping.class);
                        baseUrl = annotation.value();
                    }
    
                    // 解析 @Security 注解
                    String[] typeValues = new String[0];
                    if (aClass.isAnnotationPresent(Security.class)) {
                        Security annotation = aClass.getAnnotation(Security.class);
                        typeValues = annotation.value();
                    }
    
                    // 获取当前 Controller 下的全部方法
                    Method[] methods = aClass.getMethods();
                    for (Method method : methods) {
                        if (method.isAnnotationPresent(RequestMapping.class)) {
                            // 当前方法存在 @RequestMapping 注解
                            RequestMapping annotation = method.getAnnotation(RequestMapping.class);
                            String specificUrl = annotation.value();
                            String url = baseUrl + specificUrl;
    
                            // 封装 Handler 对象
                            Handler handler = new Handler(bean, method, Pattern.compile(url));
    
                            // 存储 HandlerMapping 映射关系
                            handlers.add(handler);
    
                            // 解析 @Security 注解
                            String[] methodValues = new String[0];
                            if (method.isAnnotationPresent(Security.class)) {
                                Security anno = method.getAnnotation(Security.class);
                                methodValues = anno.value();
                            }
    
                            // 构建权限集合
                            List<String> security = new ArrayList<>();
                            if (typeValues.length == 0 || methodValues.length == 0) {
                                security.addAll(Arrays.asList(typeValues));
                                security.addAll(Arrays.asList(methodValues));
                            } else {
                                Set<String> set = new HashSet<>(Arrays.asList(typeValues));
                                for (String methodValue : methodValues) {
                                    if (!set.add(methodValue)) {
                                        security.add(methodValue);
                                    }
                                }
                            }
                            if (security.size() > 0) {
                                handler.setSecurityList(security);
                            }
                        }
                    }
                }
            }
        }
    
        private Handler getHandler(HttpServletRequest request) {
            if (handlers.isEmpty()) return null;
            String uri = request.getRequestURI();
            for (Handler handler : handlers) {
                Matcher matcher = handler.getPattern().matcher(uri);
                if (matcher.matches()) {
                    return handler;
                }
            }
            return null;
        }
    
        // 调用请求的时候判断权限
        /*
        private boolean checkSecurity(HttpServletRequest request, HttpServletResponse response, Handler handler) {
            String username = Optional.ofNullable(request.getParameter("username")).orElse("");
            // 校验 Controller 权限
            Object controller = handler.getController();
            boolean isSecurity = check(username, controller.getClass().getAnnotation(Security.class));
            if (!isSecurity) {
                // 没有权限
                securityHandle(request, response);
                return false;
            }
            // 校验 Method 权限
            Method method = handler.getMethod();
            isSecurity = check(username, method.getAnnotation(Security.class));
            if (!isSecurity) {
                // 没有权限
                securityHandle(request, response);
                return false;
            }
            return true;
        }
        */
    
        // 预处理方式判断权限
        private boolean checkSecurity(HttpServletRequest request, HttpServletResponse response, Handler handler) {
            String username = Optional.ofNullable(request.getParameter("username")).orElse("");
            List<String> securityList = handler.getSecurityList();
            if (securityList != null) {
                if (!securityList.contains(username)) {
                    securityHandle(request, response);
                    return false;
                }
            }
            return true;
        }
    
        private boolean check(String username, Security annotation) {
            // 权限校验
            if (annotation == null) return true;
            String[] values = annotation.value();
            for (int i = 0; i < values.length; i++) {
                if (username.equals(values[i])) {
                    return true;
                }
            }
            return false;
        }
    
        private void securityHandle(HttpServletRequest request, HttpServletResponse response) {
            // 没有权限的处理
            // 可以输出提示, 也可以跳转页面
            try {
                response.getWriter().write("The current user does not have permission.");
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
    
    

    在 DispatcherServlet 中做了一些修改,之前在课程里面老师接收的参数如果是数组则直接转换成字符串,每个数组元素以"," 分割,这个地方,我将其修改成支持 String 数组类型,如果调用方法中接收的是数组类型参数,则直接传入这个数组,如果是 String 类型,则与课程中的处理方式一致。同时,在不断修改代码的过程中我发现 paramIndexMapping 这个集合特别无用,所以这里将其去掉。我这里的处理方式是,每次获取方法的参数数组,然后循环这个参数数组,不断地按照顺序去构建方法参数数组,获取请求传入参数的时候,直接使用当前方法参数的名称直接获取,如果获取的数组不是 null,则证明当前传入参数不为空。

    关于权限验证这个地方,这里是在根据请求获取到 Handler 之后进行权限校验。如果当前用户没有权限,则直接输出 The current user does not have permission.。这里校验具体实现是,通过 Handler 获取当前请求的 Controller 对象以及 Method,然后分别对 @Security 注解进行判断,这里首先判断的是 Controller。这种方式有一些弊端,每次请求的时候,都会通过反射去判断 Controller 类上面以及对应方法上面是否存在 @Security 注解,效率很差,但是不是没有优点,节约空间成本,不需要去存储权限映射。进一步优化,在初始化的时候对 @Security 注解进行扫描,然后使用一个 Map 将权限信息进行存储,当请求来的时候,只需要直接判断即可。这种方式预处理了权限信息,所以当请求来的时候,处理权限效率非常高,但是会占用空间去存储权限信息。两种方式其实可以结合起来,就是请求来的时候判断去权限 Map 中去查找,如果没有则去解析当前 Handler 是否进行权限控制,处理完毕将其存储到权限 Map 中,这种方式相当于前面两种方式的综合。这次作业里面,实现了前面两种方式。

目前存在的缺点:(对比 Spring MVC)

  1. 方法参数处理相对比较简陋
  2. 视图解析方面没有实现
  3. 在解析框架的配置文件的路径的时候,没有处理 classpath*:

问题:

  1. Tomcat 10 会报找不到 Servlet 错误。 (因为 Tomcat 10 中的 Servlet 中修改)
  2. Maven 的编译后的变量名称依旧是没有变化,依旧是 arg。 (使用 Maven 将构建好的项目清理一下,重新构建即可)
  3. 配置了 @Service 注解依旧找不到对应的 Service,一直报 NullPointerException (getFields() 方法)

扩展:

Tomcat 10自带Jakarta® EE,包名不再是“javax.servlet.**”

修改:

直播的时候,作业讲解的时候,老师说遵循注解的“就近原则”,所以这里对代码进行修改。

说明一下修改思路:

分别获取 Controller 以及 Method 方法上面的 @Security 注解的 value 数组,初始值为 String[0]。创建一个集合,然后将 Method 解析的数组放入到集合中,如果 Method 解析的数组长度为 0,那么将 Controller 解析的数组放入到集合中。最后判断集合长度,如果为 0,则说明没有权限校验,如果不为 0,那么直接返回是否包含 username 即可。

// 调用请求的时候判断权限
private boolean checkSecurity(HttpServletRequest request, HttpServletResponse response, Handler handler) {
    String username = Optional.ofNullable(request.getParameter("username")).orElse("");
    List<String> securityList = new ArrayList<>();
    // 获取 Controller 权限
    Object controller = handler.getController();
    String[] typeValues = securityValues(controller.getClass().getAnnotation(Security.class));
    // 获取 Method 权限
    Method method = handler.getMethod();
    String[] methodValues = securityValues(method.getAnnotation(Security.class));
    securityList.addAll(Arrays.asList(methodValues));
    if (methodValues.length == 0) {
        securityList.addAll(Arrays.asList(typeValues));
    }
    if (securityList.size() == 0) return true;
    return securityList.contains(username);
}


private void initHandlerMapping() {
    if (beansMap.isEmpty()) return;
    for (String key : beansMap.keySet()) {
        Object bean = beansMap.get(key);
        Class<?> aClass = bean.getClass();

        if (aClass.isAnnotationPresent(Controller.class)) {
            String baseUrl = "";
            if (aClass.isAnnotationPresent(RequestMapping.class)) {
                RequestMapping annotation = aClass.getAnnotation(RequestMapping.class);
                baseUrl = annotation.value();
            }

            // 解析 @Security 注解
            String[] typeValues = new String[0];
            if (aClass.isAnnotationPresent(Security.class)) {
                Security annotation = aClass.getAnnotation(Security.class);
                typeValues = annotation.value();
            }

            // 获取当前 Controller 下的全部方法
            Method[] methods = aClass.getMethods();
            for (Method method : methods) {
                if (method.isAnnotationPresent(RequestMapping.class)) {
                    // 当前方法存在 @RequestMapping 注解
                    RequestMapping annotation = method.getAnnotation(RequestMapping.class);
                    String specificUrl = annotation.value();
                    String url = baseUrl + specificUrl;

                    // 封装 Handler 对象
                    Handler handler = new Handler(bean, method, Pattern.compile(url));

                    // 存储 HandlerMapping 映射关系
                    handlers.add(handler);

                    // 解析 @Security 注解
                    String[] methodValues = new String[0];
                    if (method.isAnnotationPresent(Security.class)) {
                        Security anno = method.getAnnotation(Security.class);
                        methodValues = anno.value();
                    }

                    // 构建权限集合
                    List<String> security = new ArrayList<>();
                    security.addAll(Arrays.asList(methodValues));
                    if (methodValues.length == 0) {
                        security.addAll(Arrays.asList(typeValues));
                    }
                    if (security.size() > 0) {
                        handler.setSecurityList(security);
                    }
                }
            }
        }
    }
}

权限:

  1. Controller

    • DemoController

      映射请求路径:"/demo"

      权限:{"first", "second", "third"}

  2. Method

    • query()

      映射请求路径:"/query"

      权限:

    • queryArr()

      映射请求路径:"/queryArr"

      权限:

    • querySecurity()

      映射请求路径:"/querySecurity"

      权限:{"first", "fourth"} 注意:这个地方对比之前修改过

请求:

  1. http://localhost:8080 404

  2. http://localhost:8080/demo/query 没有权限

  3. http://localhost:8080/demo/query?username=first&name=xxx 正常执行

  4. http://localhost:8080/demo/query?username=second&name=xxx 正常执行

  5. http://localhost:8080/demo/query?username=first&name=xxx&name=vvv 正常执行

  6. http://localhost:8080/demo/query?username=fourth&name=xxx 没有权限

  7. http://localhost:8080/demo/queryArr?username=second&name=xxx&name=vvv 正常执行

  8. http://localhost:8080/demo/querySecurity?username=second&name=xxx 没有权限

  9. http://localhost:8080/demo/querySecurity?username=first&name=xxx 正常执行

  10. http://localhost:8080/demo/querySecurity?username=fourth&name=xxx 正常执行

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值