根据个人上一篇文章,权限设计的杂谈中,涉及到了有关于先后端分离中,页面和api接口断开表与表层面的关联,另辟蹊径从其余角度找到方式进行关联。这里咱们主要采起了shiro的自定义注解的方案。本篇文章主要解决如下的问题。java
在表与表的结构关系中,页面和接口表最终都是与权限表进行的关联(详情请查看个人上一篇文章《权限设计的杂谈》)。 数据库
如今提出了这两个概念,他们最终的实际的使用方式是什么,咱们先从如下几个角度去思考一下。apache
咱们从结论出发来看这几个问题,首先“控制对象”的存储除了在数据库中也能够代码中,也能够在配置文件中,并不必定非得在数据库;那么接着回答第二个问题,当数据库存在的接口信息,而服务端并无开发这个接口的时候,数据库的信自己就有问题,亦或者,数据库里新增的接口一定是服务端上已经部署的接口才能生效;接着就是第一个问题,那么数据库中关于“控制对象”的表中的数据并不必定是真实有效的。因此咱们能够得出如下的解决方案后端
可是这种方案仅适用于非强控制接口型的项目,在强控制型的接口项目仍然要将页面与接口进行绑定,虽然这会带来巨大的运维成本。另外也能够经过接口路由规则进行划分,例如:/api/page/xxxx/(仅对页面使用),/api/mobile/xxxxx(仅对移动端使用)将仅供页面使用的接口进行分类,这类接口仅作认证不作受权,也能够达到目的。api
经过一个理论上的思路承认以后,剩下的则是付诸技术上的实践,咱们这边采用的是Apache Shiro的安全框架,在Spring Boot的环境下应用。简要说明如下几个shiro的注解。安全
注解名 | 做用 |
---|---|
@RequiresAuthentication | 做用于的类、方法、实例上。调用时,当前的subject是必须通过了认证的。 |
@RequiresGuest | 做用于的类、方法、实例上。调用时,subject能够是guest状态。 |
@RequiresPermissions | 做用于的类、方法、实例上。调用时,须要判断suject中是否包含当前接口中的Permission(权限信息)。 |
@RequiresRoles | 做用于的类、方法、实例上。调用时,须要判断subject中是否包含当前接口中的Role(角色信息)。 |
@RequiresUser | 做用于的类、方法、实例上。调用时,须要判断subject中是否当前应用中的用户。 |
/** * 1.当前接口须要通过"认证"过程 * @return */
@RequestMapping(value = "/info",method = RequestMethod.GET)
@RequiresAuthentication
public String test(){
return "恭喜你,拿到了参数信息";
}
/** * 2.1.当前接口须要通过权限校验(需包含 角色的查询 或 菜单的查询) * @return */
@RequestMapping(value = "/info",method = RequestMethod.GET)
@RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
public String test(){
return "恭喜你,拿到了参数信息";
}
/** * 2.2.当前接口须要通过权限校验(需包含 角色的查询 与 菜单的查询) * @return */
@RequestMapping(value = "/info",method = RequestMethod.GET)
@RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
public String test(){
return "恭喜你,拿到了参数信息";
}
/** * 3.1.当前接口须要通过角色校验(需包含admin的角色) * @return */
@RequestMapping(value = "/info",method = RequestMethod.GET)
@RequiresRoles(value={"admin"})
public String test(){
return "恭喜你,拿到了参数信息";
}
/** * 3.2.当前接口须要通过角色与权限的校验(需包含admin的角色,以及角色的查询 或 菜单的查询) * @return */
@RequestMapping(value = "/info",method = RequestMethod.GET)
@RequiresRoles(value={"admin"})
@RequiresPermissions(value={"role:search","menu":"search"},logical=Logical.OR)
public String test(){
return "恭喜你,拿到了参数信息";
}
复制代码
在咱们的实际使用过程当中,实际上只须要使用@RequiresPermissions和@RequiresAuthentication就能够了这一个注解就能够了,在上一小节的结尾,咱们采起了业务模块与操做的结合方案来解耦页面和api接口的关系,和apache Shiro的这种方式正好一致。可是@RequiresRoles这个咱们尽量不采用,由于角色的组合形式太多,角色名没有办法在接口中具象惟一化(很难指定接口归某个角色调用,可是必定能知道接口归属于某些业务模块的某些操做。)app
如今咱们来回顾一下整个运转的流程。框架
可是仅仅是拥有shiro中的这5个注解确定是不够使用的。在实际的使用过程当中,根据需求,咱们会在权限认证中加入咱们本身特有的业务逻辑的,咱们为了便捷则能够采用自定义注解的方式进行使用。这种方法不只仅适用于Apache Shiro,不少其余的框架如:Hibernate Validator、SpringMVC、甚至咱们能够写一套校验体系,在aop中去验证权限,这都是没问题的。因此自定义注解的做用很广。可是在这里,我仅仅基于shiro的来实现适用于它的自定义注解。前后端分离
/** * 用于认证的接口的注解,组合形式默认是“或”的关系 */
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Auth {
/** * 业务模块 * @return */
String[] module();
/** * 操做类型 */
String[] action();
}
复制代码
/** * Auth注解的操做类 */
public class AuthHandler extends AuthorizingAnnotationHandler {
public AuthHandler() {
//写入注解
super(Auth.class);
}
@Override
public void assertAuthorized(Annotation a) throws AuthorizationException {
if (a instanceof Auth) {
Auth annotation = (Auth) a;
String[] module = annotation.module();
String[] action = annotation.action();
//1.获取当前主题
Subject subject = this.getSubject();
//2.验证是否包含当前接口的权限有一个经过则经过
boolean hasAtLeastOnePermission = false;
for(String m:module){
for(String ac:action){
//使用hutool的字符串工具类
String permission = StrFormatter.format("{}:{}",m,ac);
if(subject.isPermitted(permission)){
hasAtLeastOnePermission=true;
break;
}
}
}
if(!hasAtLeastOnePermission){
throw new AuthorizationException("没有访问此接口的权限");
}
}
}
}
复制代码
/** * 拦截器 */
public class AuthMethodInterceptor extends AuthorizingAnnotationMethodInterceptor {
public AuthMethodInterceptor() {
super(new AuthHandler());
}
public AuthMethodInterceptor(AnnotationResolver resolver) {
super(new AuthHandler(), resolver);
}
@Override
public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
// 验证权限
try {
((AuthHandler) this.getHandler()).assertAuthorized(getAnnotation(mi));
} catch (AuthorizationException ae) {
if (ae.getCause() == null) {
ae.initCause(new AuthorizationException("当前的方法没有经过鉴权: " + mi.getMethod()));
}
throw ae;
}
}
}
复制代码
/** * shiro的aop切面 */
public class AuthAopInterceptor extends AopAllianceAnnotationsAuthorizingMethodInterceptor {
public AuthAopInterceptor() {
super();
// 添加自定义的注解拦截器
this.methodInterceptors.add(new AuthMethodInterceptor(new SpringAnnotationResolver()));
}
}
复制代码
/** * 启动自定义注解 */
public class ShiroAdvisor extends AuthorizationAttributeSourceAdvisor {
public ShiroAdvisor() {
// 这里能够添加多个
setAdvice(new AuthAopInterceptor());
}
@SuppressWarnings({"unchecked"})
@Override
public boolean matches(Method method, Class targetClass) {
Method m = method;
if (targetClass != null) {
try {
m = targetClass.getMethod(m.getName(), m.getParameterTypes());
return this.isFrameAnnotation(m);
} catch (NoSuchMethodException ignored) {
}
}
return super.matches(method, targetClass);
}
private boolean isFrameAnnotation(Method method) {
return null != AnnotationUtils.findAnnotation(method, Auth.class);
}
}
复制代码
整体的思路顺序:定义注解类(定义业务可以使用的变量)->定义注解处理类(经过注解中的变量作业务逻辑处理)->定义注解的拦截器->定义aop的切面类->最后定义shiro的自定义注解启用类。其余的自定义的注解的编写思路和这个也是相似的。运维