一、引入依赖
org.springframework spring-test 4.3.18.RELEASE test org.springframework spring-webmvc 4.3.18.RELEASE org.springframework spring-jdbc 4.3.18.RELEASE org.springframework spring-context-support 4.3.18.RELEASE org.apache.shiro shiro-spring 1.4.0 net.sf.ehcache ehcache-core 2.6.11
二、创建 ehcache 缓存配置文件
文件:ehchache.xml
四、Spring 整合 Shiro
1. 创建文件 spring-shiro.xml
2. 将 shiro 的缓存管理器交给 spring-cache 管理
package com.beovo.dsd.common.shiro.cache;import org.apache.log4j.LogManager;import org.apache.log4j.Logger;import org.apache.shiro.cache.Cache;import org.apache.shiro.cache.CacheException;import org.apache.shiro.cache.CacheManager;import org.apache.shiro.util.Destroyable;import org.springframework.util.ClassUtils;/** * 使用spring-cache作为shiro缓存- 缓存管理器 * @author Jimc. * @since 2018/11/22. */public class ShiroSpringCacheManager implements CacheManager, Destroyable { private static final Logger logger = LogManager.getLogger(ShiroSpringCacheManager.class); private org.springframework.cache.CacheManager cacheManager; private final boolean hasEhcache; public ShiroSpringCacheManager() { hasEhcache = ClassUtils.isPresent("net.sf.ehcache.Ehcache", this.getClass().getClassLoader()); } public org.springframework.cache.CacheManager getCacheManager() { return cacheManager; } public void setCacheManager(org.springframework.cache.CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override publicCache getCache(String name) throws CacheException { if (logger.isTraceEnabled()) { logger.trace("Acquiring ShiroSpringCache instance named [" + name + "]"); } org.springframework.cache.Cache cache = cacheManager.getCache(name); return new ShiroSpringCache (cache, hasEhcache); } @Override public void destroy() throws Exception { cacheManager = null; }}
package com.beovo.dsd.common.shiro.cache;import cn.hutool.log.Log;import cn.hutool.log.LogFactory;import org.apache.shiro.cache.CacheException;import org.springframework.cache.Cache;import org.springframework.cache.Cache.ValueWrapper;import java.util.*;/** * 使用spring-cache作为shiro缓存 * @author Jimc. * @since 2018/11/22. */public class ShiroSpringCacheimplements org.apache.shiro.cache.Cache { private static final Log log = LogFactory.get(); private final Cache cache; private final boolean hasEhcache; public ShiroSpringCache(Cache cache, boolean hasEhcache) { if (cache == null) { throw new IllegalArgumentException("Cache argument cannot be null."); } this.cache = cache; this.hasEhcache = hasEhcache; } @Override public V get(K key) throws CacheException { if (log.isTraceEnabled()) { log.trace("Getting object from cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass()); } ValueWrapper valueWrapper = cache.get(key); if (valueWrapper == null) { if (log.isTraceEnabled()) { log.trace("Element for [" + key + "] is null."); } return null; } return (V) valueWrapper.get(); } @Override public V put(K key, V value) throws CacheException { if (log.isTraceEnabled()) { log.trace("Putting object in cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass()); } V previous = get(key); cache.put(key, value); return previous; } @Override public V remove(K key) throws CacheException { if (log.isTraceEnabled()) { log.trace("Removing object from cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass()); } V previous = get(key); cache.evict(key); return previous; } @Override public void clear() throws CacheException { if (log.isTraceEnabled()) { log.trace("Clearing all objects from cache [" + this.cache.getName() + "]"); } cache.clear(); } @Override public int size() { if (hasEhcache) { Object nativeCache = cache.getNativeCache(); if (nativeCache instanceof net.sf.ehcache.Ehcache) { net.sf.ehcache.Ehcache ehcache = (net.sf.ehcache.Ehcache) nativeCache; return ehcache.getSize(); } } return 0; } @Override public Set keys() { if (hasEhcache) { Object nativeCache = cache.getNativeCache(); if (nativeCache instanceof net.sf.ehcache.Ehcache) { net.sf.ehcache.Ehcache ehcache = (net.sf.ehcache.Ehcache) nativeCache; return new HashSet<>(ehcache.getKeys()); } } return Collections.emptySet(); } @Override public Collection values() { if (hasEhcache) { Object nativeCache = cache.getNativeCache(); if (nativeCache instanceof net.sf.ehcache.Ehcache) { net.sf.ehcache.Ehcache ehcache = (net.sf.ehcache.Ehcache) nativeCache; List keys = ehcache.getKeys(); Map
将 缓存管理器交给 Spring 来管理,在 spring-shiro.xml 添加配置如下:
3. 自定义 Realm
(1)创建一个存放用户信息的bean
package com.beovo.dsd.common.shiro;import java.io.Serializable;import java.util.List;/** * 自定义Authentication对象,使得Subject除了携带用户的登录名外还可以携带更多信息 * @author Jimc. * @since 2018/11/23. */public class ShiroUser implements Serializable { private static final long serialVersionUID = -1373760725780840091L; /** * 主键ID */ private Long id; /** * 账号 */ private String account; /** * 姓名 */ private String name; /** * 部门id集 */ private ListdeptIds; /** * 部门名称集 */ private List deptNames; /** * 角色id集 */ private List roleIds; /** * 角色名称集 */ private List roleNames; public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getAccount() { return account; } public void setAccount(String account) { this.account = account; } public String getName() { return name; } public void setName(String name) { this.name = name; } public List getDeptIds() { return deptIds; } public void setDeptIds(List deptIds) { this.deptIds = deptIds; } public List getDeptNames() { return deptNames; } public void setDeptNames(List deptNames) { this.deptNames = deptNames; } public List getRoleIds() { return roleIds; } public void setRoleIds(List roleIds) { this.roleIds = roleIds; } public List getRoleNames() { return roleNames; } public void setRoleNames(List roleNames) { this.roleNames = roleNames; }}
(2)首先创建一个继承 AuthorizingRealm 的类
package com.beovo.dsd.common.shiro;import cn.hutool.core.collection.CollUtil;import cn.hutool.core.convert.Convert;import cn.hutool.core.util.StrUtil;import cn.hutool.log.Log;import cn.hutool.log.LogFactory;import com.beovo.dsd.po.User;import com.beovo.dsd.service.UserAuthService;import org.apache.shiro.authc.AuthenticationException;import org.apache.shiro.authc.AuthenticationInfo;import org.apache.shiro.authc.AuthenticationToken;import org.apache.shiro.authc.UsernamePasswordToken;import org.apache.shiro.authc.credential.CredentialsMatcher;import org.apache.shiro.authz.AuthorizationInfo;import org.apache.shiro.authz.SimpleAuthorizationInfo;import org.apache.shiro.cache.CacheManager;import org.apache.shiro.realm.AuthorizingRealm;import org.apache.shiro.subject.PrincipalCollection;import org.springframework.beans.factory.annotation.Autowired;import java.util.HashSet;import java.util.List;import java.util.Set;/** * shiro权限认证 * @author Jimc. * @since 2018/11/22. */public class ShiroDbRealm extends AuthorizingRealm { private static final Log log = LogFactory.get(); @Autowired private UserAuthService userAuthService; public ShiroDbRealm(CacheManager cacheManager, CredentialsMatcher matcher) { super(cacheManager, matcher); } /** * Shiro登录认证(原理:用户提交 用户名和密码 --- shiro 封装令牌 ---- realm 通过用户名将密码查询返回 ---- shiro 自动去比较查询出密码和用户输入密码是否一致---- 进行登陆控制) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { log.info("Shiro开始登录认证"); UsernamePasswordToken token = Convert.convert(UsernamePasswordToken.class, authcToken); User user = userAuthService.getUser(token.getUsername()); ShiroUser shiroUser = userAuthService.getShiroUser(user); // 认证缓存信息 return userAuthService.info(shiroUser, user, getName()); } /** * Shiro权限认证 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { ShiroUser shiroUser = Convert.convert(ShiroUser.class, principals.getPrimaryPrincipal()); ListroleIds = shiroUser.getRoleIds(); Set permissionSet = new HashSet<>(); Set roleAliasSet = new HashSet<>(); for (Long roleId : roleIds) { List permissions = userAuthService.getPermissionsByRoleId(roleId); if (CollUtil.isNotEmpty(permissions)) { for (String permission : permissions) { if (StrUtil.isNotEmpty(permission)) { permissionSet.add(permission); } } } String roleAlias = userAuthService.getRoleAliasByRoleId(roleId); roleAliasSet.add(roleAlias); } SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); info.addStringPermissions(permissionSet); info.addRoles(roleAliasSet); return info; }}
(3)加入到 spring-shiro.xml 配置中
3. 添加会话管理器
4. 添加Remember管理器
5. 添加密码加密配置
package com.beovo.dsd.common.shiro;/** * shiro密码加密配置 * * @author Jimc. * @since 2018/11/22. */public class PasswordHash { /** * 加密算法名称 */ private String algorithmName; /** * 密码hash次数 */ private Integer hashIterations; public String getAlgorithmName() { return algorithmName; } public void setAlgorithmName(String algorithmName) { this.algorithmName = algorithmName; } public Integer getHashIterations() { return hashIterations; } public void setHashIterations(Integer hashIterations) { this.hashIterations = hashIterations; } /** * 密码加密 * * @param source 加密源对象 * @param salt 加密盐 * @return 加密后的字符 */ public String encrypt(Object source, Object salt) { return ShiroKit.encrypt(algorithmName, source, salt, hashIterations); }}
6. 添加密码错误次数锁定功能
package com.beovo.dsd.common.shiro;import cn.hutool.log.Log;import cn.hutool.log.LogFactory;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;import org.springframework.beans.factory.InitializingBean;import org.springframework.util.Assert;import java.util.concurrent.atomic.AtomicInteger;/** * 输错5次密码锁定半小时,ehcache.xml配置 * @author Jimc. * @since 2018/11/22. */public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher implements InitializingBean { private static final Log log = LogFactory.get(); private final static String DEFAULT_CACHE_NAME = "retryLimitCache"; private final CacheManager cacheManager; private String retryLimitCacheName; private CachepasswordRetryCache; private PasswordHash passwordHash; public RetryLimitCredentialsMatcher(CacheManager cacheManager) { this.cacheManager = cacheManager; this.retryLimitCacheName = DEFAULT_CACHE_NAME; } public String getRetryLimitCacheName() { return retryLimitCacheName; } public void setRetryLimitCacheName(String retryLimitCacheName) { this.retryLimitCacheName = retryLimitCacheName; } public void setPasswordHash(PasswordHash passwordHash) { this.passwordHash = passwordHash; } @Override public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) { String username = (String) token.getPrincipal(); //retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if(retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if(retryCount.incrementAndGet() > 5) { //if retry count > 5 throw String message = "用户名: " + username + " 密码连续输入错误超过5次,锁定半小时!"; log.warn(message); throw new ExcessiveAttemptsException(message); } else { passwordRetryCache.put(username, retryCount); } boolean matches = super.doCredentialsMatch(token, info); if(matches) { //clear retry data passwordRetryCache.remove(username); } return matches; } @Override public void afterPropertiesSet() throws Exception { Assert.notNull(passwordHash, "you must set passwordHash!"); super.setHashAlgorithmName(passwordHash.getAlgorithmName()); super.setHashIterations(passwordHash.getHashIterations()); this.passwordRetryCache = cacheManager.getCache(retryLimitCacheName); }}
7. 添加拦截器
/login = anon /captcha = anon /resources/** = anon /** = user
package com.beovo.dsd.common.shiro.filter;import cn.hutool.core.util.StrUtil;import org.apache.shiro.web.filter.authc.UserFilter;import org.apache.shiro.web.util.WebUtils;import javax.servlet.ServletRequest;import javax.servlet.ServletResponse;import javax.servlet.http.HttpServletRequest;import javax.servlet.http.HttpServletResponse;/** * ajax shiro session超时统一处理 * @author Jimc. * @since 2018/11/23. */public class ShiroAjaxSessionFilter extends UserFilter { @Override protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception { HttpServletRequest req = WebUtils.toHttp(request); String xmlHttpRequest = req.getHeader("X-Requested-With"); if (StrUtil.isNotBlank(xmlHttpRequest)) { if (xmlHttpRequest.equalsIgnoreCase("XMLHttpRequest")) { HttpServletResponse res = WebUtils.toHttp(response); // 采用res.sendError(401);在Easyui中会处理掉error,$.ajaxSetup中监听不到 res.setHeader("oauthstatus", "401"); return false; } } return super.onAccessDenied(request, response); }}
8. 将自定义Realm、缓存管理器、会话管理器以及Remember管理器交给shiro的安全管理器
9. 提供一个shiro的工具类
package com.beovo.dsd.common.shiro;import cn.hutool.core.collection.CollUtil;import cn.hutool.core.convert.Convert;import cn.hutool.core.util.ObjectUtil;import cn.hutool.core.util.RandomUtil;import cn.hutool.core.util.StrUtil;import org.apache.shiro.SecurityUtils;import org.apache.shiro.crypto.hash.Md5Hash;import org.apache.shiro.crypto.hash.SimpleHash;import org.apache.shiro.session.Session;import org.apache.shiro.subject.Subject;import org.apache.shiro.util.ByteSource;import org.apache.shiro.web.mgt.DefaultWebSecurityManager;import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;import java.util.Collection;/** * shiro工具类 * * @author Jimc. * @since 2018/11/23. */public class ShiroKit { private static final String NAMES_DELIMITER = ","; /** * 密码盐长度 */ private static final int SALT_LENGTH = 16; /** * shiro密码加密 * * @param algorithmName 算法 * @param source 源对象(密码) * @param salt 密码盐 * @param hashIterations hash次数 * @return 加密后的字符 */ public static String encrypt(String algorithmName, Object source, Object salt, int hashIterations) { ByteSource saltSource = new Md5Hash(salt); return new SimpleHash(algorithmName, source, saltSource, hashIterations).toString(); } /** * 获取随机16位盐值 */ public static String getRandomSalt() { return RandomUtil.randomString(SALT_LENGTH); } /** * 获取当前 Subject * * @return Subject */ public static Subject getSubject() { return SecurityUtils.getSubject(); } /** * 获取封装的 ShiroUser * * @return ShiroUser */ public static ShiroUser getUser() { if (isGuest()) { return null; } else { return Convert.convert(ShiroUser.class, getSubject().getPrincipals().getPrimaryPrincipal()); } } /** * 从shiro获取session */ public static Session getSession() { return getSubject().getSession(); } /** * 获取shiro指定的sessionKey */ @SuppressWarnings("unchecked") public staticT getSessionAttr(String key) { Session session = getSession(); return ObjectUtil.isNotNull(session) ? (T) session.getAttribute(key) : null; } /** * 设置shiro指定的sessionKey */ public static void setSessionAttr(String key, Object value) { Session session = getSession(); session.setAttribute(key, value); } /** * 移除shiro指定的sessionKey */ public static void removeSessionAttr(String key) { Session session = getSession(); if (ObjectUtil.isNotNull(session)) { session.removeAttribute(key); } } /** * 验证当前用户是否属于该角色?,使用时与lacksRole 搭配使用 * * @param roleName 角色名 * @return 属于该角色:true,否则false */ public static boolean hasRole(String roleName) { return ObjectUtil.isNotNull(getSubject()) && StrUtil.isNotEmpty(roleName) && getSubject().hasRole(roleName); } /** * 与hasRole标签逻辑相反,当用户不属于该角色时验证通过。 * * @param roleName 角色名 * @return 不属于该角色:true,否则false */ public static boolean lacksRole(String roleName) { return !hasRole(roleName); } /** * 验证当前用户是否属于以下任意一个角色。 * * @param roleNames 角色列表 * @return 属于:true,否则false */ public static boolean hasAnyRoles(String roleNames) { boolean hasAnyRole = false; Subject subject = getSubject(); if (ObjectUtil.isNotNull(getSubject()) && StrUtil.isNotEmpty(roleNames)) { for (String role : roleNames.split(NAMES_DELIMITER)) { if (subject.hasRole(role.trim())) { hasAnyRole = true; break; } } } return hasAnyRole; } /** * 验证当前用户是否属于以下所有角色。 * * @param roleNames 角色列表 * @return 属于:true,否则false */ public static boolean hasAllRoles(String roleNames) { boolean hasAllRole = true; Subject subject = getSubject(); if (ObjectUtil.isNotNull(getSubject()) && StrUtil.isNotEmpty(roleNames)) { for (String role : roleNames.split(NAMES_DELIMITER)) { if (!subject.hasRole(role.trim())) { hasAllRole = false; break; } } } return hasAllRole; } /** * 验证当前用户是否拥有指定权限,使用时与lacksPermission 搭配使用 * * @param permission 权限名 * @return 拥有权限:true,否则false */ public static boolean hasPermission(String permission) { return ObjectUtil.isNotNull(getSubject()) && StrUtil.isNotEmpty(permission) && getSubject().isPermitted(permission); } /** * 与hasPermission标签逻辑相反,当前用户没有制定权限时,验证通过。 * * @param permission 权限名 * @return 拥有权限:true,否则false */ public static boolean lacksPermission(String permission) { return !hasPermission(permission); } /** * 已认证通过的用户。不包含已记住的用户,这是与user标签的区别所在。与notAuthenticated搭配使用 * * @return 通过身份验证:true,否则false */ public static boolean isAuthenticated() { return ObjectUtil.isNotNull(getSubject()) && getSubject().isAuthenticated(); } /** * 未认证通过用户,与authenticated标签相对应。与guest标签的区别是,该标签包含已记住用户。。 * * @return 没有通过身份验证:true,否则false */ public static boolean notAuthenticated() { return !isAuthenticated(); } /** * 认证通过或已记住的用户。与guset搭配使用。 * * @return 用户:true,否则 false */ public static boolean isUser() { return ObjectUtil.isNotNull(getSubject()) && ObjectUtil.isNotNull(getSubject().getPrincipal()); } /** * 验证当前用户是否为“访客”,即未认证(包含未记住)的用户。用user搭配使用 * * @return 访客:true,否则false */ public static boolean isGuest() { return !isUser(); } /** * 输出当前用户信息,通常为登录帐号信息。 * * @return 当前用户信息 */ public static String principal() { Subject subject = getSubject(); if (ObjectUtil.isNotNull(subject)) { Object principal = subject.getPrincipal(); return principal.toString(); } return ""; } /** * 单用户登录时,判断用户是否已经登录 */ public static boolean isLogin(String username) { Collection sessions = ((DefaultWebSessionManager) (((DefaultWebSecurityManager) SecurityUtils .getSecurityManager()).getSessionManager())).getSessionDAO().getActiveSessions(); if (CollUtil.isNotEmpty(sessions)) { for (Session session : sessions) { String _username = Convert.toStr(session.getAttribute("username")); if (StrUtil.isNotEmpty(_username) && StrUtil.equals(username, _username)) { return true; } } } return false; } /** * 获取当前用户的部门数据范围的集合 */ /*public static List getDeptDataScope() { Long deptId = getUser().getDeptId();// List subDeptIds = ConstantFactory.me().getSubDeptId(deptId);// subDeptIds.add(deptId); return subDeptIds; }*/ /** * 判断当前用户是否是超级管理员 */ /*public static boolean isAdmin() { Set roles = ShiroKit.getUser().getRoles(); for (Long role : roles) { String singleRoleTip = ConstantFactory.me().getSingleRoleTip(role); if (singleRoleTip.equals(Const.ADMIN_NAME)) { return true; } } return false; }*/}
五、在web.xml添加shiro的过滤器
shiroFilter org.springframework.web.filter.DelegatingFilterProxy targetFilterLifecycle true shiroFilter /* REQUEST FORWARD