案例一:
- 定义权限注解:
/**
* 角色的权限验证注解
*
* 被注解的方法必须满足指定的权限才能通过验证,如果有多个权限取 & 关系
* @author ruiclear
* @date 2020-08-12 18:19
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RoleAuthVerification {
/**
* 权限
* @return
*/
AuthPermissionEnum[] value();
}
- 定义权限枚举:
@AllArgsConstructor
@Getter
public enum AuthPermissionEnum {
ADMIN(1, "权限管理"),
BURY(2, "埋点管理"),
PARAMTER(3, "参数编辑");
private Integer id; //权限id
private String name; //权限
}
- 定义权限拦截器
/**
* 角色的权限验证拦截器
* @author ruiclear
* @date 2020-08-12 18:43
*/
@Aspect
@Component
public class RoleAuthenticationInterceptor extends BaseInterceptor {
@Autowired
private AuthService authService;
@Pointcut("@annotation(com.fenbi.pipe.princi.server.annotation.RoleAuthVerification)")
public void verification(){}
@Around("verification()")
public Object authVerification(ProceedingJoinPoint joinPoint){
try {
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
RoleAuthVerification annotation = method.getAnnotation(RoleAuthVerification.class);
if (annotation != null) {
String ldap = getUsername();
AuthPermissionEnum[] authPermissionEnums = annotation.value();
if(StringUtils.isNotBlank(ldap) && handleRoleAuthentication(authPermissionEnums, ldap)){
return joinPoint.proceed();
}
LOG.info("Ldap:{} no access", ldap);
unauthorizedHandle();
}
}catch (Throwable throwable){
LOG.error("Authentication verification failed",throwable);
//为了能让GlobalExceptionHandler捕获到异常
throw new RuntimeException("error");
}
return null;
}
/**
* 验证当前用户是否有权限,多个权限取 & 关系
* @param authPermissionEnums
* @param ldap
* @return true为有权限
*/
private boolean handleRoleAuthentication(AuthPermissionEnum[] authPermissionEnums, String ldap){
//根据用户名ldap通过authService服务接口查询用户的所有权限
List<Integer> permissions = authService.getPermissionsByLdap(ldap);
boolean flag =true;
for(AuthPermissionEnum authPermissionEnum: authPermissionEnums){
flag &= permissions.contains(authPermissionEnum.getId());
}
return flag;
}
/**
* 无权限时返回403
*/
private void unauthorizedHandle() throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setStatus(HttpStatus.FORBIDDEN.value());
OutputStream os = response.getOutputStream();
os.write("无权限访问".getBytes());
os.flush();
os.close();
}
}
- 定义权限表结构
- 用户表
DROP TABLE IF EXISTS `t_auth_user`;
create table t_auth_user
(
id int(11) auto_increment COMMENT '自增主键',
name varchar(50) not null COMMENT '用户名',
createTime timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updateTime timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_name` (`name`)
)ENGINE=InnoDB AUTO_INCREMENT=9156054 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统用户组表';
- 角色表
DROP TABLE IF EXISTS `t_auth_group`;
create table t_auth_group
(
id int(11) auto_increment COMMENT '自增主键',
name varchar(50) not null COMMENT '组名',
createTime timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updateTime timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_name` (`name`)
)ENGINE=InnoDB AUTO_INCREMENT=9156054 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统用户组表';
- 用户和角色关系表
DROP TABLE IF EXISTS `t_auth_group_ldap`;
create table t_auth_group_ldap
(
id int(11) auto_increment COMMENT '自增主键',
userId varchar(50) not null COMMENT '用户id',
groupId int(11) not null COMMENT '用户组id',
createTime timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updateTime timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_group_ldap` (`groupId`,`ldap`)
)ENGINE=InnoDB AUTO_INCREMENT=9156054 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统用户和用户组多对多关系维护表';
- 权限表
DROP TABLE IF EXISTS `t_auth_permission`;
create table t_auth_permission
(
id int(11) auto_increment COMMENT '自增主键',
name varchar(50) not null COMMENT '权限名',
createTime timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updateTime timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_name` (`name`)
)ENGINE=InnoDB AUTO_INCREMENT=9156054 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统用户组表';
- 权限和角色关系表
DROP TABLE IF EXISTS `t_auth_group_permission`;
create table t_auth_group_permission
(
id int(11) auto_increment COMMENT '自增主键',
groupId int(11) not null COMMENT '用户组id',
permissionId int(11) not null COMMENT '权限,1:权限管理,2:埋点管理,3:参数编辑',
createTime timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
updateTime timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
PRIMARY KEY (`id`),
UNIQUE KEY `index_group_permission` (`groupId`,`permissionId`)
)ENGINE=InnoDB AUTO_INCREMENT=9156054 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统用户组和功能权限多对多关系维护表';
- 在需要进行权限控制的colltroller接口上加上自定义注解,并指明权限
//表明只有登录用户同时拥有管理员和参数管理权限才有权限访问
@RoleAuthVerification(AuthPermissionEnum.ADMIN, AuthPermissionEnum.PARAMTER)
- 总结:这种注解配合枚举配合aop切面的设计,会使得之后需要增加,删除,更改接口权限时变得异常方便,只需要在对应的接口上加上对应权限的注解即可。
案例二:
同样的思路,不光是可以进行权限控制,还可以控制任何任何基于数据库存储的状态权限控制(例如:如果希望对系统中的任务在不同的角色不同的任务进度进行访问控制,也可以用该思路,在拦截器中获取方法参数中的任务id(最好统一任务id在参数中的位置)查询任务的进度和登录用户在该任务中的角色然后跟colltroller接口上加上自定义注解值进行对比,一致则可以通过权限验证,否则返回403)
- 定义任务权限注解:
/**
* 任务进度的验证注解
*
* 被注解的方法必须同时满足指定的role和progress才能通过验证
*
* 注意:为了更方便统一的获取任务id,约定被该注解注解的controller接口方法的第一个参数必须是任务id,否则IssueProgressVerificationInterceptor权限验证拦截器无效
* @author ruiclear
* @date 2020-08-12 18:19
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface IssueProgressVerification {
/**
* 角色
* @return
*/
TaskIssueRoleEnum role() default TaskIssueRoleEnum.DEFAULT;
/**
* 进度
* @return
*/
TaskIssueProgressEnum progress() default TaskIssueProgressEnum.DEFAULT;
}
- 定义任务权限枚举:
/**
* 任务角色
* @author ruiclear
* @date 2020-07-17 18:38
*/
@AllArgsConstructor
@Data
public enum TaskIssueRoleEnum {
DESIGNER(0, "分析师"), //分析师
DEVELOPER(1, "研发工程师"), //研发工程师
PRODUCER(2,"业务产品"), //业务产品
DEFAULT(-1, "NONE"); //默认
private int role;
private String name;
}
/**
* 任务进度
* @author ruiclear
* @date 2020-07-17 18:38
*/
@AllArgsConstructor
@Data
public enum TaskIssueProgressEnum {
DESIGN("设计",0), //设计
DEVELOP("开发",1), //开发
TEST("测试",2), //测试
ONLINE("上线",3), //上线
DEFAULT("NONE",-1);//默认
private int progress;
private String progressName;
}
- 定义处理拦截器:
/**
* 任务进度的验证拦截器
* @author ruiclear
* @date 2020-08-14 13:49
*/
@Aspect
@Component
public class IssueProgressVerificationInterceptor extends BaseInterceptor{
//用来查询数据库任务状态
@Autowired
private TaskIssueService taskIssueService;
//用来查询数据库任务关联的人员
@Autowired
private TaskPersonService taskPersonService;
@Pointcut("@annotation(com.fenbi.pipe.princi.server.annotation.IssueProgressVerification)")
public void verification(){}
@Around("verification()")
public Object issueProgressVerification(ProceedingJoinPoint joinPoint){
try{
String ldap = getUsername();
if (StringUtils.isBlank(ldap)){
unauthorizedHandle();
return null;
}
Method method = ((MethodSignature) joinPoint.getSignature()).getMethod();
IssueProgressVerification annotation = method.getAnnotation(IssueProgressVerification.class);
if(annotation != null){
TaskIssueProgressEnum progressEnum = annotation.progress();
TaskIssueRoleEnum roleEnum = annotation.role();
Integer issueId = parseIssueId(joinPoint);
if(handleVerification(ldap, issueId, progressEnum, roleEnum)){
return joinPoint.proceed();
}
unauthorizedHandle();
}
}catch (Throwable throwable){
LOG.error("Authentication verification failed",throwable);
//为了能让GlobalExceptionHandler捕获到异常
throw new RuntimeException("error");
}
return null;
}
/**
* 从方法参数中解析出issueId
* @param joinPoint
* @return
*/
private Integer parseIssueId(ProceedingJoinPoint joinPoint){
Object[] args = joinPoint.getArgs();
//为了更方便统一的获取任务id,约定被@IssueProgressVerification注解的controller接口方法的第一个参数必须是任务id,否则该权限验证拦截器无效
Integer issueId = Integer.parseInt(args[0].toString());
return issueId;
}
/**
* 校验是否有权限访问
* @param ldap
* @param issueId
* @param progressEnum
* @param roleEnum
* @return
*/
private boolean handleVerification(String ldap, Integer issueId, TaskIssueProgressEnum progressEnum, TaskIssueRoleEnum roleEnum){
Optional<TaskIssue> taskIssue = taskIssueService.getTaskIssueById(issueId);
if(!taskIssue.isPresent()){
LOG.info("任务:[{}]不存在", issueId);
return false;
}
Integer progress = taskIssue.get().getProgress();
Integer role = taskPersonService.getRoleByIssueIdAndLdap(issueId, ldap);
if(progressEnum.getValue() == progress && roleEnum.getValue() == role){
LOG.info("Ldap:[{}] of role:[{}] operation issueId:[{}] on progress:[{}] have access!", ldap, TaskIssueRoleEnum.getEnumByRole(role).getName(), issueId, TaskIssueProgressEnum.getEnumByRole(role).getValueName());
return true;
}
LOG.info("Ldap:[{}] of role:[{}] operation issueId:[{}] on progress:[{}] not have access!", ldap, TaskIssueRoleEnum.getEnumByRole(role).getName(), issueId, TaskIssueProgressEnum.getEnumByRole(role).getValueName());
return false;
}
/**
/**
* 无权限时返回403
*/
private void unauthorizedHandle() throws IOException {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletResponse response = attributes.getResponse();
response.setStatus(HttpStatus.FORBIDDEN.value());
OutputStream os = response.getOutputStream();
os.write("无权限访问".getBytes());
os.flush();
os.close();
}
}
- 在需要进行权限控制的colltroller接口上加上自定义注解,并指明权限
//表示该方法只是能登录用户在该任务的角色是设计师且任务进度是设计阶段时才有权限访问
@IssueProgressVerification(role = TaskIssueRoleEnum.DESIGNER, progress = TaskIssueProgressEnum.DESIGN)
//为了方便拦截器统一通用处理,强制约定需要进行任务权限控制得接口第一个参数必须是任务id
public Response addFrogTask(@PathVariable(value = "issueId") int issueId,
@RequestBody FrogTask frogTask){
LOG.info("任务:[{}]新增forg", issueId);
String ldap = getUsername();
return buryLogic.addFrogTask(ldap, frogTask) ? Response.ok() : Response.warn("新增失败");
}