Spring Boot + Spring Cloud 实现权限管理系统 权限控制(Shiro 注解)
技术背景
当前,我们基于导航菜单的显示和操作按钮的禁用状态,实现了页面可见性和操作可用性的权限验证,或者叫访问控制。但这仅限于页面的显示和操作,我们的后台接口还是没有进行权限的验证,只要知道了后台的接口信息,就可以直接通过swagger或自行发送ajax请求成功调用后台接口,这是非常危险的。接下来,我们就基于Shiro的注解式权限控制方案,来给我们的后台接口提供权限保护。
权限注解
Shiro总共有5个权限注解,实现了不同的权限控制策略。
RequiresPermissions
当前Subject需要拥有某些特定的权限时,才能执行被该注解标注的方法。如果当前Subject不具有这样的权限,则方法不会被执行。
这是基于资源权限方式的权限控制主要方案,也是我们项目中进行权限控制使用的注解方案。
RequiresRoles
当前Subject必须拥有所有指定的角色时,才能访问被该注解标注的方法。如果当天Subject不同时拥有所有指定角色,则方法不会执行还会抛出AuthorizationException异常。
RequiresUser
当前Subject必须是应用的用户,才能访问或调用被该注解标注的类,实例,方法。
RequiresAuthentication
使用该注解标注的类,实例,方法在访问或调用时,当前Subject必须在当前session中已经过认证。
RequiresGuest
使用该注解标注的类,实例,方法在访问或调用时,当前Subject可以是“gust”身份,不需要经过认证或者在原先的session中存在记录。
注解优先级
Shiro的认证注解处理具有内定处理顺序,如有多个注解,会按照下面优先级逐个检查,只有所有检查通过才允许访问:
- RequiresRoles
- RequiresPermissions
- RequiresAuthentication
- RequiresUser
- RequiresGuest
代码实现
添加配置
打开kitty-admin工程,找到shiro配置类。添加如下内容,主要作用是开启Shiro的权限注解。
Shiro通过AOP方式拦截被权限注解的类或方法,然后匹配权限注解值和用户权限列表进行验证。
ShiroConfig.java

/**
* Shiro生命周期处理器
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
} /**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
} @Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}

添加注解
以菜单管理接口为例,添加 @RequiresPermissions("权限标识") 标识即可。
这个权限标识就是我们的菜单表中对应的权限标识字段(perms)对应的值。
SysMenuController.java

package com.louis.kitty.admin.controller; import java.util.List; import org.apache.shiro.authz.annotation.RequiresPermissions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import com.louis.kitty.admin.model.SysMenu;
import com.louis.kitty.admin.sevice.SysMenuService;
import com.louis.kitty.core.http.HttpResult; /**
* 菜单控制器
* @author Louis
* @date Oct 29, 2018
*/
@RestController
@RequestMapping("menu")
public class SysMenuController { @Autowired
private SysMenuService sysMenuService; @RequiresPermissions({"sys:menu:add", "sys:menu:edit"})
@PostMapping(value="/save")
public HttpResult save(@RequestBody SysMenu record) {
return HttpResult.ok(sysMenuService.save(record));
} @RequiresPermissions("sys:menu:delete")
@PostMapping(value="/delete")
public HttpResult delete(@RequestBody List<SysMenu> records) {
return HttpResult.ok(sysMenuService.delete(records));
} @RequiresPermissions("sys:menu:view")
@GetMapping(value="/findNavTree")
public HttpResult findNavTree(@RequestParam String userName) {
return HttpResult.ok(sysMenuService.findTree(userName, 1));
} @RequiresPermissions("sys:menu:view")
@GetMapping(value="/findMenuTree")
public HttpResult findMenuTree() {
return HttpResult.ok(sysMenuService.findTree(null, 0));
}
}

测试效果
启动服务,通过Swagger分别使用超级管理员和测试人员角色账户访问接口,发现admin可以正常访问,无权限的账户访问返回如下权限验证失败信息。

{
"timestamp": "2018-11-19T07:58:21.532+0000",
"status": 500,
"error": "Internal Server Error",
"message": "Subject does not have permission [sys:menu:view]",
"path": "/menu/findMenuTree"
}

