项目结构:
1、maven项目的pom中引入shiro所需的jar包依赖关系
<dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>3.0.1</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>1.2.4<version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-web</artifactId> <version>1.2.4<version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.2.4<version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.2.4<version> </dependency>
2、web项目的web.xml中引入shiro的配置信息以及配置文件
<context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml,classpath:spring-shiro.xml</param-value> </context-param>
增加shiro的filter
<filter> <filter-name>shiroFilter</filter-name> <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class> <init-param> <param-name>targetBeanName</param-name> <param-value>shiroFilterFactoryBean</param-value> </init-param> </filter> <filter-mapping> <filter-name>shiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>
配置spring-shiro.xml文件
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:context="http://www.springframework.org/schema/context" 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"> <bean id="shiroFilterFactoryBean" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager" /> <property name="loginUrl" value="/login/toLoginPage"/> <property name="successUrl" value="/login/index"/> <property name="unauthorizedUrl" value="/modules/unan.html"/> <property name="filterChainDefinitions"> <value> <!-- 后期实际应用可采取正则模糊匹配,但要避免包含了静态资源的路径导致加载静态资源时也执行这个权限验证 --> /login/adminView=authc,resourceCheckFilter /login/testView=authc,resourceCheckFilter /login/guestView=authc,resourceCheckFilter <!-- user 表示该方法支持remember的用户访问 --> /login/rememberMethod=user /permission/**=authc,resourceCheckFilter /admin/**.html=authc,resourceCheckFilter /admin/**.jsp=authc,resourceCheckFilter /admin/user/**.html=authc,resourceCheckFilter /admin/user/**.jsp=authc,resourceCheckFilter /admin/menu/**.html=authc,resourceCheckFilter /admin/menu/**.jsp=authc,resourceCheckFilter <!--shiro不拦截首页 --> /=anon <!-- shiro不拦截resources下的资源 --> /resources/**=anon <!-- shiro不拦截modules下的资源 --> /modules/**=anon <!-- shiro不拦截public下的资源 --> /public/**=anon /admin/**=anon /user/**=anon <!-- 此处login被上面的覆盖了,可能会执行shiro权限校验 --> /login/**=anon <!-- /login/login=anon --> <!-- /error=anon --> /admin=authc,roles[admin] /noAuth=roles[noAuth] <!-- /**=authc --> </value> </property> </bean> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"> <!--设置自定义realm,实现用户认证以及授权的操作--> <property name="realm" ref="monitorRealm" /> <!-- 设置自定义的权限验证方法,用于判定用户是否能对特定的菜单进行操作 --> <property name="authorizer.permissionResolver" ref="permissionResolver"/> <!-- 设置全局session超时时间 --> <property name="sessionManager" ref="sessionManager"></property> <!-- 记住我配置 --> <property name="rememberMeManager" ref="rememberMeManager" /> </bean> <!-- 全局超时时间 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <property name="sessionDAO" ref="sessionDAO" /> <!-- 1800000 半小时 10000 十秒 --> <property name="globalSessionTimeout" value="1800000"/> <property name="sessionValidationScheduler" ref="sessionValidationScheduler" /> <property name="sessionValidationSchedulerEnabled" value="true" /> </bean> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="shiro-activeSessionCache" /> </bean> <bean id="sessionValidationScheduler" class="org.apache.shiro.session.mgt.ExecutorServiceSessionValidationScheduler"> <!-- 每半小时检查一次是否过期 --> <property name="interval" value="1800000" /> </bean> <!-- shiro生命周期 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor" /> <!--自定义Realm 继承自AuthorizingRealm--> <bean id="monitorRealm" class="org.youme.realm.UserRealm"> <!-- 注入自定义密码校验方法,可在该bean中设置短时间内最大尝试次数 --> <property name="credentialsMatcher" ref="credentialsMatcher" /> <property name="cachingEnabled" value="true" /> <property name="authenticationCachingEnabled" value="true" /> <property name="authenticationCacheName" value="authenticationCache" /> <property name="authorizationCachingEnabled" value="true" /> <property name="authorizationCacheName" value="authorizationCache" /> </bean> <!-- 凭证匹配器 --> <bean id="credentialsMatcher" class="org.youme.util.RetryLimitHashedCredentialsMatcher"> <constructor-arg ref="cacheManager" /> <!-- 此处设置短时间内出错次数 --> <property name="errorTimes" value="6"></property> <!-- 设置SHIRO加密方式,需要和util里的加密方法保持一致 --> <property name="hashAlgorithmName" value="MD5" /> <!-- 设置SHIRO加密次数 --> <property name="hashIterations" value="3" /> <property name="storedCredentialsHexEncoded" value="true" /> </bean> <!-- securityManager --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager" /> <property name="arguments" ref="securityManager" /> </bean> <!-- Enable Shiro Annotations for Spring-configured beans. Only run after --> <!-- the lifecycleBeanProcessor has run: --> <bean class="org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator" depends-on="lifecycleBeanPostProcessor"/> <bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </bean> <!-- 缓存管理器 使用Ehcache实现 --> <bean id="cacheManager" class="org.apache.shiro.cache.ehcache.EhCacheManager"> <property name="cacheManagerConfigFile" value="classpath:spring-shiro-ehcache.xml" /> </bean> <!-- URL权限验证方法 --> <bean id="resourceCheckFilter" class="org.youme.filter.ResourceCheckFilter"> <!-- 设置当前用户无权限访问当前链接时跳转的页面 --> <property name="unanUrl" value="modules/unan.html"></property> </bean> <bean id="permissionResolver" class="org.youme.permission.UrlPermissionResolver"/> <!-- remenberMe配置 --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe" /> <property name="httpOnly" value="true" /> <!-- 默认记住7天(单位:秒) --> <property name="maxAge" value="604800" /> </bean> <!-- rememberMe管理器 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('4AvVhmFLUs0KTA3Kprsdag==')}" /> <property name="cookie" ref="rememberMeCookie" /> </bean> </beans>
此处引入了spring-shiro-ehcache.xml文件
<?xml version="1.0" encoding="UTF-8"?> <ehcache name="shirocache"> <diskStore path="java.io.tmpdir" /> <!-- 登录记录缓存 锁定10分钟 --> <cache name="passwordRetryCache" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true" maxBytesLocalHeap="10M"> </cache> <cache name="authorizationCache" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true" maxBytesLocalHeap="10M"> </cache> <cache name="authenticationCache" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true" maxBytesLocalHeap="10M"> </cache> <cache name="shiro-activeSessionCache" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="0" overflowToDisk="false" statistics="true" maxBytesLocalHeap="10M"> </cache> </ehcache>
实现spring-shiro中配置的自定义拦截器实现用户信息校验以及登录成功后授予相应的角色以及权限
package org.youme.realm; import java.util.HashMap; import java.util.List; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.apache.shiro.authz.AuthorizationInfo; import org.apache.shiro.authz.SimpleAuthorizationInfo; import org.apache.shiro.realm.AuthorizingRealm; import org.apache.shiro.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.subject.Subject; import org.apache.shiro.util.ByteSource; import org.springframework.beans.factory.annotation.Autowired; import org.youme.service.PermissionService; import org.youme.service.ResourceService; import org.youme.service.RoleService; import org.youme.service.UserService; public class UserRealm extends AuthorizingRealm{ @Autowired //项目中自己实现查询用户信息service UserService userService; @Autowired //项目中自己实现查询角色信息service RoleService roleService; @Autowired //暂时没用这个service ResourceService resourceService; @Autowired //项目中自己实现查询权限信息service PermissionService permissionService; @Override /** * 登录成功后给用户授予系统中特定权限
* */ protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String key = "AUTHORIZATION_INFO"; Subject subject = SecurityUtils.getSubject(); Session session = subject.getSession(false); SimpleAuthorizationInfo authorizationInfo = (SimpleAuthorizationInfo) session.getAttribute(key); if (!subject.isAuthenticated() || authorizationInfo == null) { String username = (String) principals.getPrimaryPrincipal(); HashMap map = new HashMap(); map.put("account", username); List users = userService.getUserForLogin(map); map = (HashMap)users.get(0); Object userId = map.get("id"); map.put("userId", userId); map.put("isWork", "Y"); List roles = roleService.getRolesForLogin(map); authorizationInfo = new SimpleAuthorizationInfo(); authorizationInfo.addRoles(roles); if(userId!=null){ //根据当前用户ID获取其生效的菜单权限并给该用户授权 map.clear(); map.put("userId", userId); map.put("isWork", "Y"); //此处返回list<String>类型的权限 List permissions = permissionService.findPermissionByMap(map); authorizationInfo.addStringPermissions(permissions); } session.setAttribute(key, authorizationInfo); } return authorizationInfo; } @Override /** * 用户登录密码校验方法 * */ protected AuthenticationInfo doGetAuthenticationInfo( AuthenticationToken token) throws AuthenticationException { UsernamePasswordToken upToken = (UsernamePasswordToken) token; String account = upToken.getUsername(); HashMap map = new HashMap(); map.put("account", account); List users = userService.getUserForLogin(map); if(users==null||users.size()==0){ throw new UnknownAccountException(); }else{ map = (HashMap)users.get(0); Object IsLocked = map.get("isLocked"); if(null!=IsLocked&&"Y".equals(IsLocked.toString())){ //账户被锁定 throw new LockedAccountException(); } SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(map.get("account"), map.get("passwd"), getClass().getName()); //第一种加密方式 //系统设置固定的加密盐方式 // info.setCredentialsSalt(ByteSource.Util.bytes(map.get("name"))); //第二种方式:加密盐由系统随机生成,保存在数据库中,查询用户信息时需要将该随机盐一同查询出来 Object randomSalt = map.get("salt") ;//此处值由用户表获取 info.setCredentialsSalt(ByteSource.Util.bytes(randomSalt)); return info; } } }
登录方法中将用户名和密码传递给UsernamePasswordToken,通过shiro提供的login方法即可实现登录逻辑
Subject subject = SecurityUtils.getSubject(); //前台页面中传递过来的用户账户 Object o1 = map.get("account"); //前台页面传递过来的用户密码 Object o2 = map.get("password"); //前台是否选择记住我按钮 Object rememberMe = map.get("rememberMe"); //登录反馈给前台的结果 Map<String, Object> result = new HashMap<String,Object>(); UsernamePasswordToken token = new UsernamePasswordToken(o1.toString(),o2.toString()); try { if("Y".equals(rememberMe+"")){ //记住我 token.setRememberMe(true); }
//登录方法
subject.login(token); // 在web应用中,subject.getSession()和request.getSession()作用一样 // session也一样,放入subject.getSession()和request.getSession()中的对象是共享的 // 但是为了兼容性,推荐使用subject.getSession() org.apache.shiro.session.Session session = subject.getSession(false); List users = userService.getUserForLogin(map); map = (HashMap)users.get(0); session.setAttribute("user", map); } catch (UnknownAccountException uae) { result.put("status", false); result.put("errorMsg", "用户名或密码错误"); log_.info("There is no user with username of :"+token.getPrincipal()); return result; }catch (IncorrectCredentialsException ice) { result.put("status", false); result.put("errorMsg", "用户名或密码错误"); log_.info("Password for account " + token.getPrincipal() + " was incorrect!"); return result; }catch (LockedAccountException lae) { result.put("status", false); result.put("errorMsg", "当前登录用户 :"+token.getPrincipal()+"被锁定,请联系系统管理员!"); log_.info("The account for username " + token.getPrincipal() + " is locked"); return result; }catch(ExcessiveAttemptsException eae){ result.put("status", false); result.put("errorMsg", "登录失败次数超过系统最大次数,请稍后重试!"); log_.info("登录失败次数超过系统最大次数,请稍后重试!"); //此处可对用户账户进行锁定操作 return result; } catch (Exception e) { result.put("status", false); result.put("errorMsg", "登录过程出现异常,请联系管理员!"); e.printStackTrace(); return result; } //请求成功后返回JSON格式的用户名 result.put("status", true); result.put("errorMsg", token.getPrincipal()+"登录成功!");
UrlPermissionResolver将url类型的链接转换成shiro中使用的Permission实现类Urlpermission
package org.youme.permission; import org.apache.shiro.authz.Permission; import org.apache.shiro.authz.permission.PermissionResolver; import org.apache.shiro.authz.permission.WildcardPermission; public class UrlPermissionResolver implements PermissionResolver { public Permission resolvePermission(String permissionString) { if(permissionString!=null){ if(permissionString.startsWith("/")){ return new Urlpermission(permissionString); } return new WildcardPermission(permissionString); } return new Urlpermission(""); } }
Urlpermission实现shiro的permission接口
package org.youme.permission; import org.apache.shiro.authz.Permission; import org.apache.shiro.util.AntPathMatcher; import org.apache.shiro.util.PatternMatcher; public class Urlpermission implements Permission { private String url ; public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } public Urlpermission() { } public Urlpermission(String url) { this.url = url; } public boolean implies(Permission p) { if(!(p instanceof Urlpermission))return false; Urlpermission up = (Urlpermission)p; PatternMatcher matcher = new AntPathMatcher(); return matcher.matches(url, up.getUrl()); } }
spring-shiro配置文件中定义的ResourceCheckFilter用于判定用户是否能够访问当前链接/菜单
package org.youme.filter; import java.util.logging.Logger; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.filter.AccessControlFilter; public class ResourceCheckFilter extends AccessControlFilter { private Logger log_ = Logger.getLogger(ResourceCheckFilter.class.getName()); /** * 未授权用户访问时跳转的页面路径 * */ private String unanUrl; public String getUnanUrl() { return unanUrl; } public void setUnanUrl(String unanUrl) { this.unanUrl = unanUrl; } /** * 判定用户是否具有特定权限 * */ protected boolean isAccessAllowed(ServletRequest req, ServletResponse res, Object arg2) throws Exception { //获取当前登录用户 Subject subject = getSubject(req, res); String url = getPathWithinApplication(req); return subject.isPermitted(url); } /** *用户不具备当前菜单访问权限时的处理方法 * */ protected boolean onAccessDenied(ServletRequest req, ServletResponse res) throws Exception { HttpServletResponse response = (HttpServletResponse)res; HttpServletRequest request = (HttpServletRequest)req; response.sendRedirect(request.getContextPath()+"/"+unanUrl); return false; } }
spring-shiro定义的RetryLimitHashedCredentialsMatcher用于限制短时间内密码输错的次数
package org.youme.util; import java.util.concurrent.atomic.AtomicInteger; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.credential.HashedCredentialsMatcher; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheManager; /** * 自定义密码校验,在本方法中设置最大出错次数 * */ public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher { private int errorTimes ; public int getErrorTimes() { return errorTimes; } public void setErrorTimes(int errorTimes) { this.errorTimes = errorTimes; } private Cache<String, AtomicInteger> passwordRetryCache; public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) { passwordRetryCache = cacheManager.getCache("passwordRetryCache"); } public boolean doCredentialsMatch(AuthenticationToken token,AuthenticationInfo info){ String username = (String) token.getPrincipal(); AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null){ retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } //重复输错密码5次时需要等十分钟后重试 int total = 0 ; if(errorTimes == 0){ total = 5; }else{ total = errorTimes; } if (retryCount.incrementAndGet() > total) { throw new ExcessiveAttemptsException(); } boolean matches = super.doCredentialsMatch(token, info); if(matches){ passwordRetryCache.remove(username); } return matches; } }