Shiro-00-shiro 概覽

發表於2024-02-11

RBAC

RBCA

RBCA zh_CN

Shiro

Apache Shiro 是一個強大且易於使用的 Java 安全框架,負責執行身份驗證、授權、加密和會話管理。

透過 Shiro 的易於理解的 API,您可以快速而輕鬆地保護任何應用程式,從最小的移動應用到最大的 Web 和企業應用。

Shiro 提供了應用程式安全 API,用於執行以下方面:

  • 身份驗證 - 驗證使用者身份,通常稱為使用者“登入”
  • 授權 - 訪問控制
  • 加密 - 保護或隱藏資料免受窺探
  • 會話管理 - 每個使用者的時限敏感狀態

<uml>

Subject->SecurityManager:
SecurityManager->Realms:

</uml>

shiro

shiro zh_CN

Hello world

  • pom.xml
<dependencies>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.9</version>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.1.3</version>
    </dependency>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.2.2</version>
    </dependency>
</dependencies>
  • shiro.ini
create this file under the classpath.
[users]
ryo=123
wang=123
  • ShiroTest.java
@Test
public void testHelloworld() {
    Factory<SecurityManager> factory =
            new IniSecurityManagerFactory("classpath:shiro.ini");

    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("ryo", "123");

    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        //login falied
    }

   assertEquals(true, subject.isAuthenticated());   //assert user has logined.

   //logout
   subject.logout();
}

Realms

Realms(領域)充當 Shiro 與您的應用程式安全資料之間的“橋樑”或“聯結器”。

當需要實際與安全相關的資料進行互動,例如從使用者帳戶執行身份驗證(登入)和授權(訪問控制)時,Shiro 會查詢配置為應用程式的一個或多個領域中的許多資訊。

  • Realm.java
public interface Realm {
    String getName();   //返回一個唯一的Realm名字

    boolean supports(AuthenticationToken var1); //判斷此Realm是否支援此Token

    AuthenticationInfo getAuthenticationInfo(AuthenticationToken var1) throws AuthenticationException;  //根據Token獲取認證資訊
}
  • MyRealm.java
public class MyRealm implements Realm {
    @Override
    public String getName() {
        return "firstRealm";
    }

    @Override
    public boolean supports(AuthenticationToken authenticationToken) {
        //僅支援UsernamePasswordToken型別的Token
        return authenticationToken instanceof UsernamePasswordToken;
    }

    @Override
    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();      //get username
        String password = new String((char[]) authenticationToken.getCredentials());    //get password
        if (!"ryo".equals(username)) {
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            throw new IncorrectCredentialsException();
        }

        //如果身份認證驗證成功,返回一個AuthenticationInfo實現;
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}
  • shiro-realm.ini

create this file under the classpath.

#declear realm
firstRealm=com.ryo.shiro.MyRealm
#point the realms impls of securityManager
securityManager.realms=$firstRealm
  • test()
@Test
public void testRealm() {
    Factory<SecurityManager> factory =
            new IniSecurityManagerFactory("classpath:shiro-realm.ini");
    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);
    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("ryo", "123");

    try {
        subject.login(token);
    } catch (AuthenticationException e) {
    }

    assertEquals(true, subject.isAuthenticated());

    subject.logout();
}

multi-realm

  • define another realm SecondRealm.java
public class SecondRealm implements Realm {
    public String getName() {
        return "secondRealm";
    }

    public boolean supports(AuthenticationToken authenticationToken) {
        return authenticationToken instanceof UsernamePasswordToken;
    }

    public AuthenticationInfo getAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());
        if (!"wang".equals(username)) {
            throw new UnknownAccountException();
        }
        if (!"123".equals(password)) {
            throw new IncorrectCredentialsException();
        }
        return new SimpleAuthenticationInfo(username, password, getName());
    }
}
  • define shiro-multi-realm.ini
[main]
#define
firstRealm=com.ryo.shiro.FirstRealm
secondRealm=com.ryo.shiro.SecondRealm
#use
securityManager.realms=$firstRealm,$secondRealm
  • test()
@Test
public void testMultiRealm() {
    Factory<SecurityManager> factory =
            new IniSecurityManagerFactory("classpath:shiro-multi-realm.ini");

    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);

    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("wang", "123");

    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        e.printStackTrace();
    }

    Assert.assertEquals(true, subject.isAuthenticated());

    subject.logout();
}

<label class="label label-warning">Notice</label>

The realm worked only after you used it.

JDBC Realm

  • Add jars info your pom.xml, here I user MySQL and druid datasource for test.
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>5.1.25</version>
</dependency>
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>0.2.23</version>
</dependency>
  • Here are some sql to init database.
DROP DATABASE IF EXISTS shiro;
CREATE DATABASE shiro;
USE shiro;

CREATE TABLE users (
  id            BIGINT AUTO_INCREMENT,
  username      VARCHAR(100),
  password      VARCHAR(100),
  password_salt VARCHAR(100),
  CONSTRAINT pk_users PRIMARY KEY (id)
)
  CHARSET = utf8
  ENGINE = InnoDB;
