spring 使用注解的方法完成拥有者权限验证

提示:本文中,咱们只给了部分示例代码。
若是你须要完整的代码,请点击: https://github.com/mengyunzhi/authAnnotationSample

在实际的项目中,咱们的每每会有这样的问题。
好比简单的学生、班级、教师三个实体。班级1属于教师1,班级2属于教师2。教师1登陆后,咱们在班级列表中,展示班级1。点击班级1后,咱们获取后台:/Clazz/1来获取这个班级的信息。可是,若是用户直接在网络中,修改请求地址,好比修改成 /Class/2来获取班级为2的信息,因为咱们并无作权限处理,则直接将班级2的信息返回给了只有班级1权限的教师1。
咱们把上述问题称为:拥有者权限认证问题。java

其它解决方法

  1. 最简单的思想,固然是findById()方法中,使用逻辑判断。
  2. 固然咱们也能够为ClazzController::findById(Long id)添加注解,在注解中获取传入的ID,并就此来判断当前登陆用户是否拥有相关权限。

本文解决方法

本文将阐述如何利用统一的解决方案,来实现使用一个注解,进行达到验证某个登陆的教师是否有班级查看权限,是否有学生查看权限等多个实体查看、删除、更新权限的功能。git

数据准备

初始化

初始化三个实体:学生、班级、教师。
学生:班级 = 0..n : 1
班级:教师 = 0..n : 1
其中,教师、班级的关键字类型为Integer,学生实体的为longgithub

同时,创建三个表对应的Repository(DAO)层,并继承CrudRepository接口。web

好比:spring

package com.mengyunzhi.auth_annotation_sample.repository;

import com.mengyunzhi.auth_annotation_sample.entity.Teacher;
import org.springframework.data.repository.CrudRepository;

public interface TeacherRepository extends CrudRepository<Teacher, Integer> {
}

创建接口

因为咱们要实现:能够对全部的实体进行拥有者权限认证,那么就须要考虑有些实体是尚未存在的。在JAVA中,为了实现诸如这样的功能,能够新建一个接口来统一实体的标准。
在些,咱们新建yunzhiEntity接口。json

package com.mengyunzhi.auth_annotation_sample.entity;

public interface YunZhiEntity {
}

上一步的三个实体类,分别实现该接口,好比:api

@Entity
public class Clazz implements YunZhiEntity {

创建控制器及方法

分别创建TeacherController::findById以及KlazzController::findById,并在方法中实现获取某个ID值的实体。网络

例:app

package com.mengyunzhi.auth_annotation_sample.controller;

import com.mengyunzhi.auth_annotation_sample.entity.Student;
import com.mengyunzhi.auth_annotation_sample.repository.StudentRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("Student")
public class StudentController {
    @Autowired
    StudentRepository studentRepository;

    @GetMapping("{id}")
    public Student findById(@PathVariable("id") Long id) {
        return studentRepository.findById(id).get();
    }
}

单元测试:ide

package com.mengyunzhi.auth_annotation_sample.controller;

import com.mengyunzhi.auth_annotation_sample.entity.Student;
import com.mengyunzhi.auth_annotation_sample.repository.StudentRepository;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.transaction.Transactional;

import static org.junit.Assert.*;

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@Transactional
public class StudentControllerTest {
    @Autowired
    StudentRepository studentRepository;
    @Autowired
    private MockMvc mockMvc;

