简介
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-登录认证和权限管理