CREATE UNIQUE INDEX idx_users_username ON users (username);

CREATE TABLE user_roles (
  id        BIGINT AUTO_INCREMENT,
  username  VARCHAR(100),
  role_name VARCHAR(100),
  CONSTRAINT pk_user_roles PRIMARY KEY (id)
)
  CHARSET = utf8
  ENGINE = InnoDB;
CREATE UNIQUE INDEX idx_user_roles ON user_roles (username, role_name);

CREATE TABLE roles_permissions (
  id         BIGINT AUTO_INCREMENT,
  role_name  VARCHAR(100),
  permission VARCHAR(100),
  CONSTRAINT pk_roles_permissions PRIMARY KEY (id)
)
  CHARSET = utf8
  ENGINE = InnoDB;
CREATE UNIQUE INDEX idx_roles_permissions ON roles_permissions (role_name, permission);

INSERT INTO users (username, password) VALUES ('wang', '123');
INSERT INTO users (username, password) VALUES ('ryo', '123');
  • shiro-jdbc-realm.ini
[main]
jdbcRealm=org.apache.shiro.realm.jdbc.JdbcRealm
dataSource=com.alibaba.druid.pool.DruidDataSource
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql://localhost:3307/shiro
dataSource.username=root
dataSource.password=${youOwnSQLPassword}
jdbcRealm.dataSource=$dataSource
securityManager.realms=$jdbcRealm


;1、varName=className    auto create an instance of class.
;2、varName.property=val         auto call the set()
;3、$varname             reference an object define before;
  • test()
@Test
public void testJDBCRealm() {
    Factory<org.apache.shiro.mgt.SecurityManager> factory =
            new IniSecurityManagerFactory("classpath:shiro-jdbc-realm.ini");

    org.apache.shiro.mgt.SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);

    Subject subject = SecurityUtils.getSubject();
    UsernamePasswordToken token = new UsernamePasswordToken("ryo", "123");

    try {
        subject.login(token);
    } catch (AuthenticationException e) {
        e.printStackTrace();
    }

    Assert.assertEquals(true, subject.isAuthenticated());

    subject.logout();
}

Authenticator

  • 在單領域應用程式中,ModularRealmAuthenticator 將直接呼叫單個領域。
  • 如果您希望使用自定義的 Authenticator 實現配置 SecurityManager,您可以在 shiro.ini 中進行配置,例如:
[main]
authenticator = com.foo.bar.CustomAuthenticator

securityManager.authenticator = $authenticator
SecurityManager.java
public interface SecurityManager extends Authenticator, Authorizer, SessionManager {
}
Authenticator.java
public interface Authenticator {
    AuthenticationInfo authenticate(AuthenticationToken var1) throws AuthenticationException;
}

AuthenticationStrategy

當為應用程式配置了兩個或更多領域時,ModularRealmAuthenticator 依賴於內部的 AuthenticationStrategy 元件來確定身份驗證嘗試成功或失敗的條件。

AuthenticationStrategy 類 描述
AtLeastOneSuccessfulStrategy 如果一個或多個領域成功驗證,則將考慮整體嘗試成功。如果沒有一個驗證成功,則嘗試失敗。
FirstSuccessfulStrategy 僅使用從第一個成功驗證的領域返回的資訊。所有後續領域將被忽略。如果沒有一個驗證成功,則嘗試失敗。
AllSuccessfulStrategy 所有配置的領域必須成功驗證才能考慮整體嘗試成功。如果有任何一個驗證不成功,則嘗試失敗。

1、這裡定義了三個用於測試的領域:

  • FirstRealm ryo/123 成功,返回 ryo/123
  • SecondRealm wang/123 成功,返回 wang/123
  • ThirdRealm ryo/123 成功,返回 ryo@gmail.com/123

2、shiro-authenticator-all-success.ini

ModularRealmAuthenticator 預設使用 AtLeastOneSuccessfulStrategy 實現,因為這是最常用的策略。

但是,如果需要,您可以配置不同的策略。

[main]
#point out securityManager's authenticator  
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator  
securityManager.authenticator=$authenticator  
  
#Point out securityManager.authenticator's authenticationStrategy  
allSuccessfulStrategy=org.apache.shiro.authc.pam.AllSuccessfulStrategy  
securityManager.authenticator.authenticationStrategy=$allSuccessfulStrategy  

#Define and use realms
firstRealm=com.ryo.shiro.FirstRealm
secondRealm=com.ryo.shiro.SecondRealm
thirdRealm=com.ryo.shiro.ThirdRealm
securityManager.realms=$firstRealm,$thirdRealm

3、AuthenticatorTest.java

