1. 程式人生 > >跟我學Shiro實踐-簡單的認證授權

跟我學Shiro實踐-簡單的認證授權

pass 哪些 .com jin 怎麽 don AR ejb from

本文是基於張開濤老師的跟我學Shiro系列的個人實踐。幾個月前過了一遍張開濤的跟我學Shiro系列,因為沒有實踐,基本上又全部還給開濤老師了。趁著假期,這次準備將開濤老師的講解實踐一遍。當然本人的實踐不會與開濤的實例完全相同,不然就沒必要在寫一遍了。

本文只會對相關必要的Shiro概念說明下,建議有時間可以閱讀下開濤的Shiro系列:http://jinnianshilongnian.iteye.com/blog/2018398

Shiro的架構和簡單的認證授權

1.1 Shiro架構和組件介紹

技術分享圖片

Apache Shiro是Java的一個權限安全框架,與之類似的還有Spring Security,如果只是簡單應用的話我還是比較推薦Shiro的,因為Spring Security的集成我總感覺是太過復雜。

Subject主體,可以看到主體可以是任何可以與應用交互的“用戶”

SecurityManager相當於SpringMVC中的DispatcherServlet或者Struts2中的FilterDispatcher;是Shiro的心臟;所有具體的交互都通過SecurityManager進行控制;它管理著所有Subject、且負責進行認證和授權、及會話、緩存的管理。

Authenticator認證器,負責主體認證的,這是一個擴展點,如果用戶覺得Shiro默認的不好,可以自定義實現;其需要認證策略(Authentication Strategy),即什麽情況下算用戶認證通過了;

Authrizer授權器,或者訪問控制器,用來決定主體是否有權限進行相應的操作;即控制著用戶能訪問應用中的哪些功能;

Realm可以有1個或多個Realm,可以認為是安全實體數據源,即用於獲取安全實體的;可以是JDBC實現,也可以是LDAP實現,或者內存實現等等;由用戶提供;註意:Shiro不知道你的用戶/權限存儲在哪及以何種格式存儲;所以我們一般在應用中都需要實現自己的Realm;

SessionManager如果寫過Servlet就應該知道Session的概念,Session呢需要有人去管理它的生命周期,這個組件就是SessionManager;而Shiro並不僅僅可以用在Web環境,也可以用在如普通的JavaSE環境、EJB等環境;所有呢,Shiro就抽象了一個自己的Session來管理主體與應用之間交互的數據;這樣的話,比如我們在Web環境用,剛開始是一臺Web服務器;接著又上了臺EJB服務器;這時想把兩臺服務器的會話數據放到一個地方,這個時候就可以實現自己的分布式會話(如把數據放到Memcached服務器);

SessionDAODAO大家都用過,數據訪問對象,用於會話的CRUD,比如我們想把Session保存到數據庫,那麽可以實現自己的SessionDAO,通過如JDBC寫到數據庫;比如想把Session放到Memcached中,可以實現自己的Memcached SessionDAO;另外SessionDAO中可以使用Cache進行緩存,以提高性能;

CacheManager緩存控制器,來管理如用戶、角色、權限等的緩存的;因為這些數據基本上很少去改變,放到緩存中後可以提高訪問的性能

Cryptography密碼模塊,Shiro提高了一些常見的加密組件用於如密碼加密/解密的。

1.2 認證和授權的流程

認證:

技術分享圖片

1、創建SecurityManager環境

2、Subject主體提交認證

3、SecurityManager認證

4、Authenticator認證器認證

5、Realm驗證

授權:

1、創建SecurityManager環境

2、Subject主體提交授權

3、SecurityManager授權

4、Authrizer授權

5、Realm訪問數據庫獲取權限數據

maven依賴

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
        </dependency>

        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
</dependencies>

1.3 SimpleAccountRealm認證代碼

import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.SimpleAccountRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Before;
import org.junit.Test;

public class LoginTest {
    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
    @Before
    public void addUser() {
        simpleAccountRealm.addAccount("laoyeye", "123456");
    }

    @Test
    public void testAuthentication() {
        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);
        
        // 2.Subject主體提交認證
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        
        UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456");
        subject.login(token);
        System.out.println("是否登錄:" + subject.isAuthenticated());
        subject.logout();
        System.out.println("是否登錄:" + subject.isAuthenticated());
    }
}

