Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制

前言:为了大家方便学习,这里提供在线网上书店项目的源码下载地址——百度网盘链接:https://pan.baidu.com/s/1GsjZUnEsDVPS6r4vYGhpWw,提取码:v1ma。

实现权限控制

我们以前做过一个简陋的权限管理系统,虽简陋但还是五脏俱全的,可那时我们是使用Filter实现URL级别的权限认证的,这种权限管理方案属于粗粒度的拦截方案,可我们也说过,还有一种拦截方案,即动态代理+注解的拦截方案(细粒度的拦截,可以拦截到某个具体业务方法上)
另外,前不久我们也做过一个简陋的在线网上书店项目,现在我们就对这个项目在业务层实现权限拦截,即拦截某个具体业务方法(这称之为细粒度的拦截)。想要实现这种方式的拦截,只须在我们想要拦截的某个具体业务方法上轻轻地加上那么一个注解,例如我们只想在service层提供给web层的与分类相关的服务——添加分类上轻轻地加上一个注解:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第1张图片
顺其自然地,我们接下来就要创建出这样的一个注解,在cn.liayun.utils包下创建一个@Permission注解。该注解的具体内容为:

package cn.liayun.utils;

import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;

@Retention(RetentionPolicy.RUNTIME)
public @interface Permission {

	String value();

}

温馨提示:千万不要忘记写@Retention(RetentionPolicy.RUNTIME)了。
随之而来的,我们就要思考这样一个问题:加入注解之后,内部是如何实现权限拦截的呢?其内部实现权限拦截的原理:我会写一个service层的工厂,对web层提供service层的服务,也即web层需要一个service,web层要先调用我service层的工厂拿到一个service,这样web层和service层之间就解耦了。web层调用我service层的工厂去获取一个service,那么这个工厂实际上返回给web层的,是一个代理对象(不能直接把真正的service返回),代理对象拦截对真正的service的所有方法的访问,代理对象拦截下来之后,检查一下方法上有没有注解,如果方法上有注解的话,意味着用户访问该方法需要权限,那我就检查一下这个用户有没有相对应的权限,若有则放行,让这个方法被访问,没有权限则不放行。
接下来,我们就要创建service层的工厂了,在cn.liayun.factory包中创建service层的工厂——ServiceFactory.java。该ServiceFactory类的代码编写起来还真挺麻烦的,可以说该类的代码是使用注解+动态代理实现该项目权限控制中的核心代码,但我们不应该害怕,对困难就应该迎难而上。第一次编写ServiceFactory类的代码,可能会是这样的:

package cn.liayun.factory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

import cn.liayun.service.BusinessService;
import cn.liayun.service.impl.BusinessServiceImpl;

public class ServiceFactory {

	private ServiceFactory() {}
	private static ServiceFactory instance = new ServiceFactory();
	public static ServiceFactory getInstance() {
		return instance;
	}
	
	//BusinessServiceProxy.addCategory(Category c)    or   BusinessServiceProxy.addBook(Book b)
	public BusinessService createService() {
		BusinessService service = new BusinessServiceImpl();
		
		return (BusinessService) Proxy.newProxyInstance(ServiceFactory.class.getClassLoader(), service.getClass().getInterfaces(), new InvocationHandler() {
			
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				
				//得到web层调用的方法名称,例如addCategory
				String methodName = method.getName();//addCategory
				
				//反射出真实对象上相对应的方法,检查真实对象的方法上有没有权限注解                                
				/*
                 * 真实对象相对应的方法和代理对象相对应的方法具有共同的行为,
                 * 即代理对象的方法有什么参数,真实对象相对应的方法上就有什么参数。
                 * 
                 * 也就是说你代理对象的方法(Method)是什么,我就得到真实对象上相对应的方法(Method)
                 */                                                          
				                                                             //method.getParameterTypes():得到代理对象的参数类型,例如Category.class
				Method realMethod = service.getClass().getMethod(methodName, method.getParameterTypes());
				
				return null;
			}
		});
	}
}

或许有人写到这里就被弄得云里雾里了,当初我也是这样,但我们可以这样倒推嘛!假设在Servlet那端是这样调用的:

BusinessService service = ServiceFactory.getInstance().createService();
service.addCategory(Category c);

