您的位置:首页 > 其它

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包:

<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 的源代码
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: