在线演示
演示地址: http://139.196.87.48:9002/kitty
用户名: admin 密码: admin
技术背景
当前, 我们基于导航菜单的显示和操作按钮的禁用状态, 实现了页面可见性和操作可用性的权限验证, 或者叫访问控制. 但这仅限于页面的显示和操作, 我们的后台接口还是没有进行权限的验证, 只要知道了后台的接口信息, 就可以直接通过 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 注解式权限控制方案的配置和执行流程就剖析的差不多了.
源码下载
后端: https://gitee.com/liuge1988/kitty
前端: https://gitee.com/liuge1988/kitty-ui.git
来源: https://www.cnblogs.com/xifengxiaoma/p/9983835.html