当代理对象调用addCategory方法时,其实是该方法在调用invoke方法,那么在ServiceFactory类中的invoke方法就应该是这样的:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第2张图片
释疑之后,我们接着编写ServiceFactory类的代码。

package cn.liayun.factory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import cn.liayun.domain.Category;
import cn.liayun.domain.Privilege;
import cn.liayun.service.BusinessService;
import cn.liayun.service.impl.BusinessServiceImpl;
import cn.liayun.utils.Permission;
import cn.liayun.utils.SecurityException;

public class ServiceFactory {

	private ServiceFactory() {}
	private static ServiceFactory instance = new ServiceFactory();
	public static ServiceFactory getInstance() {
		return instance;
	}
	
	//BusinessServiceProxy.addCategory(Category c)    or   BusinessServiceProxy.addBook(Book b)
	public BusinessService createService() {
		BusinessService service = new BusinessServiceImpl();
		
		return (BusinessService) Proxy.newProxyInstance(ServiceFactory.class.getClassLoader(), service.getClass().getInterfaces(), new InvocationHandler() {
			
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				
				//得到web层调用的方法名称,例如addCategory
				String methodName = method.getName();//addCategory
				
				//反射出真实对象上相对应的方法,检查真实对象的方法上有没有权限注解                                
				/*
                 * 真实对象相对应的方法和代理对象相对应的方法具有共同的行为,
                 * 即代理对象的方法有什么参数,真实对象相对应的方法上就有什么参数。
                 * 
                 * 也就是说你代理对象的方法(Method)是什么,我就得到真实对象上相对应的方法(Method)
                 */                                                          
				                                                             //method.getParameterTypes():得到代理对象的参数类型,例如Category.class
				Method realMethod = service.getClass().getMethod(methodName, method.getParameterTypes());
				
				//看真实对象上相对应的方法上有没有@Permission注解
				Permission permission = realMethod.getAnnotation(Permission.class);
				if (permission == null) {//意味着该方法上没有加权限标签,即访问该方法不需要权限
					return method.invoke(service, args);
				}
				
				//意味着真实对象相对应的方法上有权限注解,则得到访问该方法需要的权限
				Privilege p = new Privilege(permission.value());//得到方法需要的权限
				
				//接着要检查用户是否有权限,但在检查之前,还要判断用户有没有登录。
				if (user == null) {
					//照理说应该抛一个用户没有登录的异常出去,但我们不想那么麻烦了
					throw new SecurityException("您没有登录");
				}
				
				return null;
			}
		});
	}
}

那现在我们就要思考user对象该怎么传递进来了?我们有2种方式:

  • 可以写一个AppContext类,其实它就是一个ThreadLocal,你可以把用户绑定到线程上面传递进来;
  • 在Servlet那端创建真实service的代理对象时,就将用户传递进来。

第一种方式太过麻烦了,考虑到简单这点,我们在这里采用第二种方式。接着我们编写完ServiceFactory类的代码。

package cn.liayun.factory;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;

import cn.liayun.domain.Privilege;
import cn.liayun.domain.User;
import cn.liayun.service.BusinessService;
import cn.liayun.service.impl.BusinessServiceImpl;
import cn.liayun.utils.Permission;
import cn.liayun.utils.SecurityException;

public class ServiceFactory {

	private ServiceFactory() {}
	private static ServiceFactory instance = new ServiceFactory();
	public static ServiceFactory getInstance() {
		return instance;
	}
	