原理剖析
首先在Shiro配置的时候,我们配置了一个 AuthorizationAttributeSourceAdvisor 类。

/**
* Shiro生命周期处理器
*/
@Bean
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
} /**
* 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
* 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
*/
@Bean
@DependsOn({"lifecycleBeanPostProcessor"})
public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
advisorAutoProxyCreator.setProxyTargetClass(true);
return advisorAutoProxyCreator;
} @Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
authorizationAttributeSourceAdvisor.setSecurityManager(securityManager());
return authorizationAttributeSourceAdvisor;
}

在 AuthorizationAttributeSourceAdvisor 类中,我们看到了有关五个权限注解的信息,以及关联一个拦截器 AopAllianceAnnotationsAuthorizingMethodInterceptor。

public class AuthorizationAttributeSourceAdvisor extends StaticMethodMatcherPointcutAdvisor {private static final Class<? extends Annotation>[] AUTHZ_ANNOTATION_CLASSES = new Class[] {
RequiresPermissions.class, RequiresRoles.class,
RequiresUser.class, RequiresGuest.class, RequiresAuthentication.class
};
...
public AuthorizationAttributeSourceAdvisor() {
setAdvice(new AopAllianceAnnotationsAuthorizingMethodInterceptor());
}
}

在 AopAllianceAnnotationsAuthorizingMethodInterceptor 中,我们看到了关联了五种权限控制注解对象的拦截器,这样在添加了权限注解的方法被调用时,就会被对应的拦截器拦截,并进行相关的权限验证。

public class AopAllianceAnnotationsAuthorizingMethodInterceptor
extends AnnotationsAuthorizingMethodInterceptor implements MethodInterceptor { public AopAllianceAnnotationsAuthorizingMethodInterceptor() {
List<AuthorizingAnnotationMethodInterceptor> interceptors =
new ArrayList<AuthorizingAnnotationMethodInterceptor>(5);
//use a Spring-specific Annotation resolver - Spring's AnnotationUtils is nicer than the
//raw JDK resolution process.
AnnotationResolver resolver = new SpringAnnotationResolver();
//we can re-use the same resolver instance - it does not retain state:
interceptors.add(new RoleAnnotationMethodInterceptor(resolver));
interceptors.add(new PermissionAnnotationMethodInterceptor(resolver));
interceptors.add(new AuthenticatedAnnotationMethodInterceptor(resolver));
interceptors.add(new UserAnnotationMethodInterceptor(resolver));
interceptors.add(new GuestAnnotationMethodInterceptor(resolver)); setMethodInterceptors(interceptors);
}

接口被调用时,AOP拦截器 AopAllianceAnnotationsAuthorizingMethodInterceptor 的invoke方法被调用。
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
org.apache.shiro.aop.MethodInvocation mi = createMethodInvocation(methodInvocation);
return super.invoke(mi);
}
调用父类 AuthorizingMethodInterceptor 的 invoke 方法。
public Object invoke(MethodInvocation methodInvocation) throws Throwable {
assertAuthorized(methodInvocation);
return methodInvocation.proceed();
}
调用 AopAllianceAnnotationsAuthorizingMethodInterceptor 的 assertAuthorized 方法。

protected void assertAuthorized(MethodInvocation methodInvocation) throws AuthorizationException {
//default implementation just ensures no deny votes are cast:
Collection<AuthorizingAnnotationMethodInterceptor> aamis = getMethodInterceptors();
if (aamis != null && !aamis.isEmpty()) {
for (AuthorizingAnnotationMethodInterceptor aami : aamis) {
if (aami.supports(methodInvocation)) {
aami.assertAuthorized(methodInvocation);
}
}
}
}

调用 AuthorizingAnnotationMethodInterceptor 的 assertAuthorized 方法。

public void assertAuthorized(MethodInvocation mi) throws AuthorizationException {
try {
((AuthorizingAnnotationHandler)getHandler()).assertAuthorized(getAnnotation(mi));
}
catch(AuthorizationException ae) {
...
}
}

调用 PermissionAnnotationHandler 的 assertAuthorized 方法。

public void assertAuthorized(Annotation a) throws AuthorizationException {
if (!(a instanceof RequiresPermissions)) return;
RequiresPermissions rpAnnotation = (RequiresPermissions) a;
String[] perms = getAnnotationValue(a);
Subject subject = getSubject();
if (perms.length == 1) {
subject.checkPermission(perms[0]);
return;
}
...
}

调用 DelegatingSubject 的 checkPermission方法。
public void checkPermission(String permission) throws AuthorizationException {
assertAuthzCheckPossible();
securityManager.checkPermission(getPrincipals(), permission);
}
调用 AuthorizingSecurityManager 的 checkPermission方法。
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
this.authorizer.checkPermission(principals, permission);
}
调用 ModularRealmAuthorizer 的 checkPermission方法。
public void checkPermission(PrincipalCollection principals, String permission) throws AuthorizationException {
assertRealmsConfigured();
if (!isPermitted(principals, permission)) {
throw new UnauthorizedException("Subject does not have permission [" + permission + "]");
}
}

public boolean isPermitted(PrincipalCollection principals, String permission) {
assertRealmsConfigured();
for (Realm realm : getRealms()) {
if (!(realm instanceof Authorizer)) continue;
if (((Authorizer) realm).isPermitted(principals, permission)) {
return true;
}
}
return false;
}

调用 AuthorizingRealm 的 isPermitted方法。
public boolean isPermitted(PrincipalCollection principals, String permission) {
Permission p = getPermissionResolver().resolvePermission(permission);
return isPermitted(principals, p);
}
public boolean isPermitted(PrincipalCollection principals, Permission permission) {
AuthorizationInfo info = getAuthorizationInfo(principals);
return isPermitted(permission, info);
}

protected AuthorizationInfo getAuthorizationInfo(PrincipalCollection principals) {
...
if (info == null) {
// Call template method if the info was not found in a cache
info = doGetAuthorizationInfo(principals);
...
}
return info;
}

调用我们自定义的 OAuth2Realm 的 doGetAuthorizationInfo 方法,也是返回自定义权限验证的逻辑。

