作业要求:
手写 MVC 框架基础上增加如下功能:
-
定义注解
@Security
(有value
属性,接收 String 数组),该注解用于添加在Controller
类或者Handler
方法上,表明哪些用户拥有访问该Handler
方法的权限(注解配置用户名) -
访问
Handler
时,用户名直接以参数名username
紧跟在请求的url
后面即可,比如http://localhost:8080/demo/handle01?username=zhangsan
-
程序要进行验证,有访问权限则放行,没有访问权限在页面上输出
注意:自己造几个用户以及 url
,上交作业时,文档提供哪个用户有哪个 url
的访问权限
分析:
参照着 Spring MVC 框架,很多同学第一反应都应该是拦截器 Interceptor
,同样,我的第一反应也是实现拦截器,然后实现权限控制。然而,当我在参照着课程将手写 MVC 框架实现后,我发现调用 handler
方法的时候本身就是通过反射调用的,而权限校验恰好可以直接添加在这之前,这无疑比实现拦截器要简单快捷很多。
作业要求分析之后,其实就是获取请求中的 username
属性,然后按照 Controller
以及 Handler
方法上的 @Security
注解中的 value
数组进行比较,如果当前用户在数组中,则可以调用 Handler
方法,否则直接输出没有权限访问。
权限:
-
Controller
-
DemoController
映射请求路径:
"/demo"
权限:
{"first", "second", "third"}
-
-
Method
-
query()
映射请求路径:
"/query"
权限:无
-
queryArr()
映射请求路径:
"/queryArr"
权限:无
-
querySecurity()
映射请求路径:
"/querySecurity"
权限:
{"first"}
-
请求:
- http://localhost:8080 404
- http://localhost:8080/demo/query 没有权限
- http://localhost:8080/demo/query?username=first&name=xxx 正常执行
- http://localhost:8080/demo/query?username=second&name=xxx 正常执行
- http://localhost:8080/demo/query?username=first&name=xxx&name=vvv 正常执行
- http://localhost:8080/demo/query?username=fourth&name=xxx 没有权限
- http://localhost:8080/demo/queryArr?username=second&name=xxx&name=vvv 正常执行
- http://localhost:8080/demo/querySecurity?username=second&name=xxx 没有权限
- http://localhost:8080/demo/querySecurity?username=first&name=xxx 正常执行
代码:
注解部分:
-
Security
package cn.worstone.framework.annotation; import java.lang.annotation.*; @Documented @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) public @interface Security { String[] value(); }
代码修改部分:
-
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)
- 方法参数处理相对比较简陋
- 视图解析方面没有实现
- 在解析框架的配置文件的路径的时候,没有处理
classpath*:
问题:
Tomcat 10 会报找不到 Servlet 错误。(因为 Tomcat 10 中的 Servlet 中修改)Maven 的编译后的变量名称依旧是没有变化,依旧是 arg。(使用 Maven 将构建好的项目清理一下,重新构建即可)配置了 @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);
}
}
}
}
}
}
权限:
-
Controller
-
DemoController
映射请求路径:
"/demo"
权限:
{"first", "second", "third"}
-
-
Method
-
query()
映射请求路径:
"/query"
权限:无
-
queryArr()
映射请求路径:
"/queryArr"
权限:无
-
querySecurity()
映射请求路径:
"/querySecurity"
权限:
{"first", "fourth"}
注意:这个地方对比之前修改过
-
请求:
-
http://localhost:8080 404
-
http://localhost:8080/demo/query 没有权限
-
http://localhost:8080/demo/query?username=first&name=xxx 正常执行
-
http://localhost:8080/demo/query?username=second&name=xxx 正常执行
-
http://localhost:8080/demo/query?username=first&name=xxx&name=vvv 正常执行
-
http://localhost:8080/demo/query?username=fourth&name=xxx 没有权限
-
http://localhost:8080/demo/queryArr?username=second&name=xxx&name=vvv 正常执行
-
http://localhost:8080/demo/querySecurity?username=second&name=xxx 没有权限
-
http://localhost:8080/demo/querySecurity?username=first&name=xxx 正常执行
-
http://localhost:8080/demo/querySecurity?username=fourth&name=xxx 正常执行