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,拋出權限異常。

若是偏偏你也在河工,請留言加入咱們吧。
相關文章
相關標籤/搜索