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便可