简介
Apache Shiro 是一个功能强大、灵活的,开源的安全框架。它可以干净利落地处理身份验证、授权、企业会话管理和加密。
Apache Shiro Features 特性
下图为描述 Shiro 功能的框架图:
Authentication(认证), Authorization(授权), Session Management(会话管理), Cryptography(加密)被 Shiro 框架的开发团队称之为应用安全的四大基石。那么就让我们来看看它们吧:
- **Authentication(认证):**用户身份识别,通常被称为用户“登录”
 
- **Authorization(授权):**访问控制。比如某个用户是否具有某个操作的使用权限。
 
- **Session Management(会话管理):**特定于用户的会话管理,甚至在非web 或 EJB 应用程序。
 
- **Cryptography(加密):**在对数据源使用加密算法加密的同时,保证易于使用。
 
High-Level Overview 高级概述
在概念层,Shiro 架构包含三个主要的理念:Subject,SecurityManager和 Realm。下面的图展示了这些组件如何相互作用,我们将在下面依次对其进行描述。
- Subject:当前用户,Subject 可以是一个人,但也可以是第三方服务、守护进程帐户、时钟守护任务或者其它–当前和软件交互的任何事件。
 
- SecurityManager:管理所有Subject,SecurityManager 是 Shiro 架构的核心,配合内部安全组件共同组成安全伞。
 
- Realms:用于进行权限信息的验证,我们自己实现。Realm 本质上是一个特定的安全 DAO:它封装与数据源连接的细节,得到Shiro 所需的相关的数据。在配置 Shiro 的时候,你必须指定至少一个Realm 来实现认证(authentication)和/或授权(authorization)。
 
我们需要实现Realms的Authentication 和 Authorization。其中 Authentication 是用来验证用户身份,Authorization 是授权访问控制,用于对用户进行的操作授权,证明该用户是否允许进行当前操作,如访问某个链接,某个资源文件等。
引入Maven依赖
使用Shiro的关键依赖如下
1 2 3 4 5 6 7 8 9 10 11
   |        <dependency>           <groupId>org.apache.shiro</groupId>           <artifactId>shiro-core</artifactId>           <version>${shiro.version}</version>       </dependency>       <dependency>           <groupId>org.apache.shiro</groupId>           <artifactId>shiro-spring</artifactId>           <version>${shiro.version}</version>       </dependency>
 
  | 
 
Shiro 配置
首先要配置的是 ShiroConfig 类,Apache Shiro 核心通过 Filter 来实现,通过 URL 规则来进行过滤和权限校验,所以我们需要定义一系列关于 URL 的规则和访问权限。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60
   | 
 
  @Configuration public class ShiroConfig {
      @Bean("sessionManager")     public SessionManager sessionManager(RedisShiroSessionDAO redisShiroSessionDAO,                                          @Value("${db.redis.open}") boolean redisOpen,                                          @Value("${db.shiro.redis}") boolean shiroRedis){         DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();                  sessionManager.setGlobalSessionTimeout(60 * 60 * 1000);         sessionManager.setSessionValidationSchedulerEnabled(true);         sessionManager.setSessionIdUrlRewritingEnabled(false);
                   if(redisOpen && shiroRedis){             sessionManager.setSessionDAO(redisShiroSessionDAO);         }         return sessionManager;     }
      @Bean("securityManager")     public SecurityManager securityManager(UserRealm userRealm, SessionManager sessionManager) {         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();         securityManager.setRealm(userRealm);         securityManager.setSessionManager(sessionManager);
          return securityManager;     }
 
      @Bean("shiroFilter")     public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {         ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();         shiroFilter.setSecurityManager(securityManager);         shiroFilter.setLoginUrl("/login.html");         shiroFilter.setUnauthorizedUrl("/");
          Map<String, String> filterMap = new LinkedHashMap<>();         filterMap.put("/swagger/**", "anon");         filterMap.put("/v2/api-docs", "anon");         filterMap.put("/swagger-ui.html", "anon");         filterMap.put("/webjars/**", "anon");         filterMap.put("/swagger-resources/**", "anon");
          filterMap.put("/statics/**", "anon");         filterMap.put("/login.html", "anon");         filterMap.put("/sys/login", "anon");         filterMap.put("/favicon.ico", "anon");         filterMap.put("/captcha.jpg", "anon");         filterMap.put("/**", "authc");         shiroFilter.setFilterChainDefinitionMap(filterMap);
          return shiroFilter;     } 	 }
 
 
  | 
 
