提示:
本文中,咱们只给了部分示例代码。
若是你须要完整的代码,请点击: 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
,抛出权限异常。若是偏偏你也在河工,请留言加入咱们吧。