    @Test
    public void findById() throws Exception {
        Student student = new Student();
        studentRepository.save(student);

        String url = "/Student/" + student.getId().toString();
        this.mockMvc
                .perform(MockMvcRequestBuilders
                        .get(url))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }
}

单元测试经过,并返回了相应的学生实体信息.

MockHttpServletRequest:
      HTTP Method = GET
      Request URI = /Student/1
       Parameters = {}
          Headers = {}
             Body = null
    Session Attrs = {}

Handler:
             Type = com.mengyunzhi.auth_annotation_sample.controller.StudentController
           Method = public com.mengyunzhi.auth_annotation_sample.entity.Student com.mengyunzhi.auth_annotation_sample.controller.StudentController.findById(java.lang.Long)

Async:
    Async started = false
     Async result = null

Resolved Exception:
             Type = null

ModelAndView:
        View name = null
             View = null
            Model = null

FlashMap:
       Attributes = null

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = {Content-Type=[application/json;charset=UTF-8]}
     Content type = application/json;charset=UTF-8
             Body = {"id":1,"name":null,"clazz":null}
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

实现

思想:(1) 为 findById(Long id)方法增长注解,在注解中,将 StudentRepository传入注解。 (2)切面中,获取 StudentRepository对应的 Bean,利用其 findById获取相应的实体。(3)获取实体上的 教师, 并与当前 登陆教师作比对。相同,有权限;不相同,无权限,抛出异常。

建立注解

package com.mengyunzhi.auth_annotation_sample.annotation;

import org.springframework.beans.factory.annotation.Required;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 申请表单权限认证
 * @author panjie
 */
@Target({ElementType.METHOD})       // 方法注解
@Retention(RetentionPolicy.RUNTIME) // 在运行时生效
public @interface AuthorityAnnotation {
    // 仓库名称
    @Required
    Class repository();
}

创建aspect

package com.mengyunzhi.auth_annotation_sample.aspect;

import com.mengyunzhi.auth_annotation_sample.annotation.AuthorityAnnotation;
import com.mengyunzhi.auth_annotation_sample.entity.YunZhiEntity;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;

import java.util.Optional;


@Aspect
@Component
public class AuthorityAspect implements ApplicationContextAware{
    private final static Logger logger = LoggerFactory.getLogger(AuthorityAspect.class);
    private ApplicationContext applicationContext = null;           // spring上下文,用于使用spring获取Bean


    // 定义切点。使用 && 来获取多个参数,使用@annotation(authorityAnnotation)来获取authorityAnnotation注解
    @Pointcut("@annotation(com.mengyunzhi.auth_annotation_sample.annotation.AuthorityAnnotation) && args(id,..) && @annotation(authorityAnnotation)")
    public void doAccessCheck(Object id, AuthorityAnnotation authorityAnnotation) {
    }

    @Before("doAccessCheck(id, authorityAnnotation)")
    public void before(Object id, AuthorityAnnotation authorityAnnotation) {
        logger.debug("获取注解上的repository, 并经过applicationContext来获取bean");
        Class<?> repositoryClass = authorityAnnotation.repository();
        Object object = applicationContext.getBean(repositoryClass);

        logger.debug("将Bean转换为CrudRepository");
        CrudRepository<YunZhiEntity, Object> crudRepository = (CrudRepository<YunZhiEntity, Object>)object;

        logger.debug("获取实体对象");
        Optional<YunZhiEntity> yunZhiEntityOptional = crudRepository.findById(id);
        if(!yunZhiEntityOptional.isPresent()) {
            throw new RuntimeException("对不起,未找到相关的记录");
        }
        YunZhiEntity yunZhiEntity = yunZhiEntityOptional.get();
    }

