简介
Shiro是Java的安全框架,不如Spring Security强大,但是小而简单,能够解决问题。能应用于JavaSE、JavaEE,能够完成:认证、授权、加密、会话管理、与Web集成、缓存等。
基本功能点
-
Authentication:身份认证/登录,验证用户是不是拥有相应的身份。
-
Authorization:授权,即权限验证,验证某个已认证用户是否拥有某种权限,能进行某种操作。
-
Session Management:会话管理,用户登录后,在退出前的所有信息都在会话里。
-
Cryptography:加密。
-
Web Support:Web支持,易集成到web环境中。
-
Caching:缓存,比如用户登录后所具有的权限、信息不必每次都去查。
-
Concurrency:支持多线程的并发验证。
-
Testing:提供测试支持。
-
Run As:允许假装另一用户(如果用户允许的话)的身份进行访问。
-
Remember Me:记住我,下次不用登录。
-
注意:Shiro不会维护用户、维护权限,这些需要我们自己设计/提供,然后通过相应接口注入给Shiro即可。
主要组件
-
Subject:主体,可以看到主体可以是任何可以与应用交互的 “用户”;
-
SecurityManager:相当于 SpringMVC 中的 DispatcherServlet 或者 Struts2 中的 FilterDispatcher;是 Shiro 的心脏;所有具体的交互都通过 SecurityManager 进行控制;它管理着所有 Subject、且负责进行认证和授权、及会话、缓存的管理。
-
Authenticator:认证器,负责主体认证的,这是一个扩展点,如果用户觉得 Shiro 默认的不好,可以自定义实现;其需要认证策略(Authentication Strategy),即什么情况下算用户认证通过了;
-
Authrizer:授权器,或者访问控制器,用来决定主体是否有权限进行相应的操作;即控制着用户能访问应用中的哪些功能;
-
Realm:可以有 1 个或多个 Realm,可以认为是安全实体数据源,即用于获取安全实体的;可以是 JDBC 实现,也可以是 LDAP 实现,或者内存实现等等;由用户提供;
-
SessionManager:对session进行管理的组件;
-
SessionDAO:比如我们想把 Session 保存到数据库,那么可以实现自己的 SessionDAO,通过如 JDBC 写到数据库;比如想把 Session 放到 Memcached 中,可以实现自己的 Memcached SessionDAO;另外 SessionDAO 中可以使用 Cache 进行缓存,以提高性能;
-
CacheManager:缓存控制器,来管理如用户、角色、权限等的缓存的;因为这些数据基本上很少去改变,放到缓存中后可以提高访问的性能
-
Cryptography:密码模块,Shiro 提供了一些常见的加密组件用于如密码加密 / 解密的。
身份验证
在Shiro中用户需要提交principals(身份)和credentials(证明)来验证身份。
-
principals:身份,即主体的标识属性,可以是任何东西,如用户名、邮箱等,唯一即可。一个主体可以有多个 principals,但只有一个 Primary principals,一般是用户名 / 密码 / 手机号。
-
credentials:证明 / 凭证,即只有主体知道的安全值,如密码 / 数字证书等。
@Test public void testHelloworld() { //1、获取SecurityManager工厂,此处使用Ini配置文件初始化SecurityManager Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); //2、得到SecurityManager实例 并绑定给SecurityUtils org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); //3、得到Subject及创建用户名/密码身份验证Token(即用户身份/凭证) Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123"); try { //4、登录,即身份验证 subject.login(token); } catch (AuthenticationException e) { //5、身份验证失败 } Assert.assertEquals(true, subject.isAuthenticated()); //断言用户已经登录 //6、退出 subject.logout(); }
从如上代码可总结出身份验证的步骤:
- 收集用户身份 / 凭证,即如用户名 / 密码;
- 调用 Subject.login 进行登录,如果失败将得到相应的 AuthenticationException 异常,根据异常提示用户错误信息;否则登录成功;
- 最后调用 Subject.logout 进行退出操作。
- 身份认证流程
Subject -> Security Manager -> Anthenticator -> AuthenticationStrategy -> Realm
自定义 Realm 实现
多Realm配置,如果不设置securityManager.realms属性值的话可以自动识别,按照定义顺序进行认证;在配置文件中指定securityManager.realms属性了,则会按照指定的顺序进行认证,并且会忽略未指定的realm实现。通常是自定义Realm实现,实现Authenticator
接口,并重写方法。
主要默认Realm实现
-
org.apache.shiro.realm.text.IniRealm:
[users] 部分指定用户名 / 密码及其角色;
[roles] 部分指定角色即权限信息; -
org.apache.shiro.realm.text.PropertiesRealm:
user.username=password,role1,role2 指定用户名/密码及其角色;
role.role1=permission1,permission2 指定角色及权限信息; -
org.apache.shiro.realm.jdbc.JdbcRealm:
通过 sql 查询相应的信息,如
“select password from users where username = ?” 获取用户密码;
“select password, password_salt from users where username = ?” 获取用户密码及盐;
“select role_name from user_roles where username = ?” 获取用户角色;
“select permission from roles_permissions where role_name = ?” 获取角色对应的权限信息;
也可以调用相应的 api 进行自定义 sql;
Authenticator 及 AuthenticationStrategy
SecurityManager
接口继承了Authenticator
,另外还有一个ModularRealmAuthenticator
实现,其委托给多个Realm
进行验证,验证规则通过AuthenticationStrategy
接口指定。
AuthenticationStrategy默认提供的实现:
- FirstSuccessfulStrategy:只要有一个 Realm 验证成功即可,只返回第一个 Realm 身份验证成功的认证信息,其他的忽略;
- AtLeastOneSuccessfulStrategy:只要有一个 Realm 验证成功即可,和 FirstSuccessfulStrategy 不同,返回所有 Realm 身份验证成功的认证信息;
- AllSuccessfulStrategy:所有 Realm 验证成功才算成功,且返回所有 Realm 身份验证成功的认证信息,如果有一个失败就失败了。
- ModularRealmAuthenticator 默认使用 AtLeastOneSuccessfulStrategy 策略。
Shiro授权
授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等)。在授权中需了解的几个关键对象:主体(Subject)、资源(Resource)、权限(Permission)、角色(Role)。
授权方式
shiro支持三种授权方式:
- 编程式:通过if/else语句块
Subject subject = SecurityUtils.getSubject(); if(subject.hasRole(“admin”)) { //有权限 } else { //无权限 }
- 注解式:通过在执行的方法上添加注解
@RequiresRoles("admin") public void hello() { //有权限 }
- JSP/GSP式:在页面上添加相应的标签
<shiro:hasRole name="admin"> <!— 有权限 —> </shiro:hasRole>
授权
-
基于角色的访问控制
[users] zhang=123,role1,role2 wang=123,role1
Shiro 提供了 hasRole/hasRoles 用于判断用户是否拥有某个角色/某些权限;但是没有提供如 hashAnyRole 用于判断是否有某些权限中的某一个。
Shiro 提供的 checkRole/checkRoles 和 hasRole/hasAllRoles 不同的地方是它在判断为假的情况下会抛出 UnauthorizedException 异常。
这种方式的缺点就是如果很多地方进行了角色判断,但是有一天不需要了那么就需要修改相应代码把所有相关的地方进行删除;这就是粗粒度造成的问题。 -
基于资源的访问控制
[users] zhang=123,role1,role2 wang=123,role1 [roles] role1=user:create,user:update role2=user:create,user:delete
Shiro 提供了 isPermitted 和 isPermittedAll 用于判断用户是否拥有某个权限或所有权限,也没有提供如 isPermittedAny 用于判断拥有某一个权限的接口。失败的情况下会抛出 UnauthorizedException 异常。
好处就是如果要修改基本都是一个资源级别的修改,不会对其他模块代码产生影响,粒度小。
缺点是实现起来可能稍微复杂点,需要维护“用户——角色,角色——权限(资源:操作)”之间的关系。
字符串通配符权限
- “资源标识符:操作:对象实例id” ,即对哪个资源的哪个实例可以进行什么操作。
“ , ”表示操作的分割;
“ * ”表示任意资源/操作/实例。role74=menu:view:* subject().checkPermission("menu:view:1");
- Shiro 对权限字符串缺失部分的处理
一般采用前缀匹配,*可以匹配所有,不能后缀匹配必须指定所有前缀。 - 性能问题
通配符匹配方式比字符串相等匹配来说是更复杂的,因此需要花费更长时间,但是一般系统的权限不会太多,且可以配合缓存来提供其性能。
如果这样性能还达不到要求我们可以实现位操作算法实现性能更好的权限匹配。
另外实例级别的权限验证如果数据量太大也不建议使用,可能造成查询权限及匹配变慢。
可以考虑比如在sql查询时加上权限字符串之类的方式在查询时就完成了权限匹配。
授权流程
- Security Manager 继承了 Authorizer 接口,且提供了 ModularRealmAuthorizer 用于多 Realm 时的授权匹配。
- Authorizer 的职责是进行授权(访问控制),是 Shiro API 中授权核心的入口点,其提供了相应的角色/权限判断接口。
- PermissionResolver 用于解析权限字符串到 Permission 实例。
- RolePermissionResolver 用于根据角色解析相应的权限集合。
- Subject -> Security Maneger -> Authorizer -> PermissionResolver -> RolePermissionResolver
实例详解
Shiro编码加密
-
Shiro 提供了 base64 和 16 进制字符串编码 / 解码的 API 支持,方便一些编码解码操作。
//Base64编码/解码方式 String str = "hello"; String base64Encoded = Base64.encodeToString(str.getBytes()); String str2 = Base64.decodeToString(base64Encoded);
//十六进制字符串编码/解码方式 String str = "hello"; String base64Encoded = Hex.encodeToString(str.getBytes()); String str2 = new String(Hex.decode(base64Encoded.getBytes()));
-
散列算法
散列算法一般用于生成数据的摘要信息,是一种不可逆的算法,一般适合存储密码之类的数据,常见的散列算法如 MD5、SHA 等。一般进行散列时最好提供一个只有系统知道的干扰数据,如用户名和 ID(即盐)。//MD5散列 String str = "hello"; String salt = "123"; String md5 = new Md5Hash(str, salt).toString();//还可以转换为 toBase64()/toHex()
//SHA256散列 String str = "hello"; String salt = "123"; String sha1 = new Sha256Hash(str, salt).toString();
//Shrio提供了HashService,默认提供了DefaultHashService实现。 DefaultHashService hashService = new DefaultHashService(); //默认算法SHA-512 hashService.setHashAlgorithmName("SHA-512"); hashService.setPrivateSalt(new SimpleByteSource("123")); //私盐,默认无 hashService.setGeneratePublicSalt(true);//是否生成公盐,默认false hashService.setRandomNumberGenerator(new SecureRandomNumberGenerator());//用于生成公盐。默认就这个 hashService.setHashIterations(1); //生成Hash值的迭代次数 HashRequest request = new HashRequest.Builder() .setAlgorithmName("MD5").setSource(ByteSource.Util.bytes("hello")) .setSalt(ByteSource.Util.bytes("123")).setIterations(2).build(); String hex = hashService.computeHash(request).toHex();
-
加密/解密
Shiro 还提供对称式加密 / 解密算法的支持,如 AES、Blowfish 等。/AES算法实现 AesCipherService aesCipherService = new AesCipherService(); aesCipherService.setKeySize(128); //设置key长度 //生成key Key key = aesCipherService.generateNewKey(); String text = "hello"; //加密 String encrptText = aesCipherService.encrypt(text.getBytes(), key.getEncoded()).toHex(); //解密 String text2 = new String(aesCipherService.decrypt(Hex.decode(encrptText), key.getEncoded()).getBytes()); Assert.assertEquals(text, text2);
-
PasswordService/CredentialsMatcher
Shiro 提供了 PasswordService 及 CredentialsMatcher 用于提供加密密码及验证密码服务。public interface PasswordService { //输入明文密码得到密文密码 String encryptPassword(Object plaintextPassword) throws IllegalArgumentException; }
public interface CredentialsMatcher { //匹配用户输入的token的凭证(未加密)与系统提供的凭证(已加密) boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info); }
还可以实现多次失败登录锁定
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1 Element element = passwordRetryCache.get(username); if(element == null) { element = new Element(username , new AtomicInteger(0)); passwordRetryCache.put(element); } AtomicInteger retryCount = (AtomicInteger)element.getObjectValue(); if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; }