Shiro(1) 身份与权限验证
2017-06-19 15:24
295 查看
Apache Shiro 是一个轻量级的开源安全框架,用于身份认证,授权,会话管理和加密。
下图描述了Shiro的基本功能:
Authentication:有时也简称为“登录”,这是一个证明用户是他们所说的他们是谁的行为。
Authorization:访问控制的过程,即角色与权限控制。
Session Management:管理用户特定的会话,支持非 Web应用,因为Shiro自己实现了一整套的Session管理。
Cryptography:通过使用加密算法保持数据安全同时易于使用。
也提供了额外的功能来支持和加强在不同环境下所关注的方面,尤其是以下这些:
Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作快速而又高效。
Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
“Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
“Remember Me”:在会话中记住用户的身份,所以他们只需要在强制时候登录
我们先构建一个简单的案例.
首先引入jar包:
自定义一个Realm:
开始测试:
例子中主要有个组件:
DelegatingSubject 的代码可以看到操作实际上是委托给SecurityManager来执行。
- Authenticatio
ef88
nToken表示身份认证时提交的信息
- AuthenticationInfo表示从数据源中获取的用户信息
PrincipalCollection是一个集合,用来存储用户的“标识”。可以理解为用户属性信息。
一般情况下可以将用户属性信息设计为一个对象,PrincipalCollection中只存储这个对象。
将密码校验抽象出一个接口,可以将密码进行一些加密校验,比如:加盐,将密码通过指定MD5加密后比较等,比如:
AllowAllCredentialsMatcher永远验证通过;
SimpleCredentialsMatcher直接比较 AuthenticationInfo.getCredentials() 与 AuthenticationToken.getCredentials();
HashedCredentialsMatcher通过指定算法(如Md5)将提交的AuthenticationToken.getCredentials()加盐(如果有)后再与AuthenticationInfo.getCredentials()比较。
我们也可以自定义 CredentialsMatcher 来实现自己的加密算法。
SecurityManager的结构如下:
SecurityManager除了自定义的以上三个方法外,还继承了 Authenticator, Authorizer, SessionManager三个接口。
可以看到这个接口与上面自定义的MyRealm.doGetAuthenticationInfo(token)方法非常像。
实际上, Subject.login()委托给 SecurityManager来实现,
而DefaultSecurityManager 是通过内部的 ModularRealmAuthenticator 来调用Realm来实现的。
我们来看看Subject.login()的实现细节:
DelegatingSubject
DefaultSecurityManager
AuthenticatingSecurityManager
AbstractAuthenticator
ModularRealmAuthenticator
可以看到最后委托给对应的Realm执行。
查看 Authorizer 的继承关系可以看到只有一个实现了Realm的AuthorizingRealm。
很容易的猜想 Authorizer 的权限判定功能是根据 自定义Realm的 doGetAuthorizationInfo(principals)方法获取到的 AuthorizationInfo 来判断的。
AuthorizationInfo表示用户的权限信息。
那么Subject又是如何调用Realm的相关判定的?
我们知道Subject的安全操作是委托给SecurityManager来处理的,而 SecurityManager 实现了 Authorizer 接口。
实际上, SecurityManager 对于权限的相关操作都是委托给内部的 ModularRealmAuthorizer 来实现的。
如果Realm 实现了Authorizer接口(继承自AuthorizingRealm),则调用Realm的对应方法进行判定。
ModularRealmAuthorizer
通过以上代码的分析,可以看出 Shiro将所有安全操作委托给SecurityManager处理,
而SecurityManager 委托给内部的 Authenticator 和 Authorizer 处理,
最后 Authenticator 与 Authorizer 都是通过 Realm获取用户信息或权限信息来判定。
除了上面我们自定义Realm,然后创建SecurityManager并设置Realm外,shiro为我们提供了基于配置文件的权限认证功能:
基于spring提供的基于配置文件的案例:
shiro.ini
测试方法:
还可以结合上面两个方法,通过配置文件来指定Realm:
shiro.ini
测试:
具体Shiro是如何通过配置文件来指定Realms的,可以查看 IniSecurityManagerFactory 的源代码
下图描述了Shiro的基本功能:
Authentication:有时也简称为“登录”,这是一个证明用户是他们所说的他们是谁的行为。
Authorization:访问控制的过程,即角色与权限控制。
Session Management:管理用户特定的会话,支持非 Web应用,因为Shiro自己实现了一整套的Session管理。
Cryptography:通过使用加密算法保持数据安全同时易于使用。
也提供了额外的功能来支持和加强在不同环境下所关注的方面,尤其是以下这些:
Web Support:Shiro 的 web 支持的 API 能够轻松地帮助保护 Web 应用程序。
Caching:缓存是 Apache Shiro 中的第一层公民,来确保安全操作快速而又高效。
Concurrency:Apache Shiro 利用它的并发特性来支持多线程应用程序。
Testing:测试支持的存在来帮助你编写单元测试和集成测试,并确保你的能够如预期的一样安全。
“Run As”:一个允许用户假设为另一个用户身份(如果允许)的功能,有时候在管理脚本很有用。
“Remember Me”:在会话中记住用户的身份,所以他们只需要在强制时候登录
我们先构建一个简单的案例.
首先引入jar包:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.3.2</version> </dependency>
自定义一个Realm:
public class MyRealm extends AuthorizingRealm{ //usersMap存储用户 private Map<String, String> usersMap = new HashMap<String, String>(); //userRoleMap存储用户角色 private Map<String, List<String>> userRoleMap = new HashMap<String, List<String>>(); //rolesMap存储角色的权限 private Map<String, List<String>> rolePermissionsMap = new HashMap<String, List<String>>(); public MyRealm() { super(); //初始化用户、权限数据 usersMap.put("admin", "123456"); usersMap.put("zhangsan", "654321"); userRoleMap.put("admin", Arrays.asList("admin")); userRoleMap.put("zhangsan", Arrays.asList("admin","normal")); rolePermissionsMap.put("admin", Arrays.asList("user:create","user:update","user:delete")); rolePermissionsMap.put("normal", Arrays.asList("user:view")); super.setCredentialsMatcher(new SimpleCredentialsMatcher()); } //用户凭证认证并获取用户信息 @Override protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { if(!usersMap.containsKey(token.getPrincipal())){ throw new UnknownAccountException("用户不存在"); } AuthenticationInfo info = new SimpleAuthenticationInfo(token.getPrincipal(), usersMap.get(token.getPrincipal()), super.getName()); return info; } //获取用户角色与权限 @Override protected AuthorizationInfo doGetAuthorizationInfo( PrincipalCollection principals) { List<String> roles = userRoleMap.get(principals.getPrimaryPrincipal()); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addRoles(roles); for(String role: roles){ info.addStringPermissions(rolePermissionsMap.get(role)); } return info; } }
开始测试:
@Test public void test(){ DefaultSecurityManager securityManager = new DefaultSecurityManager(); securityManager.setRealm(new MyRealm()); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); //Session会话,类似与java web中的session Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); //登陆 UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456"); try{ currentUser.login(token); }catch(UnknownAccountException e){ //用户不存在 e.printStackTrace(); }catch(IncorrectCredentialsException e){ //密码不正确 e.printStackTrace(); }catch(LockedAccountException e){ //用户已锁定,不能登陆 e.printStackTrace(); }catch(AuthenticationException e){ //其它情况 e.printStackTrace(); } //判断用户是否登陆 assertTrue(currentUser.isAuthenticated()); //判断用户是否拥有admin角色 assertTrue(currentUser.hasRole("admin")); //判断用户是否拥有admin+normal角色 assertFalse(currentUser.hasAllRoles(Arrays.asList("admin","normal"))); //判断用户是否拥有权限 user:view assertFalse(currentUser.isPermitted("user:view")); //验证用户拥有user:create权限,没有则抛出 AuthorizationException try{ currentUser.checkPermission("user:create"); }catch(AuthorizationException e){ e.printStackTrace(); } //退出 currentUser.logout(); }
例子中主要有个组件:
Subject
“主体”,相当于用户,Subject定义了登陆、登出、权限与角色判定等方法,查看Subject的实现类DelegatingSubject 的代码可以看到操作实际上是委托给SecurityManager来执行。
Realm
担当Shiro与我们自己的“安全数据”之间的桥梁,用于获取用户身份信息与用户角色、权限信息。大多数情况下我们都需要自己实现一个Realm.AuthenticationToken
用户进行身份认证时提交的信息,查看此接口的源码可以看到只有两个属性:public interface AuthenticationToken extends Serializable { //返回在账户认证过程中提交的账户标识,一般情况下可以理解为用户名。 //通常情况下账户认证都是基于 用户名/密码的,此时可以使用UsernamePasswordToken Object getPrincipal(); //返回账户认证过程中提交的凭证。(比如:密码) Object getCredentials(); }
AuthenticationInfo
用户身份信息,注意与AuthenticationToken的区别:- Authenticatio
ef88
nToken表示身份认证时提交的信息
- AuthenticationInfo表示从数据源中获取的用户信息
public interface AuthenticationInfo { //返回相关的所有Principal(主体),每个主体都是对应用程序有用的标识信息, //例如用户名、用户id、给定名称等等——任何对应用程序都有用的信息,用于识别当前的主题。 PrincipalCollection getPrincipals(); //返回与该主体相关连的凭据,例如密码。 Object getCredentials(); }
PrincipalCollection是一个集合,用来存储用户的“标识”。可以理解为用户属性信息。
一般情况下可以将用户属性信息设计为一个对象,PrincipalCollection中只存储这个对象。
CredentialsMatcher
用户凭证验证,用于将身份认证时提交的AuthenticationToken与从数据源中获取的AuthenticationInfo的凭证(可以理解为密码)进行比较。将密码校验抽象出一个接口,可以将密码进行一些加密校验,比如:加盐,将密码通过指定MD5加密后比较等,比如:
AllowAllCredentialsMatcher永远验证通过;
SimpleCredentialsMatcher直接比较 AuthenticationInfo.getCredentials() 与 AuthenticationToken.getCredentials();
HashedCredentialsMatcher通过指定算法(如Md5)将提交的AuthenticationToken.getCredentials()加盐(如果有)后再与AuthenticationInfo.getCredentials()比较。
我们也可以自定义 CredentialsMatcher 来实现自己的加密算法。
SecurityManager
安全管理器,此类是Shiro的核心类,所有与安全相关的操作都是通过此类来完成,比如Subject的操作都是委托为此类的实现类完成; SecurityUtils.getSubject()最后是委托给SecurityManger.createSublect(context)来创建。SecurityManager的结构如下:
public interface SecurityManager extends Authenticator, Authorizer, SessionManager { //登陆 Subject login(Subject subject, AuthenticationToken authenticationToken) throws AuthenticationException; //登出 void logout(Subject subject); //新建主体 Subject createSubject(SubjectContext context); }
SecurityManager除了自定义的以上三个方法外,还继承了 Authenticator, Authorizer, SessionManager三个接口。
Authenticator
public interface Authenticator { /** * 身份验证 * 如果验证通过,返回一个 AuthenticationInfo 实例,存储代表用户账户的相关数据。 * 这个返回的对象一般用于构造一个更完整的用户账户。 * 身份验证过程中如果出现任何异常,都将抛出 AuthenticationException * * @param authenticationToken * @return * @throws AuthenticationException 请参阅下面列出的特定异常, * 以准确地处理这些问题,并以适当的方式通知用户身份验证失败的原因。 * @see ExpiredCredentialsException * @see IncorrectCredentialsException * @see ExcessiveAttemptsException * @see LockedAccountException * @see ConcurrentAccessException * @see UnknownAccountException */ public AuthenticationInfo authenticate(AuthenticationToken authenticationToken) throws AuthenticationException; }
可以看到这个接口与上面自定义的MyRealm.doGetAuthenticationInfo(token)方法非常像。
实际上, Subject.login()委托给 SecurityManager来实现,
而DefaultSecurityManager 是通过内部的 ModularRealmAuthenticator 来调用Realm来实现的。
我们来看看Subject.login()的实现细节:
DelegatingSubject
public void login(AuthenticationToken token) throws AuthenticationException { //清除session中的身份信息 clearRunAsIdentitiesInternal(); //委托给SecurityManager.login(subject, token)处理 Subject subject = securityManager.login(this, token); //根据返回结果重新设置Subject的属性信息 //省略…… }
DefaultSecurityManager
public Subject login(Subject subject, AuthenticationToken token) throws AuthenticationException { //委托给从Authenticator继承的 authenticate(token)方法 AuthenticationInfo info; try { info = authenticate(token); } catch (AuthenticationException ae) { try { onFailedLogin(token, ae, subject); } catch (Exception e) { if (log.isInfoEnabled()) { log.info("onFailedLogin method threw an " + "exception. Logging and propagating original AuthenticationException.", e); } } throw ae; //propagate } Subject loggedIn = createSubject(token, info, subject); onSuccessfulLogin(token, info, loggedIn); return loggedIn; }
AuthenticatingSecurityManager
private Authenticator authenticator; public AuthenticatingSecurityManager() { super(); this.authenticator = new ModularRealmAuthenticator(); } //省略…… public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { return this.authenticator.authenticate(token); }
AbstractAuthenticator
public final AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException { //token为null if (token == null) { throw new IllegalArgumentException("Method argument (authentication token) cannot be null."); } AuthenticationInfo info; try { //钩子方法,交给子类处理 info = doAuthenticate(token); if (info == null) { String msg = "No account information found for authentication token [" + token + "] by this " + "Authenticator instance. Please check that it is configured correctly."; throw new AuthenticationException(msg); } } catch (Throwable t) { //异常处理及日志,省略…… try { //通知监听器 AuthenticationListener 登陆失败 notifyFailure(token, ae); } catch (Throwable t2) { //异常处理及日志,省略…… } throw ae; } //通知监听器 AuthenticationListener 登陆成功 notifySuccess(token, info); return info; }
ModularRealmAuthenticator
protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException { assertRealmsConfigured(); Collection<Realm> realms = getRealms(); if (realms.size() == 1) { //单个Realm验证 return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken); } else { //多个Realm验证 return doMultiRealmAuthentication(realms, authenticationToken); } } //单个Realm验证 protected AuthenticationInfo doSingleRealmAuthentication(Realm realm, AuthenticationToken token) { //此realm不支持这个token, if (!realm.supports(token)) { String msg = "Realm [" + realm + "] does not support authentication token [" + token + "]. Please ensure that the appropriate Realm implementation is " + "configured correctly or that the realm accepts AuthenticationTokens of this type."; throw new UnsupportedTokenException(msg); } //此方法调用子类实现的钩子方法 doGetAuthenticationInfo(token) AuthenticationInfo info = realm.getAuthenticationInfo(token); if (info == null) { String msg = "Realm [" + realm + "] was unable to find account data for the " + "submitted AuthenticationToken [" + token + "]."; throw new UnknownAccountException(msg); } return info; }
可以看到最后委托给对应的Realm执行。
Authorizer
Authorizer接口提供了权限与角色的判定功能。public interface Authorizer { //判断是否拥有指定权限 boolean isPermitted(PrincipalCollection principals, String permission); //判断是否拥有指定权限 boolean isPermitted(PrincipalCollection subjectPrincipal, Permission permission); //判断是否拥有每个权限,并返回对应的boolean数组 boolean[] isPermitted(PrincipalCollection subjectPrincipal, String... permissions); //判断是否拥有每个权限,并返回对应的boolean数组 boolean[] isPermitted(PrincipalCollection subjectPrincipal, List<Permission> permissions); //判断是否拥有全部权限 boolean isPermittedAll(PrincipalCollection subjectPrincipal, String... permissions); //判断是否拥有全部权限 boolean isPermittedAll(PrincipalCollection subjectPrincipal, Collection<Permission> permissions); //断言拥有指定角色,没有抛出异常 void checkPermission(PrincipalCollection subjectPrincipal, String permission) throws AuthorizationException; //断言拥有指定权限,没有抛出异常 void checkPermission(PrincipalCollection subjectPrincipal, Permission permission) throws AuthorizationException; //断言拥有全部指定权限,没有抛出异常 void checkPermissions(PrincipalCollection subjectPrincipal, String... permissions) throws AuthorizationException; //断言拥有全部指定权限,没有抛出异常 void checkPermissions(PrincipalCollection subjectPrincipal, Collection<Permission> permissions) throws AuthorizationException; //判断是否拥有指定角色 boolean hasRole(PrincipalCollection subjectPrincipal, String roleIdentifier); //判断是否拥有每个角色,并返回对应的boolean数组 boolean[] hasRoles(PrincipalCollection subjectPrincipal, List<String> roleIdentifiers); //判断是否拥有全部角色 boolean hasAllRoles(PrincipalCollection subjectPrincipal, Collection<String> roleIdentifiers); //断言拥有角色,没有抛出异常 void checkRole(PrincipalCollection subjectPrincipal, String roleIdentifier) throws AuthorizationException; //断言拥有全部角色,没有抛出异常 void checkRoles(PrincipalCollection subjectPrincipal, Collection<String> roleIdentifiers) throws AuthorizationException; //断言拥有全部角色,没有抛出异常 void checkRoles(PrincipalCollection subjectPrincipal, String... roleIdentifiers) throws AuthorizationException; }
查看 Authorizer 的继承关系可以看到只有一个实现了Realm的AuthorizingRealm。
很容易的猜想 Authorizer 的权限判定功能是根据 自定义Realm的 doGetAuthorizationInfo(principals)方法获取到的 AuthorizationInfo 来判断的。
AuthorizationInfo表示用户的权限信息。
public interface extends Serializable { //返回对应 Subject的所有角色名称 Collection<String> getRoles(); //返回字符串表述的权限的集合 Collection<String> getStringPermissions(); //返回Permission表述的权限的集合 Collection<Permission> getObjectPermissions(); }
那么Subject又是如何调用Realm的相关判定的?
我们知道Subject的安全操作是委托给SecurityManager来处理的,而 SecurityManager 实现了 Authorizer 接口。
实际上, SecurityManager 对于权限的相关操作都是委托给内部的 ModularRealmAuthorizer 来实现的。
如果Realm 实现了Authorizer接口(继承自AuthorizingRealm),则调用Realm的对应方法进行判定。
public abstract class AuthorizingSecurityManager extends AuthenticatingSecurityManager { private Authorizer authorizer; public AuthorizingSecurityManager() { super(); //使用 ModularRealmAuthorizer this.authorizer = new ModularRealmAuthorizer(); } //在设置Realm时,将 ModularRealmAuthorizer的 realm设置为SecurityManager的Realm protected void afterRealmsSet() { super.afterRealmsSet(); if (this.authorizer instanceof ModularRealmAuthorizer) { ((ModularRealmAuthorizer) this.authorizer).setRealms(getRealms()); } } //省略…… public boolean isPermitted(PrincipalCollection principals, String permissionString) { return this.authorizer.isPermitted(principals, permissionString); } //省略……
ModularRealmAuthorizer
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; }
通过以上代码的分析,可以看出 Shiro将所有安全操作委托给SecurityManager处理,
而SecurityManager 委托给内部的 Authenticator 和 Authorizer 处理,
最后 Authenticator 与 Authorizer 都是通过 Realm获取用户信息或权限信息来判定。
除了上面我们自定义Realm,然后创建SecurityManager并设置Realm外,shiro为我们提供了基于配置文件的权限认证功能:
基于spring提供的基于配置文件的案例:
shiro.ini
#dingyi几个用户,格式:用户=密码,角色1,角色2... [users] root=secret,admin guest=guest,guest presidentskroob=12345,president darkhelmet=ludicrousspeed,darklord,schwartz lonestarr=vespa,goodguy,schwartz [roles] admin=* schwartz=lightsaber:* goodguy=winnebago:drive:eagle5
测试方法:
public static void main(String[] args) { //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); Subject currentUser = SecurityUtils.getSubject(); //登陆 if(!currentUser.isAuthenticated()){ UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); currentUser.login(token); } }
还可以结合上面两个方法,通过配置文件来指定Realm:
shiro.ini
[main] myRealm=com.example.shiro.MyRealm
测试:
public static void main(String[] args) { Factory<org.apache.shiro.mgt.SecurityManager> factory = new IniSecurityManagerFactory("classpath:hello/shiro/demo4/shiro.ini"); org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance(); SecurityUtils.setSecurityManager(securityManager); Subject currentUser = SecurityUtils.getSubject(); //登陆 UsernamePasswordToken token = new UsernamePasswordToken("admin", "123456"); try{ currentUser.login(token); }catch(AuthenticationException e) { if(e instanceof UnknownAccountException){ //用户不存在 }else if(e instanceof IncorrectCredentialsException){ //密码不正确 }else if(e instanceof LockedAccountException){ //用户已锁定,不能登陆 }else{ //其它情况(超出预料之外) } e.printStackTrace(); } System.out.println(currentUser.getPrincipal()+", "+currentUser.hasRole("admin")); }
具体Shiro是如何通过配置文件来指定Realms的,可以查看 IniSecurityManagerFactory 的源代码
相关文章推荐
- 使用shiro进行系统身份验证-权限控制,登录界面乱跳
- 权限系统_shiro_身份验证篇
- 基于Shiro验证用户权限,且给用户授权
- 权限验证框架Shiro学习(一)
- shiro入门 之 身份验证
- Web用户的身份验证及WebApi权限验证流程的设计和实现
- 权限验证框架Shiro学习(二)
- spring shiro权限注解方式验证;
- Shiro身份验证Realm
- Shiro身份验证
- Web用户的身份验证及WebApi权限验证流程的设计和实现
- 简单扩展shiro 实现NOT、AND、OR权限验证(支持复杂一点的表达式)
- shiro第二天——角色和权限验证(编程式授权)
- ASP.NET MVC:窗体身份验证及角色权限管理示例
- shiro 第二节 身份验证
- 第二章 Shiro身份验证
- shiro权限验证标签
- 第二章 身份验证——跟我学习springmvc shiro mybatis
- 第二章 身份验证——跟我学习springmvc shiro mybatis
- shiro权限验证标签