ZhouXiangの博客 后端工程师&机器学习爱好者

Java安全框架-Shiro

2018-06-15

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。。

什么是Shiro

Shiro是一个Java平台的开源权限框架,用于认证和访问授权.具体来说,满足对如下元素的支持:

  • 用户,角色,权限(仅仅是操作权限,数据权限必须与业务需求紧密结合),资源(url).
  • 用户分配角色,角色定义权限.
  • 访问授权时支持角色或者权限,并且支持多级的权限定义.

系统架构及组件介绍

  • Subject对当前操作用户进行抽象,将用户的概念理解为当前操作的主体,可能是一个通过浏览器请求的用户,也可能是一个运行的程序.Subject在shiro中是一个接口,接口中定义了很多认证授相关的方法,外部程序通过Subject进行认证授,而Subject是通过SecurityManager安全管理器进行认证授权.
  • SecurityManager即安全管理器,对全部的subject进行安全管理.它是shiro的核心,负责对所有的subject进行安全管理.通过SecurityManager可以完成subject的认证、授权等,实质上SecurityManager是通过Authenticator进行认证,通过Authorizer进行授权,通过SessionManager进行会话管理等.SecurityManager是一个接口,继承了Authenticator, Authorizer, SessionManager这三个接口.
  • Realm即领域,相当于datasource数据源.SecurityManager进行安全认证需要通过Realm获取用户权限数据,比如:如果用户身份数据在数据库那么realm就需要从数据库获取用户身份信息.
  • Authenticator即认证器,对用户身份进行认证.Authenticator是一个接口,shiro提供ModularRealmAuthenticator实现类,通过ModularRealmAuthenticator基本上可以满足大多数需求,也可以自定义认证器.
  • Authorizer即授权器,用户通过认证器认证通过,在访问功能时需要通过授权器判断用户是否有此功能的操作权限.
  • SessionManager即会话管理,Shiro框架定义了一套会话管理,它不依赖Web容器的Session,所以Shiro可以使用在非web应用上,也可以将分布式应用的会话集中在一点管理,此特性可使它实现单点登录.
  • SessionDAO即会话dao,是对session会话操作的一套接口.比如要将session存储到数据库,可以通过jdbc将会话存储到数据库.
  • CacheManager即缓存管理,将用户权限数据存储在缓存,这样可以提高性能.
  • Cryptography即密码管理,shiro提供了一套加密/解密的组件,方便开发.比如提供常用的散列、加/解密等功能.

Shiro用户认证时序图

Shiro身份认证流程

  • 首先调用Subject.login(token)进行登录,其会自动委托给Security Manager,调用之前必须通过SecurityUtils.SetSecurityManager()设置.
  • SecurityManager负责真正的身份验证逻辑;它会委托给Authenticator进行身份验证.
  • Authenticator才是真正的身份验证者,Shiro API中核心的身份认证入口点,此处可以自定义插入自己的实现.
  • Authenticator可能会委托给相应的AuthenticationStrategy进行多Realm身份验证,默认ModularRealmAuthenticator会调用AuthenticationStrategy进行多Realm身份验证.
  • Authenticator会把相应的token传入Realm,从Realm获取身份验证信息,如果没有返回或抛出异常表示身份验证失败了.此处可以配置多个Realm,将按照相应的顺序及策略进行访问.

Shiro身份认证流程

  • 首先调用Subject.isPermitted/hasRole 接口,其会委托给SecurityManager,而SecurityManager接着会委托给Authorizer.
  • Authorizer是真正的授权者,如果我们调用如isPermitted(“user:view”),其首先会通过PermissionResolver把字符串转换成相应的Permission实例.
  • 在进行授权之前,其会调用相应的Realm获取Subject相应的角色/权限用于匹配传入的角色/权限.
  • Authorizer会判断Realm的角色/权限是否和传入的匹配,如果有多个Realm,会委托给ModularRealmAuthorizer进行循环判断,如果匹配如isPermitted/hasRole 会返回true,否则返回false表示授权失败.

下面将分章节对Shiro中的重要部分介绍

1.Realm

Shiro从从Realm获取安全数据(如用户、角色、权限),就是说SecurityManager要验证用户身份,那么它需要从Realm获取相应的用户进行比较以确定用户身份是否合法;也需要从Realm得到用户相应的角色/权限进行验证用户是否能进行操作;可以把Realm看成DataSource.即安全数据源.下面演示如何自定义Realm:
自定义Realm类

 public class MyRealm1 implements Realm {  
    @Override  
    public String getName() {  
        return "myrealm1";  
    }  
    @Override  
    public boolean supports(AuthenticationToken token) {  
        //仅支持UsernamePasswordToken类型的Token  
        return token instanceof UsernamePasswordToken;   
    }  
    @Override  
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {  
        String username = (String)token.getPrincipal();  //得到用户名  
        String password = new String((char[])token.getCredentials()); //得到密码  
        if(!"zhang".equals(username)) {  
            throw new UnknownAccountException(); //如果用户名错误  
        }  
        if(!"123".equals(password)) {  
            throw new IncorrectCredentialsException(); //如果密码错误  
        }  
        //如果身份认证验证成功,返回一个AuthenticationInfo实现;  
        return new SimpleAuthenticationInfo(username, password, getName());  
    }  
}