@Test
public void testAllSuccessfulStrategyWithSuccess() {
    Subject subject = getSubjectByPath("classpath:shiro-authenticator-all-success.ini");

    UsernamePasswordToken token = new UsernamePasswordToken("ryo", "123");
    subject.login(token);

    PrincipalCollection principalCollection = subject.getPrincipals();
    assertEquals("ryo,ryo@gmail.com", principalCollection.toString());
}

private Subject getSubjectByPath(String configFilePath) {
    Factory<SecurityManager> factory = new IniSecurityManagerFactory(configFilePath);

    SecurityManager securityManager = factory.getInstance();
    SecurityUtils.setSecurityManager(securityManager);
    return SecurityUtils.getSubject();
}

<label class="label label-info">提示</label>

如果您想自己建立自己的 AuthenticationStrategy 實現,您可以使用 org.apache.shiro.authc.pam.AbstractAuthenticationStrategy 作為起點。

  • OnlyOneAuthenticatorStrategy.java
public class OnlyOneAuthenticatorStrategy extends AbstractAuthenticationStrategy {
    //Simply returns the aggregate argument without modification.
    @Override
    public AuthenticationInfo beforeAllAttempts(Collection<? extends Realm> realms, AuthenticationToken token) throws AuthenticationException {
        return super.beforeAllAttempts(realms, token);
    }

    //Simply returns the aggregate method argument, without modification.
    @Override
    public AuthenticationInfo beforeAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
        return super.beforeAttempt(realm, token, aggregate);
    }

    /**
     * Base implementation that will aggregate the specified singleRealmInfo into the aggregateInfo and then returns the aggregate.
     * @param realm the realm that was just consulted for AuthenticationInfo for the given token.
     * @param token the AuthenticationToken submitted for the subject attempting system log-in.
     * @param singleRealmInfo the info returned from a single realm.    單個realm資訊
     * @param aggregateInfo the aggregate info representing all realms in a multi-realm environment.    總資訊
     * @param t the Throwable thrown by the Realm during the attempt, or null if the method returned normally.
     * @return
     * @throws AuthenticationException
     */
    @Override
    public AuthenticationInfo afterAttempt(Realm realm, AuthenticationToken token, AuthenticationInfo singleRealmInfo, AuthenticationInfo aggregateInfo, Throwable t) throws AuthenticationException {
        AuthenticationInfo info;

        if(singleRealmInfo == null) {
            info = aggregateInfo;
        } else if(aggregateInfo == null) {
            info = singleRealmInfo;
        } else {
            info = merge(singleRealmInfo, aggregateInfo);

            if(info.getPrincipals().getRealmNames().size() > 1) {
                throw new AuthenticationException("Authentication token of type [" + token.getClass() + "] " +
                        "could not be authenticated by any configured realms.  Please ensure that only one realm can " +
                        "authenticate these tokens.");
            }
        }

        return info;
    }

    //Merges the specified info argument into the aggregate argument and then returns an aggregate for continued use throughout the login process.
    @Override
    protected AuthenticationInfo merge(AuthenticationInfo info, AuthenticationInfo aggregate) {
        return super.merge(info, aggregate);
    }

    //Base implementation that will aggregate the specified singleRealmInfo into the aggregateInfo and then returns the aggregate.
    @Override
    public AuthenticationInfo afterAllAttempts(AuthenticationToken token, AuthenticationInfo aggregate) throws AuthenticationException {
        return super.afterAllAttempts(token, aggregate);
    }
}
  • shiro-authenticator-onlyone-success.ini
[main]
authenticator=org.apache.shiro.authc.pam.ModularRealmAuthenticator
securityManager.authenticator=$authenticator

onlyOneAuthenticatorStrategy=com.ryo.shiro.authenticator.strategy.OnlyOneAuthenticatorStrategy
securityManager.authenticator.authenticationStrategy=$onlyOneAuthenticatorStrategy

#define and uer realms.
firstRealm=com.ryo.shiro.FirstRealm
secondRealm=com.ryo.shiro.SecondRealm
securityManager.realms=$firstRealm,$secondRealm
  • test()
@Test
    public void testOnlyOneAuthenticatorStrategy() {
        Subject subject = getSubjectByPath("classpath:shiro-authenticator-onlyone-success.ini");

        UsernamePasswordToken token = new UsernamePasswordToken("ryo", "123");
        subject.login(token);

        PrincipalCollection principalCollection = subject.getPrincipals();
        assertEquals("ryo", principalCollection.toString());
    }
  • if you change the shiro-authenticator-onlyone-success.ini into
#define and uer realms.
firstRealm=com.ryo.shiro.FirstRealm
thirdRealm=com.ryo.shiro.ThirdRealm
securityManager.realms=$firstRealm,$thirdRealm

You will get an error as following.

org.apache.shiro.authc.AuthenticationException: Authentication token of type [class org.apache.shiro.authc.UsernamePasswordToken] 
could not be authenticated by any configured realms.  Please ensure that only one realm can authenticate these tokens.
本文由部落格一文多發平臺 OpenWrite 釋出!