結果:

是否登錄:true
是否登錄:false

我們上面在上面流程中說有五步,代碼中怎麽只看到三步就登陸成功了呢,這裏我們看下源碼中內容就容易理解了。

public void login(AuthenticationToken token) throws AuthenticationException {
        clearRunAsIdentitiesInternal();
        Subject subject = securityManager.login(this, token);
        PrincipalCollection principals;

        String host = null;

        if (subject instanceof DelegatingSubject) {
            DelegatingSubject delegating = (DelegatingSubject) subject;
            //we have to do this in case there are assumed identities - we don‘t want to lose the ‘real‘ principals:
            principals = delegating.principals;
            host = delegating.host;
        } else {
            principals = subject.getPrincipals();
        }

        if (principals == null || principals.isEmpty()) {
            String msg = "Principals returned from securityManager.login( token ) returned a null or " +
                    "empty value.  This value must be non null and populated with one or more elements.";
            throw new IllegalStateException(msg);
        }
        this.principals = principals;
        this.authenticated = true;
        if (token instanceof HostAuthenticationToken) {
            host = ((HostAuthenticationToken) token).getHost();
        }
        if (host != null) {
            this.host = host;
        }
        Session session = subject.getSession(false);
        if (session != null) {
            this.session = decorate(session);
        } else {
            this.session = null;
        }
    }

首先我們的主體subject.login(token)的時候提交了認證,SecurityManager開始進入認證,然後進入到下面這裏。

   public AuthenticationInfo authenticate(AuthenticationToken token) throws AuthenticationException {
        return this.authenticator.authenticate(token);
    }

Authenticator認證器開始認證工作。接著往下走。。

    protected AuthenticationInfo doAuthenticate(AuthenticationToken authenticationToken) throws AuthenticationException {
        assertRealmsConfigured();
        Collection<Realm> realms = getRealms();
        if (realms.size() == 1) {
            return doSingleRealmAuthentication(realms.iterator().next(), authenticationToken);
        } else {
            return doMultiRealmAuthentication(realms, authenticationToken);
        }
    }

可以看到我們的Authenticator開始去取我們代碼剛開始設置的Realm數據了,然整個流程就走通了。

授權

接著上面的代碼,我們添加下面紅色標註的一行代碼

public class LoginTest {
    SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
    @Before
    public void addUser() {
        simpleAccountRealm.addAccount("laoyeye", "123456","admin");
    }
    

    @Test
    public void testAuthentication() {
        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(simpleAccountRealm);
        
        // 2.Subject主體提交認證
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        
        UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456");
        subject.login(token);
        System.out.println("是否登錄:" + subject.isAuthenticated());
        
        subject.checkRoles("admin");    //授權
        
        subject.logout();
        System.out.println("是否登錄:" + subject.isAuthenticated());
    }
}

結果:

org.apache.shiro.authz.UnauthorizedException: Subject does not have role [admin]
    at org.apache.shiro.authz.ModularRealmAuthorizer.checkRole(ModularRealmAuthorizer.java:421)
    at org.apache.shiro.authz.ModularRealmAuthorizer.checkRoles(ModularRealmAuthorizer.java:440)
    at org.apache.shiro.mgt.AuthorizingSecurityManager.checkRoles(AuthorizingSecurityManager.java:173)
    at org.apache.shiro.subject.support.DelegatingSubject.checkRoles(DelegatingSubject.java:250)
    at com.allan.test.LoginTest.testAuthentication(LoginTest.java:33)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:606)
    at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
    at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
    at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
    at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
    at org.junit.internal.runners.statements.RunBefores.evaluate(RunBefores.java:28)
    at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:263)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:69)
    at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:48)
    at org.junit.runners.ParentRunner$3.run(ParentRunner.java:231)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:60)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:229)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:50)
    at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:222)
    at org.junit.runners.ParentRunner.run(ParentRunner.java:292)
    at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:50)
    at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:38)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:467)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:683)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:390)
    at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:197)

報錯說沒有admin用戶,這時候我們在

 simpleAccountRealm.addAccount("laoyeye", "123456","admin");

就正常了。

授權的流程基本上和認證差不多,大家可以自行看下源碼。

1.4 JdbcRealm的實現

