Shiro 的組件都是 JavaBean/POJO 式的組件,因此很是容易使用 Spring 進行組件管理,能夠很是方便的從 ini 配置遷移到 Spring 進行管理,且支持 JavaSE 應用及 Web 應用的集成。html
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.wanho</groupId> <artifactId>shiroTeaching</artifactId> <packaging>war</packaging> <version>1.0-SNAPSHOT</version> <name>shiroTeaching Maven Webapp</name> <url>http://maven.apache.org</url> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <spring.version>4.2.4.RELEASE</spring.version> <mybatis.version>3.0.6</mybatis.version> <jackson.version>1.9.10</jackson.version> <shiro.version>1.2.2</shiro.version> <mybatis.spring.version>1.2.2</mybatis.spring.version> <jstl.version>1.2</jstl.version> <servlet-api.version>2.5</servlet-api.version> <jsp-api.version>2.0</jsp-api.version> </properties> <dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.12</version> <scope>test</scope> </dependency> <!-- log4j --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-log4j12</artifactId> <version>1.7.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-quartz</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>commons-logging</groupId> <artifactId>commons-logging</artifactId> <version>1.1.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>5.1.18</version> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.0.11</version> </dependency> <!-- Mybatis --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis.spring.version}</version> </dependency> <!-- Spring --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-aspects</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jms</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context-support</artifactId> <version>${spring.version}</version> </dependency> <!-- JSP相關 --> <dependency> <groupId>jstl</groupId> <artifactId>jstl</artifactId> <version>${jstl.version}</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>servlet-api</artifactId> <version>${servlet-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jsp-api</artifactId> <version>${jsp-api.version}</version> <scope>provided</scope> </dependency> <dependency> <groupId>org.codehaus.jackson</groupId> <artifactId>jackson-mapper-asl</artifactId> <version>${jackson.version}</version> </dependency> </dependencies> <build> <finalName>shiroTeaching</finalName> <plugins> <!-- 配置Tomcat插件 --> <plugin> <groupId>org.apache.tomcat.maven</groupId> <artifactId>tomcat7-maven-plugin</artifactId> <configuration> <path>/</path> <port>8080</port> </configuration> </plugin> </plugins> </build> </project>
applicationContext-shirojava
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 加載配置文件 <context:property-placeholder location="classpath:resource/*.properties"/>--> <!-- 緩存管理器 <bean id="cacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager"></bean> --> <!-- 緩存管理器 使用Ehcache實現 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:shiro/ehcache.xml"/> </bean> <!-- 憑證匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5" /><!-- 加密算法的名稱 --> <property name="hashIterations" value="2" /><!-- 配置加密的次數 --> </bean> <!--Realm實現--> <bean id="userRealm" class="net.wanho.shiro.UserRealm"> <property name="credentialsMatcher" ref="credentialsMatcher" /> <property name="cachingEnabled" value="false" /> </bean> <!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm" /> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean> <!-- 開啓shiro註解支持(切面支持)--> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!-- 至關於調用SecurityUtils.setSecurityManager(securityManager) --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <!-- rememberMeManager管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cookie" ref="rememberMeCookie" /> </bean> <!-- 記住我cookie --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe" /> <!-- 記住我cookie生效時間,默認單位是秒 7*24*60*60--> <property name="maxAge" value="604800" /> </bean> <!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login/toLogin" /> <property name="unauthorizedUrl" value="/error.html"/> <property name="filterChainDefinitions"> <value> <!-- anon:例子/admins/**=anon 沒有參數,表示能夠匿名使用。 authc:例如/admins/user/**=authc表示須要認證(登陸)才能使用,沒有參數 roles(角色):例子/admins/user/**=roles[admin],參數能夠寫多個,多個時必須加上引號,而且參數之間用逗號分割,當有多個參數時,例如admins/user/**=roles["admin,guest"],每一個參數經過纔算經過,至關於hasAllRoles()方法。 perms(權限):例子/admins/user/**=perms[user:add:*],參數能夠寫多個,多個時必須加上引號,而且參數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個參數時必須每一個參數都經過才經過,想當於isPermitedAll()方法。 rest:例子/admins/user/**=rest[user],根據請求的方法,至關於/admins/user/**=perms[user:method] ,其中method爲post,get,delete等。 port:例子/admins/user/**=port[8081],當請求的url的端口不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裏port的端口,queryString 是你訪問的url裏的?後面的參數。 authcBasic:例如/admins/user/**=authcBasic沒有參數表示httpBasic認證 ssl:例子/admins/user/**=ssl沒有參數,表示安全的url請求,協議爲https user:例如/admins/user/**=user沒有參數表示必須存在用戶,當登入操做時不作檢查 --> /login/toLogin = anon /login/checkLogin = anon /login/logout = authc /** = user </value> </property> </bean> <!--權限不足--> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <prop key="org.apache.shiro.authz.UnauthorizedException">/error</prop> </props> </property> </bean> <!-- Shiro生命週期處理器--> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> </beans>
shiroFilter:此處使用 ShiroFilterFactoryBean 來建立 ShiroFilter 過濾器;filters 屬性用於定義本身的過濾器,即 ini 配置中的 [filters] 部分;filterChainDefinitions 用於聲明 url 和 filter 的關係,即 ini 配置中的 [urls] 部分。mysql
applicationContext-daogit
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:context="http://www.springframework.org/schema/context" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.0.xsd"> <!-- 加載配置文件 --> <context:property-placeholder location="classpath:resource/*.properties"/> <!-- 數據庫鏈接池 maxActive 最大的存活時間 minIdle 最小鏈接數 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" destroy-method="close"> <property name="url" value="${jdbc.url}" /> <property name="username" value="${jdbc.username}" /> <property name="password" value="${jdbc.password}" /> <property name="driverClassName" value="${jdbc.driver}" /> <property name="maxActive" value="10"></property > <property name="minIdle" value="5"></property> </bean> <!-- 配置sqlSessionFactory ,關聯數據源 value只能配置簡單數據類型,ref關聯到複雜類型(對象) --> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="configLocation" value="classpath:mybatis/SqlMapConfig.xml"/> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 配置自動掃描,掃描mapper對象,交給spring代理 --> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="net.wanho.mapper"></property> </bean> </beans>
applicationContext-transgithub
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:aop="http://www.springframework.org/schema/aop" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.0.xsd"> <!-- 配置事務管理器 --> <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <!-- 實際上用的是數據源的事務 --> <property name="dataSource" ref="dataSource"></property> </bean> <!-- 通知 --> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <!-- 傳播行爲 --> <tx:method name="save*" propagation="REQUIRED"/> <tx:method name="insert*" propagation="REQUIRED"/> <tx:method name="add*" propagation="REQUIRED"/> <tx:method name="create*" propagation="REQUIRED"/> <tx:method name="delete*" propagation="REQUIRED"/> <tx:method name="update*" propagation="REQUIRED"/> <tx:method name="find*" propagation="SUPPORTS" read-only="true"/> <tx:method name="select*" propagation="SUPPORTS" read-only="true"/> <tx:method name="get*" propagation="SUPPORTS" read-only="true"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* net.wanho.service.*.*(..))"/> </aop:config> </beans>
springMVC.xmlweb
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:aop="http://www.springframework.org/schema/aop" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd"> <!-- 加載配置文件 --> <context:property-placeholder location="classpath:resource/*.properties"/> <!-- 默認的註解映射的支持 --> <mvc:annotation-driven/> <context:component-scan base-package="net.wanho.controller"/> <!-- 視圖解析器--> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="viewClass" value="org.springframework.web.servlet.view.JstlView"/> <property name="prefix" value="/WEB-INF/jsp/"/> <property name="suffix" value=".jsp"></property> </bean> <!-- 啓用AOP 自定義註解用 --> <aop:config proxy-target-class="true"></aop:config> </beans>
web.xml算法
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" version="2.5" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"> <display-name>smePlantform</display-name> <welcome-file-list> <welcome-file>index.html</welcome-file> <welcome-file>index.htm</welcome-file> <welcome-file>index.jsp</welcome-file> <welcome-file>default.html</welcome-file> <welcome-file>default.htm</welcome-file> <welcome-file>default.jsp</welcome-file> </welcome-file-list> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring/applicationContext-*.xml</param-value> </context-param> <listener> <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class> </listener> <!-- 配置一個解決post中文亂碼的過濾器 --> <filter> <filter-name>characterEncodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>utf-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>characterEncodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <!--配置springmvc--> <servlet> <servlet-name>springServlet</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value> classpath:spring/springmvc.xml </param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>springServlet</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping> <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> </web-app>
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" name="shiroCache"> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> <!-- 受權緩存 --> <cache name="authorizationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="1" overflowToDisk="false" statistics="true"> </cache> <!-- 認證緩存 --> <cache name="authenticationCache" maxEntriesLocalHeap="2000" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="1" overflowToDisk="false" statistics="true"> </cache> <!--session緩存--> <cache name="shiro-activeSessionCache" maxEntriesLocalHeap="10000" overflowToDisk="false" eternal="false" diskPersistent="false" timeToIdleSeconds="0" timeToLiveSeconds="1" statistics="true"> </cache> </ehcache>
散列算法通常用於生成數據的摘要信息,是一種不可逆的算法,通常適合存儲密碼之類的數據,常見的散列算法如 MD五、SHA 等。通常進行散列時最好提供一個 salt(鹽),好比加密密碼 「admin」,產生的散列值是 「21232f297a57a5a743894a0e4a801fc3」,能夠到一些 md5 解密網站很容易的經過散列值獲得密碼 「admin」,即若是直接對密碼進行散列相對來講破解更容易,此時咱們能夠加一些只有系統知道的干擾數據,如用戶名和 ID(即鹽);這樣散列的對象是 「密碼 + 用戶名 +ID」,這樣生成的散列值相對來講更難破解。spring
String str = "hello"; String salt = "123"; String md5 = new Md5Hash(str, salt).toString();//還能夠轉換爲 toBase64()/toHex()
Shiro 還提供了通用的散列支持:sql
String str = "hello"; String salt = "123"; //內部使用MessageDigest String simpleHash = new SimpleHash("MD5", str, salt).toString();
經過調用 SimpleHash 時指定散列算法,其內部使用了 Java 的 MessageDigest 實現。數據庫
Shiro 提供了 CredentialsMatcher 的散列實現 HashedCredentialsMatcher,和以前的 PasswordMatcher 不一樣的是,它只用於密碼驗證,且能夠提供本身的鹽,而不是隨機生成鹽,且生成密碼散列值的算法須要本身寫,由於能提供本身的鹽。
一、生成密碼散列值
此處咱們使用 MD5 算法,「密碼 + 鹽(用戶名 + 隨機數)」 的方式生成散列值:
String algorithmName = "md5"; String username = "liu"; String password = "123"; String salt1 = username; String salt2 = new SecureRandomNumberGenerator().nextBytes().toHex(); int hashIterations = 2; SimpleHash hash = new SimpleHash(algorithmName, password, salt1 + salt2, hashIterations); String encodedPassword = hash.toHex()
若是要寫用戶模塊,須要在新增用戶 / 重置密碼時使用如上算法保存密碼,將生成的密碼及 salt2 存入數據庫(由於咱們的散列算法是:md5(md5(密碼 +username+salt2)))。
二、功能實現
User
public class TUser { private Integer tUserId; private String tUserName; private String tUserAccount; private String tUserPassword; private String tSort; private String tUserType; private Integer tParentUserId; private Integer departmentId; private String tStatus; }
public class UserService implements IUserService{ /** * 獲取全部的角色 * @param username * @return */ @Override public Set<String> findRoles(String username) { Set<String> set = new HashSet<>(); set.add("admin"); return set; } /** * 獲取全部的權限 * @param username * @return */ @Override public Set<String> findPermissions(String username) { Set<String> set = new HashSet<>(); set.add("dustin:test"); return set; } /** * 根據用戶名獲取用戶信息 * @param username * @return */ @Override public TUser findByUsername(String username) { TUser user = new TUser(); user.settUserName(username); user.settUserPassword(shiroMD5("123456")); user.settSort("abcd"); return user; } //shiro的密碼加密,參數hashIterations表示加密次數,應該跟配置文件中的相同 private String shiroMD5(String credentials){ //加密方式 String hashAlgorithmName = "MD5"; //String credentials = "123456"; //鹽 ByteSource credentialsSalt = ByteSource.Util.bytes("dustin"+"abcd"); //加密次數 int hashIterations = 2; Object obj = new SimpleHash(hashAlgorithmName, credentials, credentialsSalt, hashIterations); return obj.toString(); } }
public class UserRealm extends AuthorizingRealm { @Resource private IUserService userService; /** * 獲取角色與權限 *doGetAuthorizationInfo執行時機有三個,以下: * 一、subject.hasRole(「admin」) 或 subject.isPermitted(「admin」):本身去調用這個是否有什麼角色或者是否有什麼權限的時候; * 二、@RequiresRoles("admin") :在方法上加註解的時候; * 三、@shiro.hasPermission name = "admin"/@shiro.hasPermission:"dustin:test"在頁面上加shiro標籤的時候,即進這個頁面的時候掃描到有這個標籤的時候。 * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String)principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); //獲取用戶全部角色 authorizationInfo.setRoles(userService.findRoles(username)); //獲取用戶全部權限 authorizationInfo.setStringPermissions(userService.findPermissions(username)); return authorizationInfo; } /** * 登陸信息驗證 * * 1.doGetAuthenticationInfo執行時機以下 * 當調用Subject currentUser = SecurityUtils.getSubject(); * currentUser.login(token); * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String)token.getPrincipal(); TUser user = userService.findByUsername(username); if(user == null) { throw new UnknownAccountException();//沒找到賬號 } if(Boolean.TRUE.equals(user.gettStatus())) { throw new LockedAccountException(); //賬號鎖定 } //交給AuthenticatingRealm使用CredentialsMatcher進行密碼匹配,若是以爲人家的很差能夠自定義實現 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user.gettUserName(), //用戶名 user.gettUserPassword(), //密碼 ByteSource.Util.bytes(user.gettUserName()+user.gettSort()),//salt=username+salt getName() //realm name ); return authenticationInfo; } @Override public void clearCachedAuthorizationInfo(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); } @Override public void clearCachedAuthenticationInfo(PrincipalCollection principals) { super.clearCachedAuthenticationInfo(principals); } @Override public void clearCache(PrincipalCollection principals) { super.clearCache(principals); } public void clearAllCachedAuthorizationInfo() { getAuthorizationCache().clear(); } public void clearAllCachedAuthenticationInfo() { getAuthenticationCache().clear(); } public void clearAllCache() { clearAllCachedAuthenticationInfo(); clearAllCachedAuthorizationInfo(); } }
public class loginController { @RequestMapping("/toLogin") public String tologin(HttpServletRequest request, HttpServletResponse response, Model model){ System.out.println("來自IP[" + request.getRemoteHost() + "]的訪問"); return "login"; } /** * 驗證用戶名和密碼 * @return */ @RequestMapping("/checkLogin") public String login(String username,String password,ModelMap map) { UsernamePasswordToken token = new UsernamePasswordToken(username, password); Subject currentUser = SecurityUtils.getSubject(); try { //使用shiro來驗證 if (!currentUser.isAuthenticated()){ //這裏是記住我,記cookie token.setRememberMe(false); currentUser.login(token);//驗證角色和權限 } } catch (IncorrectCredentialsException e) { e.printStackTrace(); map.put("code","登陸失敗:"+"帳號密碼不正確"); } return JSONUtils.toJSONString(map); } @RequestMapping("/isLogin") @ResponseBody public String isLogin() { Subject currentUser = SecurityUtils.getSubject(); currentUser.isAuthenticated(); return String.valueOf(currentUser.isAuthenticated()); } @RequestMapping("/logout") @ResponseBody public String logout() { Subject currentUser = SecurityUtils.getSubject(); currentUser.logout(); return "logout success"; } }
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> <title>登陸頁面</title> <%@include file="/taglib.jsp" %> </head> <body> <form action="${ctx}/login/checkLogin" method="post"> username: <input type="text" name="username"><br> password: <input type="password" name="password"><br> <input type="submit" value="登陸"> </form> </body> </html>
此處就是把步驟 1 中生成的相應數據組裝爲 SimpleAuthenticationInfo,經過 SimpleAuthenticationInfo 的 credentialsSalt 設置鹽,HashedCredentialsMatcher 會自動識別這個鹽。
此處還要注意 Shiro 默認使用了 apache commons BeanUtils,默認是不進行 Enum 類型轉型的,此時須要本身註冊一個 Enum 轉換器 「BeanUtilsBean.getInstance().getConvertUtils().register(new EnumConverter(), JdbcRealm.SaltStyle.class);」 具體請參考示例 「com.github.zhangkaitao.shiro.chapter5.hash.PasswordTest」 中的代碼。
三、spring配置
<!-- 憑證匹配器 --> <bean id="credentialsMatcher" class="org.apache.shiro.authc.credential.HashedCredentialsMatcher"> <property name="hashAlgorithmName" value="md5" /><!-- 加密算法的名稱 --> <property name="hashIterations" value="2" /><!-- 配置加密的次數 --> </bean>
此處最須要注意的就是 HashedCredentialsMatcher 的算法須要和生成密碼時的算法同樣。另外 HashedCredentialsMatcher 會自動根據 AuthenticationInfo 的類型是不是 SaltedAuthenticationInfo 來獲取 credentialsSalt 鹽。
如在 1 個小時內密碼最多重試 5 次,若是嘗試次數超過 5 次就鎖定 1 小時,1 小時後可再次重試,若是仍是重試失敗,能夠鎖定如 1 天,以此類推,防止密碼被暴力破解。咱們經過繼承 HashedCredentialsMatcher,且使用 Ehcache 記錄重試次數和超時時間。
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String)token.getPrincipal(); //retry count + 1 Element element = passwordRetryCache.get(username); if(element == null) { element = new Element(username , new AtomicInteger(0)); passwordRetryCache.put(element); } AtomicInteger retryCount = (AtomicInteger)element.getObjectValue(); if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry count passwordRetryCache.remove(username); } return matches; }
如上代碼邏輯比較簡單,即若是密碼輸入正確清除 cache 中的記錄;不然 cache 中的重試次數 +1,若是超出 5 次那麼拋出異常表示超出重試次數了。
Shiro 提供了 JSTL 標籤用於在 JSP/GSP 頁面進行權限控制,如根據登陸用戶顯示相應的頁面按鈕
導入標籤庫
<%@taglib prefix="shiro" uri="http://shiro.apache.org/tags" %>
標籤庫定義在 shiro-web.jar 包下的 META-INF/shiro.tld 中定義。
<shiro:guest> 歡迎遊客訪問,<a href="${pageContext.request.contextPath}/login.jsp">登陸</a> </shiro:guest>
用戶沒有身份驗證時顯示相應信息,即遊客訪問信息。
<shiro:user> 已認證經過的用戶,setRememberMe = true </shiro:user>
用戶已經身份驗證 / 記住我登陸後顯示相應的信息。
<shiro:authenticated> 用戶[<shiro:principal/>]已身份驗證經過 </shiro:authenticated
用戶已經身份驗證經過,即 Subject.login 登陸成功,不是記住我登陸的。
<shiro:notAuthenticated> 未身份驗證(包括記住我) </shiro:notAuthenticated>
用戶已經身份驗證經過,即沒有調用 Subject.login 進行登陸,包括記住我自動登陸的也屬於未進行身份驗證。
<shiro: principal/>
顯示用戶身份信息,默認調用 Subject.getPrincipal() 獲取,即 Primary Principal。
<shiro:principal type="java.lang.String"/>
至關於 Subject.getPrincipals().oneByType(String.class)。
<shiro:principal property="username"/>
至關於 ((User)Subject.getPrincipals()).getUsername()。
<shiro:hasRole name="admin"> 用戶[<shiro:principal/>]擁有角色admin<br/> </shiro:hasRole>
若是當前 Subject 有角色將顯示 body 體內容。
<shiro:hasAnyRoles name="admin,user"> 用戶[<shiro:principal/>]擁有角色admin或user<br/> </shiro:hasAnyRoles>;
若是當前 Subject 有任意一個角色(或的關係)將顯示 body 體內容。
<shiro:hasPermission name="user:create"> 用戶[<shiro:principal/>]擁有權限user:create<br/> </shiro:hasPermission>
若是當前 Subject 有權限將顯示 body 體內容。
<shiro:lacksPermission name="org:create"> 用戶[<shiro:principal/>]沒有權限org:create<br/> </shiro:lacksPermission>
若是當前 Subject 沒有權限將顯示 body 體內容。
Shiro 提供了相應的註解用於權限控制,若是使用這些註解就須要使用 AOP 的功能來進行判斷,如 Spring AOP;Shiro 提供了 Spring AOP 集成用於權限註解的解析和驗證。
@RequiresAuthentication
表示當前 Subject 已經經過 login 進行了身份驗證;即 Subject.isAuthenticated() 返回 true。
@RequiresUser
表示當前 Subject 已經身份驗證或者經過記住我登陸的。
@RequiresGuest
表示當前 Subject 沒有身份驗證或經過記住我登陸過,便是遊客身份。
@RequiresRoles(value={「admin」, 「user」}, logical= Logical.AND)
表示當前 Subject 須要角色 admin 和 user。
@RequiresPermissions (value={「user:a」, 「user:b」}, logical= Logical.OR)
表示當前 Subject 須要權限 user:a 或 user:b。
此處使用了 Spring MVC 來測試 Shiro 註解,固然 Shiro 註解不只僅能夠在 web 環境使用,在獨立的 JavaSE 中也是能夠用的,此處只是以 web 爲例了。
在 spring-mvc.xml 配置文件添加 Shiro Spring AOP 權限註解的支持:
<aop:config proxy-target-class="true"></aop:config> <bean class=" org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean>
如上配置用於開啓 Shiro Spring AOP 權限註解的支持;<aop:config proxy-target-class="true">
表示代理類。
接着就能夠在相應的控制器(Controller)中使用以下方式進行註解:
@RequiresRoles("admin") @RequestMapping("/hello2") public String hello2() { return "success"; }
訪問 hello2 方法的前提是當前用戶有 admin 角色。
Shiro 提供了完整的企業級會話管理功能,不依賴於底層容器(如 web 容器 tomcat),無論 JavaSE 仍是 JavaEE 環境均可以使用,提供了會話管理、會話事件監聽、會話存儲 / 持久化、容器無關的集羣、失效 / 過時支持、對 Web 的透明支持、SSO 單點登陸的支持等特性。即直接使用 Shiro 的會話管理能夠直接替換如 Web 容器的會話管理。
所謂會話,即用戶訪問應用時保持的鏈接關係,在屢次交互中應用可以識別出當前訪問的用戶是誰,且能夠在屢次交互中保存一些數據。如訪問一些網站時登陸成功後,網站能夠記住用戶,且在退出以前均可以識別當前用戶是誰。
Shiro 的會話支持不只能夠在普通的 JavaSE 應用中使用,也能夠在 JavaEE 應用中使用,如 web 應用。且使用方式是一致的。
login("classpath:shiro.ini", "zhang", "123"); Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession()
登陸成功後使用 Subject.getSession() 便可獲取會話;其等價於 Subject.getSession(true),即若是當前沒有建立 Session 對象會建立一個;另外 Subject.getSession(false),若是當前沒有建立 Session 則返回 null(不過默認狀況下若是啓用會話存儲功能的話在建立 Subject 時會主動建立一個 Session)。
session.getId();
獲取當前會話的惟一標識。
session.getHost();
獲取當前 Subject 的主機地址,該地址是經過 HostAuthenticationToken.getHost() 提供的。
session.getTimeout(); session.setTimeout(毫秒);
獲取 / 設置當前 Session 的過時時間;若是不設置默認是會話管理器的全局過時時間。
session.getStartTimestamp(); session.getLastAccessTime();
獲取會話的啓動時間及最後訪問時間;若是是 JavaSE 應用須要本身按期調用 session.touch() 去更新最後訪問時間;若是是 Web 應用,每次進入 ShiroFilter 都會自動調用 session.touch() 來更新最後訪問時間。
session.touch(); session.stop();
更新會話最後訪問時間及銷燬會話;當 Subject.logout() 時會自動調用 stop 方法來銷燬會話。若是在 web 中,調用 javax.servlet.http.HttpSession. invalidate() 也會自動調用 Shiro Session.stop 方法進行銷燬 Shiro 的會話。
session.setAttribute("key", "123"); session.removeAttribute("key");
設置 / 獲取 / 刪除會話屬性;在整個會話範圍內均可以對這些屬性進行操做。
Shiro 提供的會話能夠用於 JavaSE/JavaEE 環境,不依賴於任何底層容器,能夠獨立使用,是完整的會話模塊。
測試
/** * 測試會話session */ @Test public void testSession() { Subject subject = login("classpath:shiro-realm.ini", "zhang", "123"); Session session = subject.getSession(); //獲取當前會話的惟一標識 System.out.println(session.getId()); //獲取當前 Subject 的主機地址 System.out.println(session.getHost()); //獲取 / 設置當前 Session 的過時時間 System.out.println(session.getTimeout()); // 獲取會話的啓動時間及最後訪問時間 System.out.println(session.getStartTimestamp()); System.out.println(session.getLastAccessTime()); // 更新會話最後訪問時間及銷燬會話 //session.touch(); //session.stop(); //設置屬性值 session.setAttribute("name", "zhangsan"); System.out.println(session.getAttribute("name")); }
會話管理器管理着應用中全部 Subject 的會話的建立、維護、刪除、失效、驗證等工做。是 Shiro 的核心組件,頂層組件 SecurityManager 直接繼承了 SessionManager,且提供了SessionsSecurityManager 實現直接把會話管理委託給相應的 SessionManager,DefaultSecurityManager 及 DefaultWebSecurityManager 默認 SecurityManager 都繼承了 SessionsSecurityManager。
SecurityManager 提供了以下接口:
Session start(SessionContext context); //啓動會話 Session getSession(SessionKey key) throws SessionException; //根據會話Key獲取會話
另外用於 Web 環境的 WebSessionManager 又提供了以下接口:
boolean isServletContainerSessions();// 是否使用 Servlet 容器的會話
Shiro 還提供了 ValidatingSessionManager 用於驗資並過時會話:
void validateSessions();// 驗證全部會話是否過時
DefaultSessionManager:DefaultSecurityManager 使用的默認實現,用於 JavaSE 環境;
ServletContainerSessionManager:DefaultWebSecurityManager 使用的默認實現,用於 Web 環境,其直接使用 Servlet 容器的會話;
DefaultWebSessionManager:用於 Web 環境的實現,能夠替代 ServletContainerSessionManager,本身維護着會話,直接廢棄了 Servlet 容器的會話管理。
spring中配置
<!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionDAO" ref="sessionDAO"/> </bean>
會話監聽器用於監聽會話建立、過時及中止事件:
public class MySessionListener1 implements SessionListener { @Override public void onStart(Session session) {//會話建立時觸發 System.out.println("會話建立:" + session.getId()); } @Override public void onExpiration(Session session) {//會話過時時觸發 System.out.println("會話過時:" + session.getId()); } @Override public void onStop(Session session) {//退出/會話過時時觸發 System.out.println("會話中止:" + session.getId()); } }
若是隻想監聽某一個事件,能夠繼承 SessionListenerAdapter 實現:
public class MySessionListener2 extends SessionListenerAdapter { @Override public void onStart(Session session) { System.out.println("會話建立:" + session.getId()); } }
Shiro 提供 SessionDAO 用於會話的 CRUD,即 DAO(Data Access Object)模式實現:
//如DefaultSessionManager在建立完session後會調用該方法;如保存到關係數據庫/文件系統/NoSQL數據庫;便可以實現會話的持久化;返回會話ID;主要此處返回的ID.equals(session.getId()); Serializable create(Session session); //根據會話ID獲取會話 Session readSession(Serializable sessionId) throws UnknownSessionException; //更新會話;如更新會話最後訪問時間/中止會話/設置超時時間/設置移除屬性等會調用 void update(Session session) throws UnknownSessionException; //刪除會話;當會話過時/會話中止(如用戶退出時)會調用 void delete(Session session); //獲取當前全部活躍用戶,若是用戶量多此方法影響性能 Collection<Session> getActiveSessions();
Shiro 內嵌了以下 SessionDAO 實現:
AbstractSessionDAO 提供了 SessionDAO 的基礎實現,如生成會話 ID 等;CachingSessionDAO 提供了對開發者透明的會話緩存的功能,只須要設置相應的 CacheManager 便可;MemorySessionDAO 直接在內存中進行會話維護;而 EnterpriseCacheSessionDAO 提供了緩存功能的會話維護,默認狀況下使用 MapCache 實現,內部使用 ConcurrentHashMap 保存緩存的會話。
Shiro 提供了使用 Ehcache 進行會話存儲,Ehcache 能夠配合 TerraCotta 實現容器無關的分佈式集羣。
首先在 pom.xml 裏添加以下依賴:
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.2</version> </dependency>
配置 ehcache.xml
<cache name="shiro-activeSessionCache" maxEntriesLocalHeap="10000" overflowToDisk="false" eternal="false" diskPersistent="false" timeToLiveSeconds="0" timeToIdleSeconds="0" statistics="true"/>
Cache 的名字默認爲 shiro-activeSessionCache,即設置的 sessionDAO 的 activeSessionsCacheName 屬性值。
spring中配置
<!-- 會話ID生成器 --> <bean id="sessionIdGenerator" class="org.apache.shiro.session.mgt.eis.JavaUuidSessionIdGenerator"/> <!-- 會話DAO --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="shiro-activeSessionCache"/> <property name="sessionIdGenerator" ref="sessionIdGenerator"/> </bean> <!-- 會話驗證調度器 --> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.quartz.QuartzSessionValidationScheduler"> <property name="sessionValidationInterval" value="1800000"/> <property name="sessionManager" ref="sessionManager"/> </bean> <!-- 會話管理器 --> <bean id="sessionManager" class="org.apache.shiro.session.mgt.DefaultSessionManager"> <property name="globalSessionTimeout" value="1800000"/> <property name="deleteInvalidSessions" value="true"/> <property name="sessionValidationSchedulerEnabled" value="true"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler"/> <property name="sessionDAO" ref="sessionDAO"/> </bean>
若是自定義實現 SessionDAO,繼承 CachingSessionDAO 便可:
public class MySessionDAO extends CachingSessionDAO { private JdbcTemplate jdbcTemplate = JdbcTemplateUtils.jdbcTemplate(); protected Serializable doCreate(Session session) { Serializable sessionId = generateSessionId(session); assignSessionId(session, sessionId); String sql = "insert into sessions(id, session) values(?,?)"; jdbcTemplate.update(sql, sessionId, SerializableUtils.serialize(session)); return session.getId(); } protected void doUpdate(Session session) { if(session instanceof ValidatingSession && !((ValidatingSession)session).isValid()) { return; //若是會話過時/中止 不必再更新了 } String sql = "update sessions set session=? where id=?"; jdbcTemplate.update(sql, SerializableUtils.serialize(session), session.getId()); } protected void doDelete(Session session) { String sql = "delete from sessions where id=?"; jdbcTemplate.update(sql, session.getId()); } protected Session doReadSession(Serializable sessionId) { String sql = "select session from sessions where id=?"; List<String> sessionStrList = jdbcTemplate.queryForList(sql, String.class, sessionId); if(sessionStrList.size() == 0) return null; return SerializableUtils.deserialize(sessionStrList.get(0)); } }
doCreate/doUpdate/doDelete/doReadSession 分別表明建立 / 修改 / 刪除 / 讀取會話;此處經過把會話序列化後存儲到數據庫實現。
Shiro 提供了記住我(RememberMe)的功能,好比訪問如淘寶等一些網站時,關閉了瀏覽器下次再打開時仍是能記住你是誰,下次訪問時無需再登陸便可訪問,基本流程以下:
spring中配置
<!-- 記住我cookie --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe" /> <!-- 記住我cookie生效時間,默認單位是秒 7*24*60*60--> <property name="maxAge" value="604800" /> </bean>
<!-- rememberMeManager管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cookie" ref="rememberMeCookie" /> </bean>
<!-- 安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <property name="realm" ref="userRealm" /> <property name="cacheManager" ref="cacheManager"/> <property name="rememberMeManager" ref="rememberMeManager"/> </bean>
web過濾器
<!-- Shiro的Web過濾器 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <property name="loginUrl" value="/login/toLogin" /> <property name="unauthorizedUrl" value="/error.html"/> <property name="filterChainDefinitions"> <value> /login/toLogin = anon /login/checkLogin = anon /login/logout = authc /** = user </value> </property> </bean>
anon:例子/admins/**=anon 沒有參數,表示能夠匿名使用。 authc:例如/admins/user/**=authc表示須要認證(登陸)才能使用,沒有參數 roles(角色):例子/admins/user/**=roles[admin],參數能夠寫多個,多個時必須加上引號,而且參數之間用逗號分割,當有多個參數時,例如admins/user/**=roles["admin,guest"],每一個參數經過纔算經過,至關於hasAllRoles()方法。 perms(權限):例子/admins/user/**=perms[user:add:*],參數能夠寫多個,多個時必須加上引號,而且參數之間用逗號分割,例如/admins/user/**=perms["user:add:*,user:modify:*"],當有多個參數時必須每一個參數都經過才經過,想當於isPermitedAll()方法。 rest:例子/admins/user/**=rest[user],根據請求的方法,至關於/admins/user/**=perms[user:method] ,其中method爲post,get,delete等。 port:例子/admins/user/**=port[8081],當請求的url的端口不是8081是跳轉到schemal://serverName:8081?queryString,其中schmal是協議http或https等,serverName是你訪問的host,8081是url配置裏port的端口,queryString 是你訪問的url裏的?後面的參數。 authcBasic:例如/admins/user/**=authcBasic沒有參數表示httpBasic認證 ssl:例子/admins/user/**=ssl沒有參數,表示安全的url請求,協議爲https user:例如/admins/user/**=user沒有參數表示必須存在用戶,當登入操做時不作檢查
處理shiro異常有3種方式:
(1)使用Spring-MVC提供的SimpleMappingExceptionResolver;
(2)實現Spring的異常處理接口HandlerExceptionResolver 自定義本身的異常處理器;
(3)使用@ExceptionHandler註解實現異常處理;
代碼若拋出受權未經過異常即UnauthorizedException,跳轉noPermission.jsp頁面(會通過視圖解析器後跳轉)
<!--權限不足--> <bean class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"> <property name="exceptionMappings"> <props> <prop key="org.apache.shiro.authz.UnauthorizedException">/noPermission</prop> </props> </property> </bean>
注:/noPermission 是指通過視圖解析器的頁面,這裏直接配須要跳轉到的頁面的名字便可
實現HandlerExceptionResolver 接口自定義異常處理器,HandlerExceptionResolver是一個接口,只有一個方法,咱們只須要實現這個接口;
咱們在springMVC的配置文件中進行以下配置
<!--而後經過 Spring的HandlerExceptionResolver去進行全局捕獲,不論你在系統哪裏去throw,只要實現了 HandlerExceptionResovler這個接口,Spring都會攔截下異常進行處理 --> <bean id="exceptionResolver" class="net.wanho.exception.MyExceptionResolver"></bean>
MyExceptionResolver
public class MyExceptionResolver implements HandlerExceptionResolver { public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { System.out.println("==============異常開始============="); ex.printStackTrace(); System.out.println("==============異常結束============="); ModelAndView mv = new ModelAndView("noPermission"); return mv; } }
將@ExceptionHandler標註在Controller的方法上,該方法將處理由@RequestMapping方法拋出的異常
首先要增長BaseController類,並在類中使用@ExceptionHandler註解聲明異常處理,代碼以下:
public class BaseController { /** 基於@ExceptionHandler異常處理 */ @ExceptionHandler public String exp(HttpServletRequest request, Exception ex) { // 根據不一樣錯誤轉向不一樣頁面 if(ex instanceof org.apache.shiro.authz.UnauthorizedException) { return "noPermission"; }else { return "error"; } }
將須要異常處理的Controller繼承BaseController便可