    /**
     * 将应用上下文绑定到私有变量中
     * @param applicationContext
     * @throws BeansException
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

此时,咱们发现还须要两上功能的支持。

  1. 获取当前登陆教师。
  2. 获取当前实体对应的拥有教师。

获取当前登陆教师

新建TeacherService并实现,同时增长setCurrentLoginTeachergetCurrentLoginTeacher方法。

package com.mengyunzhi.auth_annotation_sample.service;

import com.mengyunzhi.auth_annotation_sample.entity.Teacher;
import org.springframework.stereotype.Service;

@Service
public class TeacherServiceImpl implements TeacherService {
    private Teacher currentLoginTeacher;
    @Override
    public Teacher getCurrentLoginTeacher() {
        return this.currentLoginTeacher;
    }

    @Override
    public void setCurrentLoginTeacher(Teacher teacher) {
        this.currentLoginTeacher = teacher;
    }
}

获取当前实体对应的拥有教师

package com.mengyunzhi.auth_annotation_sample.entity;

public interface YunZhiEntity {
    Teacher getBelongToTeacher();
}
package com.mengyunzhi.auth_annotation_sample.entity;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;

@Entity
public class Student implements YunZhiEntity {
    @Id @GeneratedValue
    private Long id;
    private String name;
    @ManyToOne
    private Teacher  teacher;
    @ManyToOne private Clazz clazz;

    // setter and getter

    @Override
    public Teacher getBelongToTeacher() {
        return this.getTeacher();
    }
}

完善aspect

logger.debug("获取实体对象");
        Optional<YunZhiEntity> yunZhiEntityOptional = crudRepository.findById(id);
        if(!yunZhiEntityOptional.isPresent()) {
            throw new RuntimeException("对不起,未找到相关的记录");
        }
        YunZhiEntity yunZhiEntity = yunZhiEntityOptional.get();

        logger.debug("获取登陆教师以及拥有者,并进行比对");
        Teacher belongToTeacher  = yunZhiEntity.getBelongToTeacher();
        Teacher currentLoginTeacher = teacherService.getCurrentLoginTeacher();
        if (currentLoginTeacher != null && belongToTeacher != null) {
            if (!belongToTeacher.getId().equals(currentLoginTeacher.getId())) {
                throw new RuntimeException("权限不容许");
            }
        }

测试

  • 为方法添加注解。
@AuthorityAnnotation(repository = StudentRepository.class)
    @GetMapping("{id}")
    public Student findById(@PathVariable("id") Long id) {
        return studentRepository.findById(id).get();
    }
  • 组织测试
package com.mengyunzhi.auth_annotation_sample.controller;

import com.mengyunzhi.auth_annotation_sample.entity.Student;
import com.mengyunzhi.auth_annotation_sample.entity.Teacher;
import com.mengyunzhi.auth_annotation_sample.repository.StudentRepository;
import com.mengyunzhi.auth_annotation_sample.repository.TeacherRepository;
import com.mengyunzhi.auth_annotation_sample.service.TeacherService;
import org.assertj.core.api.Assertions;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;

import javax.transaction.Transactional;

import static org.junit.Assert.*;

@SpringBootTest
@RunWith(SpringRunner.class)
@AutoConfigureMockMvc
@Transactional
public class StudentControllerTest {
    private final static Logger logger = LoggerFactory.getLogger(StudentControllerTest.class);
    @Autowired
    StudentRepository studentRepository;
    @Autowired
    private MockMvc mockMvc;
    @Autowired
    TeacherService teacherService;
    @Autowired
    TeacherRepository teacherRepository;

    @Test
    public void findById() throws Exception {
        logger.info("建立两个教师");
        Teacher teacher0 = new Teacher();
        teacherRepository.save(teacher0);
        Teacher teacher1 = new Teacher();
        teacherRepository.save(teacher0);

        logger.debug("建立一个学生,并指明它属于教师0");
        Student student = new Student();
        student.setTeacher(teacher0);
        studentRepository.save(student);

        logger.debug("当前登陆为teacher1,断言发生异常");
        teacherService.setCurrentLoginTeacher(teacher1);
        String url = "/Student/" + student.getId().toString();
        Boolean catchException = false;
        try {
            this.mockMvc
                    .perform(MockMvcRequestBuilders
                            .get(url))
                    .andExpect(MockMvcResultMatchers.status().isOk())
                    .andDo(MockMvcResultHandlers.print());
        } catch (Exception e) {
            catchException = true;
            Assertions.assertThat(e.getMessage()).endsWith("权限不容许");
        }
        Assertions.assertThat(catchException).isTrue();

        logger.debug("当前登陆为teacher0,断言正常");
        teacherService.setCurrentLoginTeacher(teacher0);
        this.mockMvc
                .perform(MockMvcRequestBuilders
                        .get(url))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andDo(MockMvcResultHandlers.print());
    }
}

clipboard.png

测试经过

总结

有了上述思想,相信咱们一样能够处理一些复杂的拥有者权限。

好比:学生直接属于班级,而班级才属于某个教师呢?此时,权限判断怎么写?
答案是,咱们在学生实体的 getBelongToTeacher()方法中:
return this.getKlazz().getBelongToTeacher();
好比:学生的信息除了班主任能看之外,全部的角色为“校领导”的教师也所有能看。

这时候,咱们能够这样:

  1. 创建一个接口,在这个接口中,增长一个校验当前登陆用户拥有权限的方法。
  2. 建立当前接口的实现类,其它的service则继承这个实现类。该实类中的实现判断功能:学生的信息除了班主任能看之外,全部的角色为“校领导”的教师也所有能看。
  3. 若是有些service的权限验证须要定制,则重写实现类的校验当前登陆用户拥有权限的方法。
  4. service注入到相应的如findById(Long id)方法中。
  5. Aspect中调用接口中定义的校验当前登陆用户拥有权限,返回false,抛出权限异常。

若是偏偏你也在河工,请留言加入咱们吧。
相关文章
相关标签/搜索