maven依賴

<dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.9</version>
        </dependency>
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-core</artifactId>
            <version>1.4.0</version>
        </dependency>
        <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>
    </dependencies>
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.authc.UsernamePasswordToken;
import org.apache.shiro.mgt.DefaultSecurityManager;
import org.apache.shiro.realm.jdbc.JdbcRealm;
import org.apache.shiro.subject.Subject;
import org.junit.Test;

public class JdbcRealmTest {
    DruidDataSource dataSource = new DruidDataSource();
    {
        dataSource.setUrl("jdbc:mysql://xxxxxx:3306/shiro");
        dataSource.setUsername("root");
        dataSource.setPassword("xxxx");
    }
    @Test
    public void testAuthentication() {
        JdbcRealm jdbcRealm = new JdbcRealm();
        jdbcRealm.setDataSource(dataSource);
        //開啟角色權限
        jdbcRealm.setPermissionsLookupEnabled(true);
        
        //自定義SQL
        //認證查詢
        jdbcRealm.setAuthenticationQuery("select password from test_users where username = ?");
        //用戶角色查詢
        jdbcRealm.setUserRolesQuery("select role_name from test_user_roles where username = ?");
        
        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(jdbcRealm);
        
        // 2.主題提交認證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        
        UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456");
        subject.login(token);
        System.out.println("是否登錄:" + subject.isAuthenticated());
        
        subject.checkRoles("admin","user");    //授權
        subject.checkPermission("user:select");    //權限
        
        subject.logout();
        System.out.println("是否登錄:" + subject.isAuthenticated());
    }

}

1.5 自定義Realm的實現

public class CustomRealm extends AuthorizingRealm {
    Map<String, String> userMap = new HashMap<>();
    //模擬數據庫或緩存的數據
    {
//        Md5Hash md5 = new Md5Hash("123456");    //加密
        Md5Hash md5 = new Md5Hash("123456","laoyeyesalt");//加鹽
        userMap.put("laoyeye", md5.toString());
//        userMap.put("laoyeye", "123456");
        super.setName("customRealm");
    }
    
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = (String) principals.getPrimaryPrincipal();
        // 從數據庫或者緩存中獲得角色數據
        Set<String> roles = getRolesByUserName(username);
        Set<String> permissions = getPermissionsByUserName(username);
        
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        
        return simpleAuthorizationInfo;
    }

    private Set<String> getPermissionsByUserName(String username) {
        Set<String> sets = new HashSet<>();
        sets.add("user:delete");
        sets.add("user:add");
        return sets;
    }

    private Set<String> getRolesByUserName(String username) {
        Set<String> sets = new HashSet<>();
        sets.add("admin");
        sets.add("user");
        return sets;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 1.從主體傳過來的認證信息中,獲得用戶名
        String username = (String) token.getPrincipal();
        
        // 2.通過用戶名到數據庫中獲取憑證
        String password = getPasswordByUsername(username);
        if(password == null) {
            return null;
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo("laoyeye", password, "customRealm");
        //加鹽
        simpleAuthenticationInfo.setCredentialsSalt(ByteSource.Util.bytes("laoyeyesalt"));
        return simpleAuthenticationInfo;
    }

    private String getPasswordByUsername(String username) {
        return userMap.get(username);
    }

}

public class CustomRealmTest {
    
    CustomRealm customRealm = new CustomRealm();
    
    
    @Test
    public void testAuthentication() {
        // 1.構建SecurityManager環境
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
        defaultSecurityManager.setRealm(customRealm);
        
        //聲明CustomRealm使用了Md5加密
        HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
        matcher.setHashAlgorithmName("md5");
        matcher.setHashIterations(1);
        customRealm.setCredentialsMatcher(matcher);
        
        // 2.主題提交認證請求
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        Subject subject = SecurityUtils.getSubject();
        
        UsernamePasswordToken token = new UsernamePasswordToken("laoyeye", "123456");
        subject.login(token);
        System.out.println("是否登錄:" + subject.isAuthenticated());
        
        subject.checkRoles("admin");    //授權
        subject.checkPermission("user:delete");
        
        subject.logout();
        System.out.println("是否登錄:" + subject.isAuthenticated());
    }
    
}

跟我學Shiro實踐-簡單的認證授權