2
提示:本文中,我们只给了部分示例代码。
如果你需要完整的代码,请点击:https://github.com/mengyunzhi/authAnnotationSample

在实际的项目中,我们的往往会有这样的问题。
比如简单的学生、班级、教师三个实体。班级1属于教师1,班级2属于教师2。教师1登录后,我们在班级列表中,展现班级1。点击班级1后,我们获取后台:/Clazz/1来获取这个班级的信息。但是,如果用户直接在网络中,修改请求地址,比如修改为 /Class/2来获取班级为2的信息,由于我们并没有做权限处理,则直接将班级2的信息返回给了只有班级1权限的教师1。
我们把上述问题称为:拥有者权限认证问题。

其它解决方法

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

本文解决方法

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

数据准备

初始化

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

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

比如:

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接口。

package com.mengyunzhi.auth_annotation_sample.entity;

public interface YunZhiEntity {
}

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

@Entity
public class Clazz implements YunZhiEntity {

建立控制器及方法

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

例:

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();
    }
}

单元测试:

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,抛出权限异常。

如果恰恰你也在河工,请留言加入我们吧。

潘杰
3.1k 声望239 粉丝