	//BusinessServiceProxy.addCategory(Category c)    or   BusinessServiceProxy.addBook(Book b)
	public BusinessService createService(User user) {
		BusinessService service = new BusinessServiceImpl();
		
		return (BusinessService) Proxy.newProxyInstance(ServiceFactory.class.getClassLoader(), service.getClass().getInterfaces(), new InvocationHandler() {
			
			@Override
			public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
				
				//得到web层调用的方法名称,例如addCategory
				String methodName = method.getName();//addCategory
				
				//反射出真实对象上相对应的方法,检查真实对象的方法上有没有权限注解                                
				/*
                 * 真实对象相对应的方法和代理对象相对应的方法具有共同的行为,
                 * 即代理对象的方法有什么参数,真实对象相对应的方法上就有什么参数。
                 * 
                 * 也就是说你代理对象的方法(Method)是什么,我就得到真实对象上相对应的方法(Method)
                 */                                                          
				                                                             //method.getParameterTypes():得到代理对象的参数类型,例如Category.class
				Method realMethod = service.getClass().getMethod(methodName, method.getParameterTypes());
				
				//看真实对象上相对应的方法上有没有@Permission注解
				Permission permission = realMethod.getAnnotation(Permission.class);
				if (permission == null) {//意味着该方法上没有加权限标签,即访问该方法不需要权限
					return method.invoke(service, args);
				}
				
				//意味着真实对象相对应的方法上有权限注解,则得到访问该方法需要的权限
				Privilege p = new Privilege(permission.value());//得到方法需要的权限
				
				//接着要检查用户是否有权限,但在检查之前,还要判断用户有没有登录。
				if (user == null) {
					//照理说应该抛一个用户没有登录的异常出去,但我们不想那么麻烦了
					throw new SecurityException("您没有登录");
				}
				
				//检查用户是否有权限
				List<Privilege> list = service.getUserAllPrivilege(user);//得到用户拥有的所有权限
				if (list.contains(p)) {
					return method.invoke(service, args);
				}
				//意味着用户没有权限,就要抛一个异常出去(抛一个编译时异常出去,希望web层检查这个异常,给用户一个友好提示,说他没有权限)
				throw new SecurityException("您没有权限");
			}
		});
	}
}

接下来我们先编写出SecurityException异常,在cn.liayun.utils包下创建该异常。该异常的具体代码为:

package cn.liayun.utils;

public class SecurityException extends Exception {

	public SecurityException() {
		super();
		// TODO Auto-generated constructor stub
	}

	public SecurityException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
		super(message, cause, enableSuppression, writableStackTrace);
		// TODO Auto-generated constructor stub
	}

	public SecurityException(String message, Throwable cause) {
		super(message, cause);
		// TODO Auto-generated constructor stub
	}

	public SecurityException(String message) {
		super(message);
		// TODO Auto-generated constructor stub
	}

	public SecurityException(Throwable cause) {
		super(cause);
		// TODO Auto-generated constructor stub
	}

}

接下来,我们就要设计Privilege类,也即现在要做一个权限管理系统了,我们不可能再像以前那样做的那么麻烦了,没有那么多的时间,我们打算做一个非常简单的权限管理系统,所以就没有角色这个概念了,直接就说某个用户拥有某个权限,用户和权限是多对多的关系,即一个用户可以拥有多个权限,一个权限可以属于多个用户。
我们首先在数据库bookstore中创建2张表,建表语句如下:

create table privilege
(
	id varchar(40) primary key,
	name varchar(40),
	description varchar(255)
);

create table user_privilege
(
	user_id varchar(40),
	privilege_id varchar(40)
);

温馨提示:user_privilege表中没有建外键约束,一切从简。然后我们向这两张表中添加一些测试数据:

  • privilege表的测试数据如下:
    在这里插入图片描述
  • user_privilege表的测试数据如下:
    Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第3张图片

接下来,我们就要真正地设计Privilege类了,在cn.liayun.domain包中创建一个Privilege类。该类的具体代码如下:

package cn.liayun.domain;

public class Privilege {
	
	private String id;
	private String name;
	private String description;
	
	public Privilege() {
		super();
		// TODO Auto-generated constructor stub
	}
	
	public Privilege(String name) {
		super();
		this.name = name;
	}
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public String getDescription() {
		return description;
	}
	public void setDescription(String description) {
		this.description = description;
	}

	/*
     * 需要重写hashCode()、equals()方法,千万注意:只能重写name属性
     */
	@Override
	public int hashCode() {
		final int prime = 31;
		int result = 1;
		result = prime * result + ((name == null) ? 0 : name.hashCode());
		return result;
	}

	@Override
	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		Privilege other = (Privilege) obj;
		if (name == null) {
			if (other.name != null)
				return false;
		} else if (!name.equals(other.name))
			return false;
		return true;
	}
	
}