sessionManager 方法管理会话,配置相关参数
securityManager 是管理所有Shiro相关对象的核心方法
shiroFilter 拦截器用来过滤和校验
Filter Chain 定义说明:
- 1、一个URL可以配置多个 Filter,使用逗号分隔
 
- 2、当设置多个过滤器时,全部验证通过,才视为通过
 
- 3、部分过滤器可指定参数,如 perms,roles
 
Shiro 内置的 FilterChain
| Filter Name | 
Class | 
| anon | 
org.apache.shiro.web.filter.authc.AnonymousFilter | 
| authc | 
org.apache.shiro.web.filter.authc.FormAuthenticationFilter | 
| authcBasic | 
org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter | 
| perms | 
org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter | 
| port | 
org.apache.shiro.web.filter.authz.PortFilter | 
| rest | 
org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter | 
| roles | 
org.apache.shiro.web.filter.authz.RolesAuthorizationFilter | 
| ssl | 
org.apache.shiro.web.filter.authz.SslFilter | 
| user | 
org.apache.shiro.web.filter.authc.UserFilter | 
- anon:所有 url 都都可以匿名访问
 
- authc: 需要认证才能进行访问
 
- user:配置记住我或认证通过可以访问
 
Authentication(认证)
在认证、授权内部实现机制中都有提到,最终处理都将交给Real进行处理。因为在 Shiro 中,最终是通过 Realm 来获取应用程序中的用户、角色及权限信息的。通常情况下,在 Realm 中会直接从我们的数据源中获取 Shiro 需要的验证信息。可以说,Realm 是专用于安全框架的 DAO. Shiro 的认证过程最终会交由 Realm 执行,这时会调用 Realm 的getAuthenticationInfo(token)方法。
该方法主要执行以下操作:
- 1、检查提交的进行认证的令牌信息
 
- 2、根据令牌信息从数据源(通常为数据库)中获取用户信息
 
- 3、对用户信息进行匹配验证。
 
- 4、验证通过将返回一个封装了用户信息的
AuthenticationInfo实例。 
- 5、验证失败则抛出
AuthenticationException异常信息。 
而在我们的应用程序中要做的就是自定义一个 Realm 类,继承AuthorizingRealm 抽象类,重载 doGetAuthenticationInfo(),重写获取用户信息的方法。
doGetAuthenticationInfo 的重写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
   | @Component public class UserRealm extends AuthorizingRealm {     	     
 
  	@Override 	protected AuthenticationInfo doGetAuthenticationInfo( 			AuthenticationToken authcToken) throws AuthenticationException { 		UsernamePasswordToken token = (UsernamePasswordToken)authcToken;
  		 		SysUserEntity user = new SysUserEntity(); 		user.setUsername(token.getUsername()); 		user = sysUserDao.selectOne(user);
  		 		if(user == null) { 			throw new UnknownAccountException("账号或密码不正确"); 		}
  		 		if(user.getStatus() == 0){ 			throw new LockedAccountException("账号已被锁定,请联系管理员"); 		}
  		SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, user.getPassword(), ByteSource.Util.bytes(user.getSalt()), getName()); 		return info; 	}      }
   | 
 
前端
页面代码如下
1 2 3
   | <div class="col-xs-4">   <button type="button" class="btn btn-primary btn-block btn-flat" @click="login">登录</button> </div>
   | 
 
js逻辑如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
   | <script type="text/javascript"> var vm = new Vue({ 	el:'#rrapp', 	data:{ 		username: '', 		password: '', 		captcha: '', 		error: false, 		errorMsg: '', 		src: 'captcha.jpg' 	}, 	beforeCreate: function(){ 		if(self != top){ 			top.location.href = self.location.href; 		} 	}, 	methods: { 		refreshCode: function(){ 			this.src = "captcha.jpg?t=" + $.now(); 		}, 		login: function (event) { 			var data = "username="+vm.username+"&password="+vm.password+"&captcha="+vm.captcha; 			$.ajax({ 				type: "POST", 			    url: "sys/login", 			    data: data, 			    dataType: "json", 			    success: function(result){ 					if(result.code == 0){ 						parent.location.href ='index.html'; 					}else{ 						vm.error = true; 						vm.errorMsg = result.msg; 						 						vm.refreshCode(); 					} 				} 			}); 		} 	} }); </script>
   | 
 
