提示:
本文中,我们只给了部分示例代码。
如果你需要完整的代码,请点击:https://github.com/mengyunzhi/authAnnotationSample
在实际的项目中,我们的往往会有这样的问题。
比如简单的学生、班级、教师三个实体。班级1属于教师1,班级2属于教师2。教师1登录后,我们在班级列表中,展现班级1。点击班级1后,我们获取后台:/Clazz/1
来获取这个班级的信息。但是,如果用户直接在网络中,修改请求地址,比如修改为 /Class/2
来获取班级为2的信息,由于我们并没有做权限处理,则直接将班级2的信息返回给了只有班级1权限的教师1。
我们把上述问题称为:拥有者权限认证问题。
其它解决方法
- 最简单的思想,当然是
findById()
方法中,使用逻辑判断。 - 当然我们也可以为
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;
}
}
此时,我们发现还需要两上功能的支持。
- 获取当前登录教师。
- 获取当前实体对应的拥有教师。
获取当前登录教师
新建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();
}
}
完善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());
}
}
测试通过
总结
有了上述思想,相信我们同样可以处理一些复杂的拥有者权限。
比如:学生直接属于班级,而班级才属于某个教师呢?此时,权限判断怎么写?
答案是,我们在学生实体的getBelongToTeacher()
方法中:
return this.getKlazz().getBelongToTeacher();
比如:学生的信息除了班主任能看以外,所有的角色为“校领导”的教师也全部能看。
这时候,我们可以这样:
- 建立一个接口,在这个接口中,增加一个
校验当前登录用户拥有权限
的方法。 - 创建当前接口的实现类,其它的
service
则继承这个实现类。该实类中的实现判断功能:学生的信息除了班主任能看以外,所有的角色为“校领导”的教师也全部能看。 - 如果有些
service
的权限验证需要定制,则重写实现类的校验当前登录用户拥有权限
的方法。 - 将
service
注入到相应的如findById(Long id)
方法中。 - 在
Aspect
中调用接口中定义的校验当前登录用户拥有权限
,返回false
,抛出权限异常。
如果恰恰你也在河工,请留言加入我们吧。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。