RBAC 权限:
一、关系:
这基于角色的访问控制的结构就叫RBAC结构。
二、RBAC 重要对象:
- 用户(Employee):角色施加的主体;用户通过拥有某个或多个角色以得到对应的权限。
- 角色(Role):表示一组权限的集合。
- 权限(Permission):一个资源代表一个权限,是否能访问该资源,就是看是否有该权限。
三、权限认证方式:
--其实就是控制用户访问我们的controller层中的方法。
--- 可以自定义一个注解,在需要权限的方法上面贴一个注解,在不需要权限的方法上就不贴注解。
例子:
@ RequiredPermission (name="权限名称",expression="权限表达式") //--expression:是我们判断的依据。
public User list(){
//查询所有学生
}
步骤:
1.自定义注解;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiredPermission {
String name();//----权限名称
String expression();//----权限表达式
}
2.将自定义注解贴在需要校验权限请求方法上面;
@RequstMapping("/list")
@RequiredPermission (name="员工新增或修改",expression="department:list")
public void saveOrUpdate(Role role) {
if (role.getId() != null) { // 修改
roleService.update(role);
} else { // 新增
roleService.save(role);
}
}
注意:
在权限系统开发中,一般权限系统有一个权限加载的功能。
如果没有权限加载的功能的话,要把系统中所有的权限信息保存到数据库中。假如有 10个Controller接口,每个接口中有5个方法贴了自定义注解 @RequiredPermission 这样的话,得我们自己手动新增50次,而如果通过权限加载这个功能的话,就只需要点击这个功能按钮。这个按钮就可以把我们系统中所有贴了该注解的权限信息保存到数据库中。
权限加载的功能实现:
实现权限加载步骤:
0. 先把数据库中所有的权限表达式查询出来
1. 先拿到所有Controller --> 通过Spring容器去拿
2. 通过contoller去拿到每一个controller方法
3. 通过方法去拿方法上的注解@RequirdPermission
4. 判断如果注解不为空并且注解中权限表达式不在数据库中,拿到注解中name,expression,封装到Permission对象中
5. 把permission对象保存到数据库权限信息表中
方式一:
@Service
@Slf4j
public class PermissionServiceImpl implements IPermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
private ApplicationContext context; //获取Spring Ioc 容器
@override
public void reload() {
//0 先把数据库中所有的权限表达式查询出来
List<String> expression = permissionMapper.selectExpression();
// 1 从spring容器中去拿到所有的controLLer
Map<String, Obiect> beansithAnnotation = context.getBeanswithAnnotation(Controller.class);
// map key 把controller类名小写字符串 ,value才是我们想要的controller
Collection<Object> controllers = beanswithAnnotation.values();
// 2 根据controller 拿到每一个方法
for (Object controller : controllers) {
//拿到controller对象,通过反射,去获取字节码对象,在获取自身所有的方法
Method[] methods = controller.getClass().getDeclaredMethods();
for (Method method : methods){
// 3 根据方法拿到方法上注解 @RequiredPermission
RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
//判断注解是否为空,将在数据库中查询的权限表达式(第0步),与注解上表达式比对,获取list中不包含的权限表达式
if(annotation!=null && !expression.contatins(annotation.expression())){
// 4 判断注解是否为空 ,如果不为空数据封装到Permission对象中
Permission p = new Permission();
String name = annotation.name():
String expression = annotation.expression();
p.setExpression(expression);
p.setName(name);
// 5.把数据保存到数据库中
permissionMapper.insert(p);
}
}
}
}
方式二:
@Service
@Slf4j
public class PermissionServiceImpl implements IPermissionService {
@Autowired
private PermissionMapper permissionMapper;
@Autowired
//SpringMVC处理器映射器 ;通过处理器映射器获取Controller注解类中的方法信息,封装到HandlerMethod中
private RequestMappingHandlerMapping requestMappingHandlerMapping;//SpringMVC处理器映射器
@override
public void reload() {
//0 先把数据库中所有的权限表达式查询出来
List<String> expression = permissionMapper.selectExpression();
// 在启动的时候requestMappingHandlerMapping 会把controller中所有的方法封装HandlerMethod 中
Map<RequestMappingInfo, HandlerMethod> map = requestMappingHandlerMapping.getHandlerMethods();
Collection<HandlerMethod> handlerMethods = map.values();
for (HandlerMethod handlerMethod : handlerMethods) {
// 到方法
Method method = handlerMethod.getMethod();
// 方法上的注解
//3 根据方法拿到方法上注@RequiredPermission
RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
if(annotation!=null 8& !expressions.contains(annotation.expression())){
// 4 断注解是否为空 ,如果不为空把权限表达式数据封装到Permission对象中
Permission p = new Permission();
String name = annotation.name();
String expression = annotation.expression();
p.setName(name);
p.setExpression(expression);
// 5 把数据保存到数据库中
permissionMapper.insert(p);
}
}
}
权限新增删除:
角色(Role) 角色中间表(role_permission) 权限(permission)
即:其通过对角色:新增或删除权限:
通过中间表来关联,并在中间表上新增和删除。在中间表上对外键 role_id与permission_id 之间的关系来增加或删除。
用户(Employee) 用户中间表(Employee_role) 角色(role)
删除的时候,不仅删除用户或角色表中的信息,还要删除中间表中的信息。
四、登录校验:
@Data
public class JsonResult {
private boolean success;
private String msg;
public JsonResult(boolean success, String msg) {
this.success = success;
this.msg = msg;
}
}
1.controller层:
@Controller
public class LoginController {
@Autowired
private IEmployeeService employeeService;
@Autowired
private IPermissionService permissionService;
@RequestMapping("/login")
@ResponseBody
// {"success":true,"msg":"登录成功"}
// {"success":false,"msg":"登录失败"}
public JsonResult login(String username, String password) {
try{
Employee employee = employeeService.login(username, password);
return new JsonResult(true, "操作成功");
}catch{
e.printStackTrace();
return new JsonResult(false, e.getMessage);
}
}
2.service层:
@Service
public class EmployeeServiceImpl implements IEmployeeService {
@Override
public Employee login(String username, String password) {
//校验参数非空判断
if(StringUtils.isEmpty(username) || StringUtil.isEmpty(password)){
throw new RuntimeException("账号或者密码不能为空");
}
//根据账号和密码上数据库中进行查询
Employee employee = employeeMapper.selectByUsernameAndPassword(username, password);
//如果为空抛出异常
if(employee == null){
throw new RuntimeException("账号或者密码错误");
}
return employee;
}
}
3.前端:
$(".submitBtn").click(function () (
// 发送ajax请求
$.post("/login",$("#loginForm").serialize(),function (data) {
// data -> JsonResult
if(data.success)(
Location.href="/department/list";
}else(
Swal.fire({
text: data.msg
})
}
})
})
4.***注意这样的话还是不行,用户可以直接输入网址进入页面;所以要对url进行拦截,进行登录校验;
步骤:
第一步: 用户开始时登录的时候,如果用户登录成功,则把用户信息放到ssesion中。
第二步: 定义一个拦截器,对每一次用户请求的资源进行拦截。
第三步: 对拦截器进行配置。
(1)要拦截哪些资源?
(2)排除哪些资源路径?
第一步:用户信息放到ssesion中
@Controller
public class LoginController {
@Autowired
private IEmployeeService employeeService;
@Autowired
private IPermissionService permissionService;
@RequestMapping("/login")
@ResponseBody
// {"success":true,"msg":"登录成功"}
// {"success":false,"msg":"登录失败"}
public JsonResult login(HtttpSession session, String username, String password) {
try{
Employee employee = employeeService.login(username, password);
//验证用户身份成功之后,将用户的信息保存到session中
session.setAttribute("USER_IN_SESSION", employee);
return new JsonResult(true, "操作成功");
}catch{
e.printStackTrace();
return new JsonResult(false, e.getMessage);
}
}
第二步: 定义一个拦截器
public class CheckLoginInterceptor implements HandlerInterceptor{
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, 0bject handler) throws{
// 查询session 中保存用户信息
Employee employee = (Employee) request.getSession().getAttribute( "USER_IN_SESSION");
// 判断如果存在就放行,如果不存在跳转到登灵界面中
if(employee!=nul1){
return true;
}
response.sendRedirect( s:"/static/login.html"); //重定向到登录界面
return false;
}
第三步: 对拦截器进行配置:
@Configuration
public class WebConfig implements WebMvcConfigurer (
@Bean
public CheckLoginInterceptor checkLoginInterceptor(){
return new CheckLoginInterceptor();
}
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(checkLoginInterceptor()).
addPathPatterns("/**").//将拦截器注入到容器中
excludePathPatterns("/static/**","/login","/logout");//放行那些资源
}
五、退出登录:
就是把session中登录的信息删除掉
@RequestMapping("/logout")
public String logout(HttpSession session){
session.removeAttribute("USER_IN_SESSION");
return "redirect:/static/login.html";//重定向
}
六、权限拦截:
分析:
步骤:
1 上session去获取当前登录用户信息
2 判断用户是否是超级管理员->如果是直接放行
3 拿到当前访问方法
4 获取方法上的注解@RequiredPermission
5 判断注解是否为空 ->直接放行
6 要把当前登录的用户所拥有的权限表达式给查询出来
7 如果注解中权限表示式是在从数据库中查询出来权限表达式集合中,说明用户拥有这个权限的 ->放行
8 没有这个权限跳转到没有权限界面中,进行拦截
1.定义一个拦截器
***拓展:
拦截器中 preHandle 方法的参数列表中有一个参数叫:Object handler 的参数:
通过反射 handler.getClass打印输出为:class org.springframework.web.method.HanderMethod(真实类型)
在 SpringMvc 中: 有一个处理器映射器。处理器映射器会把我们所有贴有Controller注解的类中的方法的信息,封装到HanderMethod中。
所以在拦截器中我们可以通过 HanderMethod 这个去获得贴有Controller注解的类中的方法的信息
/**
*权限校验拦截器
**/
public class CheckPermissionIntercetor implements HandlerInterceptor{
@Autowird
private IPermissionService permissionService;
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
// 1.从session中去拿当前用户登录信息
Employee employee = (Employee) request.getSession().getAtribute("USER_IN_SESSION");
// 2.判断是否是超级超级管理员
if(employee.isAdmin()){
return true;
}
// 3.获取到当前访问方法 不明白看拓展;
HandlerMethod handlerMethod = (Handlerethod)handler
Method method = handlerMethod.getMethod();
// 4.通过方法拿方法上的注解
RequiredPermission annotation = method.getAnnotation(RequiredPermission.class);
// 5.判断注解如果为空表示访问方法时不需要权限
if(annotation == null){
return true;
}
// 6.把用户所拥有的所有权限表达式集合给查询出来
List<String> list = permissionService.getExpressionByEmpId(employee.getId());
// 7.判断注解中的权限表达式,是否在集合中
if(list.contatins(annotation.expression())){
return true;
}
// 8. 如果不包含则表示没有权限 跳转到没有权限的页面中
response.sendRedirect("/permission/nopermission");//url从定向
}
2.编写url从定向接口
@RequestMapping("/nopermission")
public String nopermission(){
return "自己定义的页面路径";
}
3.将拦截器注入到容器中
@Configuration
public class WebConfig implements WebMvcConfigurer (
//登录拦截器
@Bean
public CheckLoginInterceptor checkLoginInterceptor(){
return new CheckLoginInterceptor();
}
//权限拦截器
@Bean
public CheckPermissionIntercetor checkPermissionIntercetor(){
return new CheckPermissionIntercetor();
}
//登录拦截器注入
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(checkLoginInterceptor()).
addPathPatterns("/**").//将拦截器注入到容器中
excludePathPatterns("/static/**","/login","/logout");//放行那些资源
}
//登录拦截器注入
@Override
public void addInterceptors(InterceptorRegistry registry){
registry.addInterceptor(CheckPermissionIntercetor()).
addPathPatterns("/**").//将拦截器注入到容器中
excludePathPatterns("/static/**","/login","/logout");//放行那些资源
}
}
七、统一异常处理
如何解决
-
手动 try
- 弊端是到处是重复代码,系统的代码耦合度高,工作量大且不好统一,维护的工作量也很大。
-
利用 Spring MVC 的方式
- Spring MVC 为 Controller 处理方法执行出现异常提供了全局统一处理,可以使用
@ExceptionHandler
配合@ControllerAdvice
注解实现异常处理,可减少代码量,提高拓展性和可维护性。 - 添加处理控制器异常处理类,确保 Spring 配置中要能扫描到这个类。
- 针对不同异常进行不同处理,针对不同处理方法响应的内容,需要进行不同处理,比如原来方法响应 HTML 依然响应 HTML,若原来方法响应 JSON 依然响应 JSON。
- Spring MVC 为 Controller 处理方法执行出现异常提供了全局统一处理,可以使用
/**
* 对控制器进行增强处理
*/
@ControllerAdvice
public class RuntimeExceptionHandler {
/**
* 该方法是用于捕获并处理某种异常
* e:现在出现的异常对象
* method:现在出现异常的那个处理方法
*/
@ExceptionHandler(RuntimeException.class)
public String exceptionHandler(RuntimeException e, HandlerMethod method, HttpServletResponse response) {
e.printStackTrace(); // 方便开发的时候找 bug
// 若原本控制器的方法是返回 JSON,现在出异常也应该返回 JSON
// 获取当前出现异常的方法,判断是否有 ResponseBody 注解,有就代表需要返回 JSON
if(method.hasMethodAnnotation(ResponseBody.class)){
try {
response.setContentType("application/json;charset=UTF-8");
response.getWriter()
.print(JSON.toJSONString(new JsonResult(false, "系统异常,请联系管理员")));
} catch (IOException e1) {
e1.printStackTrace();
}
return null;
}
// 若原本控制器的方法是返回 HTML,现在也应该返回 HTML
return "common/error";
}
}
2、自定义异常
在开发中还可以根据自己业务的异常情况来自定义业务逻辑异常类,一般继承于 java.lang.RuntimeException
。
public class LogicException extends RuntimeException {
public LogicException(String errorMsg){
super(errorMsg);
}
}
比如虽然登录出错,也响应 JSON 数据,但其需要提示的消息更详细,所以通过自定义异常,再利用 Spring MVC 全局处理异常方式,针对这个异常进行专门处理。可参考另一篇博客SpringBoot统一异常处理