提示:
本文中,咱們只給了部分示例代碼。
若是你須要完整的代碼,請點擊: https://github.com/mengyunzhi/authAnnotationSample
在實際的項目中,咱們的每每會有這樣的問題。
好比簡單的學生、班級、教師三個實體。班級1屬於教師1,班級2屬於教師2。教師1登陸後,咱們在班級列表中,展示班級1。點擊班級1後,咱們獲取後臺:/Clazz/1
來獲取這個班級的信息。可是,若是用戶直接在網絡中,修改請求地址,好比修改成 /Class/2
來獲取班級爲2的信息,因爲咱們並無作權限處理,則直接將班級2的信息返回給了只有班級1權限的教師1。
咱們把上述問題稱爲:擁有者權限認證問題。java
findById()
方法中,使用邏輯判斷。ClazzController::findById(Long id)
添加註解,在註解中獲取傳入的ID
,並就此來判斷當前登陸用戶是否擁有相關權限。本文將闡述如何利用統一的解決方案,來實現使用一個註解,進行達到驗證某個登陸的教師是否有班級查看權限,是否有學生查看權限等多個實體查看、刪除、更新權限的功能。git
初始化三個實體:學生、班級、教師。
學生:班級 = 0..n : 1
班級:教師 = 0..n : 1
其中,教師、班級的關鍵字類型爲Integer
,學生實體的爲long
github
同時,創建三個表對應的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(); }
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; } }
此時,咱們發現還須要兩上功能的支持。
新建TeacherService
並實現,同時增長setCurrentLoginTeacher
和getCurrentLoginTeacher
方法。
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(); } }
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()); } }
測試經過
有了上述思想,相信咱們一樣能夠處理一些複雜的擁有者權限。
好比:學生直接屬於班級,而班級才屬於某個教師呢?此時,權限判斷怎麼寫?
答案是,咱們在學生實體的getBelongToTeacher()
方法中:
return this.getKlazz().getBelongToTeacher();
好比:學生的信息除了班主任能看之外,全部的角色爲「校領導」的教師也所有能看。
這時候,咱們能夠這樣:
校驗當前登陸用戶擁有權限
的方法。service
則繼承這個實現類。該實類中的實現判斷功能:學生的信息除了班主任能看之外,全部的角色爲「校領導」的教師也所有能看。service
的權限驗證須要定製,則重寫實現類的校驗當前登陸用戶擁有權限
的方法。service
注入到相應的如findById(Long id)
方法中。Aspect
中調用接口中定義的校驗當前登陸用戶擁有權限
,返回false
,拋出權限異常。若是偏偏你也在河工,請留言加入咱們吧。