Shiro.ini

#声明一个realm  
myRealm1=com.zx.realm.MyRealm1  
#指定securityManager的realms实现  
securityManager.realms=$myRealm1,$myRealm2

测试类

    @Test
    public void test_Realm() {
        //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", "1234");
        try {
            //4、登录,即身份验证
            subject.login(token);
        } catch (AuthenticationException e) {
            //5、身份验证失败
            e.printStackTrace();
            return;
        }
        logger.info("通过检查");
        //6、退出
        subject.logout();
    }

2.授权

授权,也叫访问控制,即在应用中控制谁能访问哪些资源(如访问页面/编辑数据/页面操作等).可通过修改ini配置文件或自定义Authorizer实现授权.
Authorizer的职责是进行授权(访问控制),是Shiro API中授权核心的入口点,其提供了相应的角色/权限判断接口,具体请参考其Javadoc.SecurityManager继承了Authorizer接口,且提供了ModularRealmAuthorizer用于多Realm时的授权匹配.PermissionResolver用于解析权限字符串到Permission实例,而RolePermissionResolver用于根据角色解析相应的权限集合.

3.根对象SecurityManager与ini配置文件

根对象SecurityManager配置

Shiro是从根对象SecurityManager进行身份验证和授权的;也就是所有操作都是自它开始的l这个对象是线程安全且真个应用只需要一个即可,因此Shiro提供了SecurityUtils让我们绑定它为全局的,方便后续操作.
通过纯Java的方式设置SecurityManager属性

    DefaultSecurityManager securityManager = new DefaultSecurityManager();  
    //设置authenticator  
    ModularRealmAuthenticator authenticator = new ModularRealmAuthenticator();  
    authenticator.setAuthenticationStrategy(new AtLeastOneSuccessfulStrategy());  
    securityManager.setAuthenticator(authenticator);  
    //设置authorizer  
    ModularRealmAuthorizer authorizer = new ModularRealmAuthorizer();  
    authorizer.setPermissionResolver(new WildcardPermissionResolver());  
    securityManager.setAuthorizer(authorizer);  
    //设置Realm  
    DruidDataSource ds = new DruidDataSource();  
    ds.setDriverClassName("com.mysql.jdbc.Driver");  
    ds.setUrl("jdbc:mysql://localhost:3306/shiro");  
    ds.setUsername("root");  
    ds.setPassword("");  
    JdbcRealm jdbcRealm = new JdbcRealm();  
    jdbcRealm.setDataSource(ds);  
    jdbcRealm.setPermissionsLookupEnabled(true);  
    securityManager.setRealms(Arrays.asList((Realm) jdbcRealm));  
    //将SecurityManager设置到SecurityUtils 方便全局使用  
    SecurityUtils.setSecurityManager(securityManager);  
    Subject subject = SecurityUtils.getSubject();  
    UsernamePasswordToken token = new UsernamePasswordToken("zhang", "123");  
    subject.login(token);  
    Assert.assertTrue(subject.isAuthenticated());  

与上面等价的ini配置,并加载SecurityManager

[main]  
#authenticator  
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator  
authenticationStrategy=org.apache.shiro.authc.pam.AtLeastOneSuccessfulStrategy  
authenticator.authenticationStrategy=$authenticationStrategy  
securityManager.authenticator=$authenticator  
#authorizer  
authorizer=org.apache.shiro.authz.ModularRealmAuthorizer  
permissionResolver=org.apache.shiro.authz.permission.WildcardPermissionResolver  
authorizer.permissionResolver=$permissionResolver  
securityManager.authorizer=$authorizer  
#realm  
dataSource=com.alibaba.druid.pool.DruidDataSource  
dataSource.driverClassName=com.mysql.jdbc.Driver  
dataSource.url=jdbc:mysql://localhost:3306/shiro  
dataSource.username=root  
#dataSource.password=  
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm  
jdbcRealm.dataSource=$dataSource  
jdbcRealm.permissionsLookupEnabled=true  
securityManager.realms=$jdbcRealm   
Factory<org.apache.shiro.mgt.SecurityManager> factory =  new IniSecurityManagerFactory("classpath:shiro-config.ini");