温馨提示:由于在编写ServiceFactory类的代码时,我们检查用户是否有权限的代码为:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第4张图片
学过Java基础的人应该知道List集合的contains方法内部调用的是equals方法,通常我们自定义的Privilege对象并没有重写equals方法(就连hashCode方法也一起重写算了,因为Eclipse会自动帮我们重写这两个方法),就这样比较是不行的。因此,应在Privilege类中重写这两个方法,但是只能重写name属性,因为Privilege实例对象只是用name属性new出来的
下面,我们就要编写得到用户所有权限的代码了。首先在UserDao接口中添加如下方法:
在这里插入图片描述
接着在UserDao接口的实现类——UserDaoImpl中实现该方法:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第5张图片
再接着在BusinessService接口中添加如下方法:
在这里插入图片描述
最后在BusinessService接口的实现类——BusinessServiceImpl中实现该方法:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第6张图片
现在我们终于可以来实现权限控制了。接下来我们修改web层的CategoryServlet的代码,由于该CategoryServlet需要一个service,所以会调用service层的工厂(即ServiceFactory)拿到一个service。因此我们就要这样修改CategoryServlet的代码:

package cn.liayun.web.manager;

import java.io.IOException;
import java.util.List;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.liayun.domain.Category;
import cn.liayun.domain.User;
import cn.liayun.factory.ServiceFactory;
import cn.liayun.service.BusinessService;
import cn.liayun.utils.SecurityException;
import cn.liayun.utils.WebUtils;

@WebServlet("/manager/CategoryServlet")
public class CategoryServlet extends HttpServlet {
	
	//private BusinessService service = new BusinessServiceImpl();
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String method = request.getParameter("method");
		if ("add".equals(method)) {
			add(request, response);
		}
		if ("getAll".equals(method)) {
			getAll(request, response);
		}
	}

	private void getAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//使用ServiceFactory工厂创建service时,需要传递一个用户过去
		BusinessService service = ServiceFactory.getInstance().createService((User) request.getSession().getAttribute("user"));
		try {
			List<Category> list = service.getAllCategory();
			request.setAttribute("categories", list);
			request.getRequestDispatcher("/manager/listcategory.jsp").forward(request, response);
		} catch (SecurityException e) {//getAllCategory方法调用的是invoke方法,而invoke方法是会抛出一个SecurityException异常的,
			                           //但是为什么这里抓不到这个异常呢?这是因为上面的代码并不会抛出这个SecurityException异常。
		}
		
	}

	private void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//balabala...
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}

明明下面这句代码:
在这里插入图片描述
是要抛出SecurityException异常的,但为什么我们抓这个异常的时候会报错呢?我们抓不住这个异常是因为这句代码根本不会抛出SecurityException异常,那这又是为什么呢?这个原因非常隐晦,因为invoke方法是由代理对象的addCategory(Category c)、addBook(Book b)等等诸如这些方法调用的,所以这个SecurityException异常就抛给了代理对象的这些方法,而这些方法又是由SUN公司帮我们产生代理对象的时候给我们动态产生的,我们把这个异常抛给这些方法,这些方法内部收到这个异常,就会转换成运行时异常抛出来
弄清这点之后,CategoryServlet的代码就该修改为:

package cn.liayun.web.manager;

import java.io.IOException;
import java.util.List;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import cn.liayun.domain.Category;
import cn.liayun.domain.User;
import cn.liayun.factory.ServiceFactory;
import cn.liayun.service.BusinessService;
import cn.liayun.utils.SecurityException;
import cn.liayun.utils.WebUtils;

@WebServlet("/manager/CategoryServlet")
public class CategoryServlet extends HttpServlet {
	
	//private BusinessService service = new BusinessServiceImpl();
	
	protected void doGet(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		String method = request.getParameter("method");
		if ("add".equals(method)) {
			add(request, response);
		}
		if ("getAll".equals(method)) {
			getAll(request, response);
		}
	}

	private void getAll(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//使用ServiceFactory工厂创建service时,需要传递一个用户过去
		BusinessService service = ServiceFactory.getInstance().createService((User) request.getSession().getAttribute("user"));
		try {
			List<Category> list = service.getAllCategory();
			request.setAttribute("categories", list);
			request.getRequestDispatcher("/manager/listcategory.jsp").forward(request, response);
		} catch (Exception e) {//getAllCategory方法调用的是invoke方法,而invoke方法是会抛出一个SecurityException异常的,
			                   //但是为什么这里抓不到这个异常呢?这是因为上面的代码并不会抛出这个SecurityException异常。
			
			//抓住异常,再得到导致异常的原因,判断导致这个异常(e)的异常是不是SecurityException,若是给用户以提示
			if (e.getCause() instanceof SecurityException) {
				request.setAttribute("message", e.getCause().getMessage());
				request.getRequestDispatcher("/message.jsp").forward(request, response);
			}
		}
		
	}