后端
Controller层配置登录验证的方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
   | 
 
  @ResponseBody @RequestMapping(value = "/sys/login", method = RequestMethod.POST) public R login(String username, String password, String captcha) {    String kaptcha = ShiroUtils.getKaptcha(Constants.KAPTCHA_SESSION_KEY);    if(!captcha.equalsIgnoreCase(kaptcha)){       return R.error("验证码不正确");    }        try{       Subject subject = ShiroUtils.getSubject();       UsernamePasswordToken token = new UsernamePasswordToken(username, password);       subject.login(token);    }catch (UnknownAccountException e) {       return R.error(e.getMessage());    }catch (IncorrectCredentialsException e) {       return R.error("账号或密码不正确");    }catch (LockedAccountException e) {       return R.error("账号已被锁定,请联系管理员");    }catch (AuthenticationException e) {       return R.error("账户验证失败");    }         return R.ok(); }
 
  | 
 
小结
登陆验证流程:
- 若用户未登录,Shiro配置的拦截器会生效,跳转到登录界面
 
- 页面中点击登录按钮,按照js逻辑进行登录验证请求,此处即 
/sys/login 
- 后端Controller层调用相应方法处理登录验证请求
 
- Controller层方法最终会调用
Realm 中的doGetAuthenticationInfo方法 
Authorization(授权)
Shiro 的权限授权是通过继承AuthorizingRealm抽象类,重载doGetAuthorizationInfo();当访问到页面的时候,链接配置了相应的权限或者 Shiro 标签才会执行此方法否则不会执行,所以如果只是简单的身份认证没有权限的控制的话,那么这个方法可以不进行实现,直接返回 null 即可。在这个方法中主要是使用类:SimpleAuthorizationInfo进行角色的添加和权限的添加。
doGetAuthorizationInfo 的重写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
   | @Component public class UserRealm extends AuthorizingRealm {	              
 
  	@Override 	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { 		SysUserEntity user = (SysUserEntity)principals.getPrimaryPrincipal(); 		Long userId = user.getUserId(); 		 		List<String> permsList; 		 		 		if(userId == Constant.SUPER_ADMIN){ 			List<SysMenuEntity> menuList = sysMenuDao.selectList(null); 			permsList = new ArrayList<>(menuList.size()); 			for(SysMenuEntity menu : menuList){ 				permsList.add(menu.getPerms()); 			} 		}else{ 			permsList = sysUserDao.queryAllPerms(userId); 		}
  		 		Set<String> permsSet = new HashSet<>(); 		for(String perms : permsList){ 			if(StringUtils.isBlank(perms)){ 				continue; 			} 			permsSet.addAll(Arrays.asList(perms.trim().split(","))); 		} 		 		SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); 		info.setStringPermissions(permsSet); 		return info; 	}      }
   | 
 
@RequiresPermissions() 注释的使用
在Controller层对应方法上添加注释,例如
1 2 3 4 5 6 7 8 9 10
   | 
 
  @RequestMapping("/list") @RequiresPermissions("sys:user:list") public R list(@RequestParam Map<String, Object> params){    PageUtils page = sysUserService.queryPage(params);
     return R.ok().put("page", page); }
 
  | 
 
数据库样例
部分权限数据如下
访问接口的时候,会验证其拥有的权限是否包含接口上的权限
Session Management(会话管理)
会话管理的配置在ShiroConfig类中。
Cryptography(加密)
加密配置 可以通过设置Shiro的bean,传入自定义密码加密器,参见SpringBoot Shiro 配置自定义密码加密器;也可以重写Realm的setCredentialsMatcher方法
setCredentialsMatcher 的重写
传入的参数包括加密方式和迭代次数
1 2 3 4 5 6 7 8 9 10 11 12
   | @Component public class UserRealm extends AuthorizingRealm {     	 	@Override 	public void setCredentialsMatcher(CredentialsMatcher credentialsMatcher) { 		HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher(); 		shaCredentialsMatcher.setHashAlgorithmName(ShiroUtils.hashAlgorithmName); 		shaCredentialsMatcher.setHashIterations(ShiroUtils.hashIterations); 		super.setCredentialsMatcher(shaCredentialsMatcher); 	}      }
   | 
 
参考资料
Apache Shiro 官方网站
Spring Boot 整合 Shiro-登录认证和权限管理