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的区别:
- AuthenticationToken表示身份认证时提交的信息
- 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 的源代码

Logo

瓜分20万奖金 获得内推名额 丰厚实物奖励 易参与易上手

更多推荐