ini配置文件结构详解

[main]  
#提供了对根对象securityManager及其依赖的配置  
securityManager=org.apache.shiro.mgt.DefaultSecurityManager    
securityManager.realms=$jdbcRealm  

[users]  
#提供了对用户/密码及其角色的配置,用户名=密码,角色1,角色2  
username=password,role1,role2  
  
[roles]  
#提供了角色及权限之间关系的配置,角色=权限1,权限2  
role1=permission1,permission2  
  
[urls]  
#用于web,提供了对web url拦截相关的配置,url=拦截器[参数],拦截器  
/index.html = anon  
/admin/** = authc, roles[admin], perms["permission1"]  

4.加密

编码/解码

String str = "hello";  
String base64Encoded = Base64.encodeToString(str.getBytes());  
String str2 = Base64.decodeToString(base64Encoded);  
Assert.assertEquals(str, str2);

String str = "hello";  
String base64Encoded = Hex.encodeToString(str.getBytes());  
String str2 = new String(Hex.decode(base64Encoded.getBytes()));  
Assert.assertEquals(str, str2);

Java本地实例

pom.xml

    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.25</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>

log4j.properties

log4j.rootLogger=INFO, stdout,

log4j.appender.stdout=org.apache.log4j.ConsoleAppender
log4j.appender.stdout.layout=org.apache.log4j.PatternLayout
log4j.appender.stdout.layout.ConversionPattern=%d %p [%c] - %m%n

shiro.ini

# =============================================================================
# Tutorial INI configuration
#
# Usernames/passwords are based on the classic Mel Brooks' film "Spaceballs" :)
# =============================================================================

# -----------------------------------------------------------------------------
# Users and their (optional) assigned roles
# username = password, role1, role2, ..., roleN
# 用户名 = 角色1,角色2,角色3
# -----------------------------------------------------------------------------
[users]
root = secret, admin
guest = guest, guest
presidentskroob = 12345, president
darkhelmet = ludicrousspeed, darklord, schwartz
lonestarr = vespa, goodguy, schwartz

# -----------------------------------------------------------------------------
# Roles with assigned permissions
# roleName = perm1, perm2, ..., permN
# 角色名 = 权限1,权限2,权限3
# -----------------------------------------------------------------------------
[roles]
admin = *
schwartz = lightsaber:*
goodguy = winnebago:drive:eagle5

Test_Shiro.java

package shiro;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.ConcurrentAccessException;
import org.apache.shiro.authc.IncorrectCredentialsException;
import org.apache.shiro.authc.UnknownAccountException;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.config.IniSecurityManagerFactory;
import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.subject.Subject;
import org.apache.shiro.util.Factory;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class Test_Shiro {
    private static final transient Logger logger = LoggerFactory.getLogger(Test_Shiro.class);
    @Test
    public void test_1() {

        Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini");
        SecurityManager securityManager = factory.getInstance();
        SecurityUtils.setSecurityManager(securityManager);

        String name = "lonestarr";
        String pass = "vespa";
        Subject currentUser = SecurityUtils.getSubject();

        if(!currentUser.isAuthenticated()) {
            UsernamePasswordToken token = new UsernamePasswordToken(name, pass);
            token.setRememberMe(true);
            try {
                currentUser.login(token);
            } catch (UnknownAccountException e) {
                logger.error(String.format("user not found: %s", name), e);
            } catch(IncorrectCredentialsException e) {
                logger.error(String.format("user: %s pwd: %s error", name, pass), e);
            } catch (ConcurrentAccessException e) {
                logger.error(String.format("user has been authenticated: %s", name), e);
            } catch (AuthenticationException e) {
                logger.error(String.format("account except: %s", name), e);
            }
        }

        logger.info( "User [" + currentUser.getPrincipal() + "] logged in successfully." );

        String role = "schwartz";
        if(currentUser.hasRole(role)) {
            logger.info(String.format("用户: %s 属于角色:%s", name, role));
        }else{
            logger.error(String.format("用户: %s 不属于角色:%s", name, role));
        }

        String perm = "lightsaber:cc:zz";
        if(currentUser.isPermitted(perm)) {
            logger.info(String.format("用户: %s 拥有权限:%s", name, perm));
        }else {
            logger.error(String.format("用户:%s 没有权限:%s", name, perm));
        }

        // 层次化的角色值
        String permInstanceLevel = "winnebago:drive";
        if(currentUser.isPermitted(permInstanceLevel)) {
            logger.info(String.format("用户: %s 拥有权限:%s", name, permInstanceLevel));
        }else {
            logger.error(String.format("用户:%s 没有权限:%s", name, permInstanceLevel));
        }
        currentUser.logout();
    }
}


Similar Posts

下一篇 Mahout简介

Comments