	private void add(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		//使用ServiceFactory工厂创建service时,需要传递一个用户过去
		BusinessService service = ServiceFactory.getInstance().createService((User) request.getSession().getAttribute("user"));
		
		try {
			Category c = WebUtils.request2Bean(request, Category.class);
			c.setId(UUID.randomUUID().toString());
			service.addCategory(c);
			
			request.setAttribute("message", "添加成功!!");
		} catch (Exception e) {
			if (e.getCause() instanceof SecurityException) {
				request.setAttribute("message", e.getCause().getMessage());
			} else {
				e.printStackTrace();
				request.setAttribute("message", "添加失败!!");
			}
		}
		request.getRequestDispatcher("/message.jsp").forward(request, response);
	}

	protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		// TODO Auto-generated method stub
		doGet(request, response);
	}

}

至此,注解+动态代理实现权限控制就已经完美完成了,大家可以进行测试哟!

测试一:没有任何用户登录时

Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第7张图片

测试二:yemeimei用户登录,但他没有添加分类的权限

Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第8张图片

测试三:liayun用户登录,他有添加分类的权限

Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第9张图片

总结

这种细粒度的拦截不仅可以拦截到某个具体业务方法上,还可以拦截对某个属性字段的访问。假设有一个JavaBean代表一个产品,该产品有如下属性:
Java Web基础入门第九十四讲 在线网上书店(九)——注解+动态代理实现权限控制_第10张图片
也即拥有不同权限的人可以看见不同的字段。要想实现这样的权限控制,可这样做:

@Permission("老板")
public double getPrice() {
    return price; 
}

我只是简单地概述了一下,并没有写代码来实现这样的权限控制,但我们还是要知道这种情况的,因为在实际开发中还是有这样的需求的。

总结一

将来在做开发的时候,我们会学Spring框架,可能会碰到这样一种场景,我们写好一个对象之后,结果把这个对象交给Spring,然后再把这个对象get出来,会发现这个对象会比原来强大很多。其原理是:我们往Spring容器里面加了一个对象,然后再get出来的时候,get出来的不是原始对象,而是利用动态代理技术产生的代理对象。即容器给我们的是一个代理对象,代理对象拦截了对原有真实对象所有方法的访问,在原有真实对象的方法的基础上增强了很多。

总结二

在Java里面,我们要产生某个对象的代理对象,这个对象必须要有一个特点,即这个对象必须实现一个接口,动态代理技术只能基于接口进行代理。有时候我们在做开发的时候,这个对象就没有实现接口,有人可能会说,它既然没有接口,那我就给它定义一个接口,这是不行的,因为有时候我们拿到一个对象,而这个对象是服务器产生给我们的,是服务器提供给我们的,又不是我们自己写的,动不动就给它定义一个接口,给它找个爸爸,哪那行呢?但我们现在要对它进行增强,这时用动态代理技术就不行了,因为动态代理技术只能是基于接口,那如果这个对象没有接口,又该怎么做呢?
那这时我们就需要使用另外一套API——cglib了,这套API,即使没有接口,它也可以帮我们产生这个对象的代理对象。它的内部是怎么去产生这个对象的代理对象的呢?实际上产生的是这个对象的子类,也即我们把一个对象交给cglib,它返回出来的似乎是一个代理对象(但它不是要产生一个对象的代理对象),但其实这个代理对象就是这个对象的子类,利用子类的方式来创建代理对象。在Sping里面就是这样做的,Spring里面有一个AOP编程(即面向切面编程,说白了就是动态代理,我们经常会交给Spring一个对象,它就会返回代理对象给我们,它在返回代理对象的时候,首先会检查我们这个对象有没有实现一个接口,如果我们这个类有接口,它使用Java的动态代理技术来帮我们构建出代理对象;如果我们这个类没有实现接口,它会使用cglib这套API,采用创建子类的方式来创建代理对象)

总结三

在实际开发里面,还有这样一类问题,现在交给你这样一个类,这个类既没有实现接口,又用final修饰,这时在Java界你就无法产生其代理对象了。

你可能感兴趣的:(Java,Web基础入门)