/**
* 授权(接口保护,验证接口调用权限时调用)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
SysUser user = (SysUser)principals.getPrimaryPrincipal();
// 用户权限列表,根据用户拥有的权限标识与如 @permission标注的接口对比,决定是否可以调用接口
Set<String> permsSet = sysUserService.findPermissions(user.getName());
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
return info;
}

AuthorizingRealm 查询到用户权限信息,将注解权限值跟用户权限信息列表进行匹配,决定权限验证是否通过。

protected boolean isPermitted(Permission permission, AuthorizationInfo info) {
Collection<Permission> perms = getPermissions(info);
if (perms != null && !perms.isEmpty()) {
for (Permission perm : perms) {
if (perm.implies(permission)) {
return true;
}
}
}
return false;
}

到这里,关于Shiro注解式权限控制方案的配置和执行流程就剖析的差不多了。
Spring Boot + Spring Cloud 实现权限管理系统 权限控制(Shiro 注解)的更多相关文章
- 新书上线:《Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统》,欢迎大家买回去垫椅子垫桌脚
新书上线 大家好,笔者的新书<Spring Boot+Spring Cloud+Vue+Element项目实战:手把手教你开发权限管理系统>已上线,此书内容充实.材质优良,乃家中必备垫桌脚 ...
- [权限管理系统(四)]-spring boot +spring security短信认证+redis整合
[权限管理系统]spring boot +spring security短信认证+redis整合 现在主流的登录方式主要有 3 种:账号密码登录.短信验证码登录和第三方授权登录,前面一节Sprin ...
- Spring Boot(十四):spring boot整合shiro-登录认证和权限管理
Spring Boot(十四):spring boot整合shiro-登录认证和权限管理 使用Spring Boot集成Apache Shiro.安全应该是互联网公司的一道生命线,几乎任何的公司都会涉 ...
- 部署spring boot + Vue遇到的坑(权限、刷新404、跨域、内存)
部署spring boot + Vue遇到的坑(权限.刷新404.跨域.内存) 项目背景是采用前后端分离,前端使用vue,后端使用springboot. 工具 工欲善其事必先利其器,我们先找一个操作L ...
- spring Boot+spring Cloud实现微服务详细教程第二篇
上一篇文章已经说明了一下,关于spring boot创建maven项目的简单步骤,相信很多熟悉Maven+Eclipse作为开发常用工具的朋友们都一目了然,这篇文章主要讲解一下,构建spring bo ...
- spring Boot+spring Cloud实现微服务详细教程第一篇
前些天项目组的大佬跟我聊,说项目组想从之前的架构上剥离出来公用的模块做微服务的开发,恰好去年的5/6月份在上家公司学习了国内开源的dubbo+zookeeper实现的微服务的架构.自己平时对微服务的设 ...
- Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台
Cola Cloud 基于 Spring Boot, Spring Cloud 构建微服务架构企业级开发平台: https://gitee.com/leecho/cola-cloud
- spring boot、cloud v2.1.0.RELEASE 使用及技术整理
2018年10月30日 springboot v2.1.0.RELEASE 发布: https://github.com/spring-projects/spring-boot/releases/ta ...
- Spring Boot/Spring Cloud、ESB、Dubbo
如何使用Spring Boot/Spring Cloud 实现微服务应用spring Cloud是一个基于Spring Boot实现的云应用开发工具,它为基于JVM的云应用开发中的配置管理.服务发现. ...
- 使用Spring Boot,Spring Cloud和Docker实现微服务架构
https://github.com/sqshq/PiggyMetrics Microservice Architecture with Spring Boot, Spring Cloud a ...
随机推荐
- MFC 的资源文件 就是那个后缀名是 .rc的那个
参考: http://blog.csdn.net/zgrjkflmkyc/article/details/16897881 http://www.oschina.net/question/565065 ...
- Tomcat出现The origin server did not find a current representation for the target resourc...
访问页面出现404 解决方法: https://blog.csdn.net/dbc_121/article/details/79204340 我的问题主要还是在tomcat调整上, 对了,关于loca ...
- ECMAScript6 入门教程 初学记录let命令 块级作用域
一.基本语法-let命令 (1)ES6新增了let命令,用来声明变量.所声明的变量,只在let命令所在的代码块内有效. 循环的计数器,就很合适使用let命令.计数器i只在for循环体内有效,在循环体外 ...
- supervisor 守护进程
一.supervisor 安装 1.yum -y install epel-release 2.yum -y install supervisor 二.supervisor 配置文件详解 三.supe ...
- 给学习立个flag
今天是2018年7月7号,此时的砖相比昨天格外烫手,望着手套因被磨破而露出来的半截手指头,一股股热浪溜溜的从指间划过,背后还有小山一样高的砖头,感觉对面today店里的冰镇西瓜又成了不可奢望的梦... ...
- Solr入门介绍
solr入门案例 solr是apache下的一个全文检索引擎系统. 我们需要在服务器上单独去部署solr, 通过它的客户端工具包solrJ, 就是一个 jar包, 集成到我们项目中来调用服务器中 ...
- 基于Vue的Quasar Framework 介绍 这个框架UI组件很全面
基于Vue的Quasar Framework 介绍 这个框架UI组件很全面 基于Vue的Quasar Framework 中文网http://www.quasarchs.com/ quasarfram ...
- ArrayList去重
1.利用HashSet(不保证元素顺序一致) HashSet不会存在相同的元素,可以利用这一点去除List中的重复元素 List<String> beforeList = new Arra ...
- CentOS7查看systemctl 控制的服务的相关配置
例如,启动配置文件 [root@Docker_Machine_192.168.31.130 ~]# systemctl show --property=FragmentPath docker Frag ...
- Luogu P1892 P1525 团伙 关押罪犯
(怎么都是抓罪犯 怪不得写法差不多) 团伙 关押罪犯 并查集.以"敌人的敌人是朋友"的思路来处理.所以增加一个e/E数组来存储敌人. 关押罪犯还用到了贪心的思路.将冲突值从大到小排 ...