一、使用 1.搭建基礎環境 (1)導入 Spring 和 Shiro 的 Jar 包 正常導入 spring jar包 導入日誌包 導入 shiro 包 (2)配置文件 web.xml spring-shiro.xml spring.xml 正常配置即可。 springmvc.xml 正常配置即可。 ...
一、使用
1.搭建基礎環境
(1)導入 Spring 和 Shiro 的 Jar 包
- 正常導入 spring jar包
- 導入日誌包
- log4j-1.2.15.jar
- slf4j-api-1.6.1.jar
- slf4j-log4j12-1.6.1.jar
- 導入 shiro 包
- shiro-core-1.2.2.jar
- shiro-ehcache-1.2.2.jar
- shiro-spring-1.2.2.jar
- shiro-web-1.2.2.jar
(2)配置文件
- web.xml
- 讀取所有配置文件
- springmvc 的 DispatcherServlet 配置
- shiroFilter 的配置(該配置參考的是:shiro-root-1.2.2\samples\spring\src\main\webapp\WEB-INF\web.xml)
- 見文章末的兩個 web.xml
- spring-shiro.xml
- 該配置參考:shiro-root-1.2.2\samples\spring\src\main\webapp\WEB-INF\applicationContext.xml
- 此次實驗在 shiro.xml 文件中採用的緩存管理器是 ehcache 。需要額外導入ehcache jar包和配置文件。
- ehcache 使用的 Hibernate 下的。
- ehcache-core-2.4.3.jar(hibernate-release-4.2.4.Final\lib\optional\ehcache\) 和 ehcache.xml(hibernate-release-4.2.4.Final\project\etc\)
- 見文章末的兩個 spring-shiro.xml
- 註意:(1)緩存的配置(2)自定義Realm
- spring.xml 正常配置即可。
- springmvc.xml 正常配置即可。
(3)檢測
- 添加自定義 Realm,需要繼承自 AuthorizingRealm(即包含認證,也包含授權的 Realm)
- 添加了一個空的自定義的 Realm ,沒有添加認證和授權邏輯。
- 啟動項目,檢測能否正常啟動,檢測配置是否正確。
2.登錄
(1)添加登錄頁面
<%@ page contentType="text/html;charset=UTF-8" language="java" %> <html> <head> <title>Title</title> </head> <body> <h4> Login Page </h4> <form action="shiro-login" method="post"> <input name="userName" type="text"/> <input name="password" type="password"/> <input type="submit" value="submit"> </form> </body> </html>login.jsp
(2)對應 Handler 方法
@RequestMapping("/shiro-login") public String login(String userName, String password) { System.out.println("userName:" + userName + ", password:" + password); Subject currentUser = SecurityUtils.getSubject(); if(!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken(userName, password); token.setRememberMe(true); try { currentUser.login(token); } catch(UnknownAccountException uae) { System.out.println("用戶名不正確!"); } catch(IncorrectCredentialsException ice) { System.out.println("密碼不匹配!"); } catch(LockedAccountException lae) { System.out.println("賬戶被鎖定!"); } catch(AuthenticationException ae) { System.out.println("認證失敗!"); } } return "success"; }
這裡參考的是官方的 demo :shiro-root-1.2.2\samples\quickstart\src\main\java\Quickstart.java
/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.config.IniSecurityManagerFactory; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.session.Session; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.Factory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Simple Quickstart application showing how to use Shiro's API. * * @since 0.9 RC2 */ public class Quickstart { private static final transient Logger log = LoggerFactory.getLogger(Quickstart.class); public static void main(String[] args) { // The easiest way to create a Shiro SecurityManager with configured // realms, users, roles and permissions is to use the simple INI config. // We'll do that by using a factory that can ingest a .ini file and // return a SecurityManager instance: // Use the shiro.ini file at the root of the classpath // (file: and url: prefixes load from files and urls respectively): Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro.ini"); SecurityManager securityManager = factory.getInstance(); // for this simple example quickstart, make the SecurityManager // accessible as a JVM singleton. Most applications wouldn't do this // and instead rely on their container configuration or web.xml for // webapps. That is outside the scope of this simple quickstart, so // we'll just do the bare minimum so you can continue to get a feel // for things. SecurityUtils.setSecurityManager(securityManager); // Now that a simple Shiro environment is set up, let's see what you can do: // get the currently executing user: Subject currentUser = SecurityUtils.getSubject(); // Do some stuff with a Session (no need for a web or EJB container!!!) Session session = currentUser.getSession(); session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); if (value.equals("aValue")) { log.info("-->Retrieved the correct value! [" + value + "]"); } // let's login the current user so we can check against roles and permissions: if (!currentUser.isAuthenticated()) { UsernamePasswordToken token = new UsernamePasswordToken("lonestarr", "vespa"); token.setRememberMe(true); try { currentUser.login(token); } catch (UnknownAccountException uae) { log.info("-->There is no user with username of " + token.getPrincipal()); } catch (IncorrectCredentialsException ice) { log.info("-->Password for account " + token.getPrincipal() + " was incorrect!"); } catch (LockedAccountException lae) { log.info("The account for username " + token.getPrincipal() + " is locked. " + "Please contact your administrator to unlock it."); } // ... catch more exceptions here (maybe custom ones specific to your application? catch (AuthenticationException ae) { //unexpected condition? error? } } //say who they are: //print their identifying principal (in this case, a username): log.info("-->User [" + currentUser.getPrincipal() + "] logged in successfully."); //test a role: if (currentUser.hasRole("schwartz")) { log.info("-->May the Schwartz be with you!"); } else { log.info("Hello, mere mortal."); } //test a typed permission (not instance-level) if (currentUser.isPermitted("lightsaber:weild")) { log.info("-->You may use a lightsaber ring. Use it wisely."); } else { log.info("Sorry, lightsaber rings are for schwartz masters only."); } //a (very powerful) Instance Level permission: if (currentUser.isPermitted("winnebago:drive:eagle5")) { log.info("-->You are permitted to 'drive' the winnebago with license plate (id) 'eagle5'. " + "Here are the keys - have fun!"); } else { log.info("Sorry, you aren't allowed to drive the 'eagle5' winnebago!"); } //all done - log out! currentUser.logout(); System.exit(0); } }Quickstart.java
說明一下:
官方 demo 演示的是一個 java 項目,而不是一個 web 項目,不同點是:web 項目下,shiro 的大管家是由容器去創建的,而不需要我們手動去獲取。
(3)檢測能否正常運行,若能,則證明登錄測試成功。
3.認證
(1)實現自定義 Realm 的 認證方法。
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("開始認證!"); UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); Object credentials = "123456"; SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username, credentials, this.getName()); return info; }
或
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("開始認證!"); UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = new User(); user.setId(1000); user.setUserName(username); user.setPassword("123456"); user.getRoleNames().add("admin"); return new SimpleAuthenticationInfo(user, user.getPassword(), this.getName()); }
說明一下:
其中 username 是從頁面獲取到的,而密碼是通過查詢資料庫獲取的。註意標紅加粗的地方。
實現的參考:org.apache.shiro.realm.jdbc.JdbcRealm
(2)測試,此時密碼不為 "123456"能否登錄。測試認證是否成功。
4.授權
(1)實現自定義 Realm 的授權方法。
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("開始授權!"); String username = (String)this.getAvailablePrincipal(principalCollection); System.out.println("userName:" + username); Set<String> roleNames = new HashSet<>(); roleNames.add("admin"); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(roleNames); return info; }
或:User 對象已經包含角色信息,不需要再次查詢資料庫。
@Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { System.out.println("開始授權!"); User user = (User) principalCollection.getPrimaryPrincipal(); Set<String> roleNames = user.getRoleNames(); return new SimpleAuthorizationInfo(roleNames); }
實現的參考:org.apache.shiro.realm.jdbc.JdbcRealm
(2)測試,添加對應的頁面,然後在 Shiro 配置文件中配置訪問對應的頁面需要什麼樣的角色才能訪問。
5.認證和授權的資源數據從資料庫中獲取
(1)受保護資源和需要角色許可權間的關係存在在 shiro.xml 文件中。
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitions"> <value> /user.jsp = authc /admin.jsp = roles[admin] /** = anon </value> </property> </bean>
(2)要想改為從資料庫中獲取,思路就是:filterChainDefinitions 屬性值能從 Java 文件中獲取。
參看:filterChainDefinitions 屬性的官方使用
public void setFilterChainDefinitions(String definitions) { Ini ini = new Ini(); ini.load(definitions); Section section = ini.getSection("urls"); if(CollectionUtils.isEmpty(section)) { section = ini.getSection(""); } this.setFilterChainDefinitionMap(section); } public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) { this.filterChainDefinitionMap = filterChainDefinitionMap; }
實際上放的是一個 Map。那麼來看看具體的 Map 存放的是怎麼的一些數據格式?在標紅的代碼處打個斷點,可以看到如下內容:
可以看到, Map 內容的就是在 shiro.xml 通過屬性 filterChainDefinitions 定義的值。
(3)具體操作
/** * @author solverpeng * @create 2016-09-21-18:59 */ public class FilterChainDefinitionMapBuilder { public Map<String, String> getFilterChainDefinitionMap() { Map<String, String> filterChainDefinitionMap = new HashMap<>(); filterChainDefinitionMap.put("/admin.jsp", "roles[admin],authc"); filterChainDefinitionMap.put("/user.jsp", "authc"); filterChainDefinitionMap.put("/**", "anon"); return filterChainDefinitionMap; } }
更改配置:
<bean id="filterChainDefinitionMapBuilder" class="com.nucsoft.shiro.shiro.FilterChainDefinitionMapBuilder"/> <bean id="filterChainDefinitionMap" factory-bean="filterChainDefinitionMapBuilder" factory-method="getFilterChainDefinitionMap"/> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login.jsp"/> <property name="unauthorizedUrl" value="/unauthorized.jsp"/> <property name="filterChainDefinitionMap" ref="filterChainDefinitionMap"/> </bean>
此時,資源信息可以通過 Java 方法來處理,而此時,這些數據就可以從資料庫中獲取。
6.鹽值加密
加密指的是對密碼的加密,用戶註冊時,將密碼使用一定的加密方式存放到資料庫中,用戶登錄的時候,同樣以相同的加密方式進行比對。
在什麼地方進行的對比?
註意:不是在自定義認證的時候對比的。在 MyRealm 中
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("開始認證!"); UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = new User(); user.setId(1000); user.setUserName(username); user.setPassword("42e56621cf3adc9ecc261936188d31d7"); user.getRoleNames().add("admin"); String hashedCredentials = user.getPassword(); ByteSource credentialsSalt = ByteSource.Util.bytes("abcd"); String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, hashedCredentials, credentialsSalt, realmName); return info; }
的這個方法的作用,只是根據傳入的 Token 獲取到 username,從而到資料庫中查詢對應的 User 對象,以及鹽值信息,然後返回封裝這些信息的 SimpleAuthenticationInfo 的對象。
密碼的對比是在這之後對比的,來看返回後,返回到了 org.apache.shiro.realm.AuthenticatingRealm#getAuthenticationInfo 這個方法
public final AuthenticationInfo getAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { AuthenticationInfo info = getCachedAuthenticationInfo(token); if (info == null) { //otherwise not cached, perform the lookup: info = doGetAuthenticationInfo(token); log.debug("Looked up AuthenticationInfo [{}] from doGetAuthenticationInfo", info); if (token != null && info != null) { cacheAuthenticationInfoIfPossible(token, info); } } else { log.debug("Using cached authentication info [{}] to perform credentials matching.", info); } if (info != null) { assertCredentialsMatch(token, info); } else { log.debug("No AuthenticationInfo found for submitted AuthenticationToken [{}]. Returning null.", token); } return info; }
其中第一處標紅的地方是調用我們自定義 Realm 的 doGetAuthenticationInfo() 方法以及返回值。
第二處標紅的地方表示如果可以緩存的話,就把此登錄賬戶進行緩存。
第三處才是真正進行比較的地方,看看方法名和參數,見名知意。 token 為表單提交過來的用戶信息,而 info 是我們做認證時從資料庫查詢獲取到的。
詳細來看:org.apache.shiro.realm.AuthenticatingRealm#assertCredentialsMatch
protected void assertCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) throws AuthenticationException { CredentialsMatcher cm = getCredentialsMatcher(); if (cm != null) { if (!cm.doCredentialsMatch(token, info)) { //not successful - throw an exception to indicate this: String msg = "Submitted credentials for token [" + token + "] did not match the expected credentials."; throw new IncorrectCredentialsException(msg); } } else { throw new AuthenticationException("A CredentialsMatcher must be configured in order to verify " + "credentials during authentication. If you do not wish for credentials to be examined, you " + "can configure an " + AllowAllCredentialsMatcher.class.getName() + " instance."); } }
兩個核心的地方:
第一處標紅是:獲取憑證的匹配器(密碼的匹配器),來看這個介面中封裝了一下什麼信息。
public interface CredentialsMatcher { boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info); }
只有一個 doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) 的方法。
來看它的體系:
看到了 Md5CredentialsMatcher, 然後發現它是一個過期的類,HashedCredentialsMatcher 作為對它的一個替代,需要 setHashAlgorithmName() 來指定加密方式。
這裡直接對 HashedCredentialsMatcher 給出說明:
(1)加密方式:setHashAlgorithmName(String hashAlgorithmName)
(2)加密次數:setHashIterations(int hashIterations)
說了這麼多,如何由我們自己指定 CreaentialsMatcher ?
我自定義的 MyRealm 是 AuthenticatingRealm 它的子類,所以想法是,在 AuthenticatingRealm 調用 getCredentialsMatcher() 之前,就將 CreaentialsMatcher set到 Realm 中。
來看具體操作:
在 MyRealm 中定義一個初始化方法:
public void initCredentialsMatcher() { HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher(); credentialsMatcher.setHashAlgorithmName("MD5"); credentialsMatcher.setHashIterations(1000); setCredentialsMatcher(credentialsMatcher); }
指定了加密方式,然後加密次數,然後設置到了 Realm 中。
為了保證在在 AuthenticatingRealm 調用 getCredentialsMatcher() 之前,就將 CreaentialsMatcher set到 Realm 中,在容器初始化的時候就設置。
applicationContext-shiro.xml 的配置:
<bean id="realm" class="com.nucsoft.shiro.shiro.MyRealm" init-method="initCredentialsMatcher"/>
第二處標紅進行的真正的密碼匹配:cm.doCredentialsMatch(token, info)
詳細來看:org.apache.shiro.authc.credential.HashedCredentialsMatcher#doCredentialsMatch
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { Object tokenHashedCredentials = hashProvidedCredentials(token, info); Object accountCredentials = getCredentials(info); return equals(tokenHashedCredentials, accountCredentials); }
其中 tokenHashedCredentials 是從表單提交過來的密碼經同樣的加密方式處理後的憑證;accountCredentials 是從資料庫中取出來的憑證信息。
關註點:是怎麼對錶單密碼進行加密處理的
詳細來看:
org.apache.shiro.authc.credential.HashedCredentialsMatcher#hashProvidedCredentials(org.apache.shiro.authc.AuthenticationToken, org.apache.shiro.authc.AuthenticationInfo)
protected Object hashProvidedCredentials(AuthenticationToken token, AuthenticationInfo info) { Object salt = null; if (info instanceof SaltedAuthenticationInfo) { salt = ((SaltedAuthenticationInfo) info).getCredentialsSalt(); } else { //retain 1.0 backwards compatibility: if (isHashSalted()) { salt = getSalt(token); } } return hashProvidedCredentials(token.getCredentials(), salt, getHashIterations()); }
org.apache.shiro.authc.credential.HashedCredentialsMatcher#hashProvidedCredentials(java.lang.Object, java.lang.Object, int)
protected Hash hashProvidedCredentials(Object credentials, Object salt, int hashIterations) { String hashAlgorithmName = assertHashAlgorithmName(); return new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); }
hashAlgorithmName:這個就是在憑證匹配器中定義的加密方式。
核心:進行加密的就是這個:
new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations)
hashAlgorithmName: 加密方式
credentials:表單提交的密碼
salt:鹽值
hashIterations:加密次數
用戶註冊的時候,向資料庫存入密碼的時候,可以使用此種方式對密碼進行加密。
來看一個測試:
public static void main(String[] args) { String hashAlgorithmName = "MD5"; Object credentials = "123456"; ByteSource salt = ByteSource.Util.bytes("abcd"); int hashIterations = 1000; SimpleHash simpleHash = new SimpleHash(hashAlgorithmName, credentials, salt, hashIterations); System.out.println(simpleHash); }
說了這麼多,還沒有說 自定義認證方法的鹽值是如何添加的:
com.nucsoft.shiro.shiro.MyRealm#doGetAuthenticationInfo
來看:
@Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("開始認證!"); UsernamePasswordToken upToken = (UsernamePasswordToken) token; String username = upToken.getUsername(); User user = new User(); user.setId(1000); user.setUserName(username); user.setPassword("42e56621cf3adc9ecc261936188d31d7"); user.getRoleNames().add("admin"); String hashedCredentials = user.getPassword(); ByteSource credentialsSalt = ByteSource.Util.bytes("abcd"); String realmName = getName(); SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user, hashedCredentials, credentialsSalt, realmName); return info; }
標紅的地方就是加鹽值後的處理方式。
在註冊的時候,隨機生成鹽值,同用戶信息存入資料庫中。
7.關於細粒度的基於註解的授權和基於標簽庫的授權,本篇文章不進行說明。
二、總結
介紹了 Spring 環境下 shiro 的使用,包括環境的搭建,以及是如何配置的,自定義 Realm 可以完成自定義認證和自定義授權,也可以完成憑證匹配器的設置。
以及具體是怎麼完成自定義認證和授權的,也將受保護的資源與訪問的許可權從xml文件中轉到了 java 類中,為後續從資料庫中讀取提供了方便。
也介紹了加密的方式:加密類型,加密次數,加密鹽值,以及具體是如何加密的。並沒有講明在真實項目中是如何使用的,以後有機會寫文章來說明。
三、詳細配置文件
1.web.xml
(1)shiro 官方 demo 中的 web.xml
<?xml version="1.0" encoding="UTF-8"?> <!-- ~ Licensed to the Apache Software Foundation (ASF) under one ~ or more contributor license agreements. See the NOTICE file ~ distributed with this work for additional information ~ regarding copyright ownership. The ASF licenses this file ~ to you under the Apache License, Version 2.0 (the ~ "License"); you may not use this file except in compliance ~ with the License. You may obtain a copy of the License at ~ ~ http://www.apache.org/licenses/LICENSE-2.0 ~ ~ Unless required by applicable law or agreed to in writing, ~ software distributed under the License is distributed on an ~ "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY ~ KIND, either express or implied. See the License for the ~ specific language governing permissions and limitations ~ under the License. --> <web-app xmlns="http://java.sun.com/xml/ns/javaee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5"> <!-- ================================================================== Context parameters ================================================================== --> <context-param> <param-name>contextConfigLocation</param-name> <param-value>/WEB-INF/applicationContext.xml</param-value> </context-param> <!-- - Key of the system property that should specify the root directory of this - web app. Applied by WebAppRootListener or Log4jConfigListener. --> <context-param> <param-name>webAppRootKey</param-name> <param-value>spring-sample.webapp.root</param-value> </context-param> <!-- ================================================================== Servlet listeners ================================================================== --> <listener> <listener-class>org.springframework.web.util.Log4jConfigListener</listener-class> </listener> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- ================================================================== Filters ================================================================== --> <!-- Shiro Filter is defined in the spring application context: --> <filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetFilterLifecycle</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!-- ================================================================== Servlets ================================================================== --> <servlet> <servlet-name>sample</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>sample</servlet-name> <url-pattern>/s/*</url-pattern> </servlet-mapping> <servlet> <servlet-name>remoting</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet