该架构采用spring + springMVC + shiro + ehcache搭建有哪里不对的地方请大神指明,万分感谢!!
先来个标题
<description>Shiro安全配置</description> //别以为没啥用,这行代码代表功能的开始搭建,虽然对功能没什么软用。。。
接下来是 shiroFilter 这个名称和要 web.xml 里面配置的 shiroFilter 相对应,
说到这里了,顺便说下 web.xml 里面 shiroFilter 拦截和 springMVC 拦截
按说 / 和 /* 都是拦截,区别就在与一个有返回,一个没有返回,
但是 springMVC 必须配置 / ,而 shiroFilter 必须配置 /* ,否则 shiroFilter 会拦截不到,
看网上好多都说 shiroFiter 必须在 springMVC 前面,经测试,两者间的顺序没有讲究,
在一个,登陆时候会拦截三次,按照本人的想法是,硬编码一次,springMVC 一次,shiroFilter 一次,具体原因因为没有测试,所以不明。
<!-- 启用shrio授权注解拦截方式 --> <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
<!-- 这个属性是必须的,否侧会报 SecurityManager property must be set --> <property name="securityManager" ref="securityManager"/>
<!-- 登陆地址 --> <property name="loginUrl" value="/login"/>
<!-- 登陆成功地址 --> <property name="successUrl" value="/user"/>
<!--
还有个配置,是没有权限的登陆地址,感觉没有什么用,所以没有配置,
登陆成功之后一般都是固定到上次访问的路径,而上次访问的路径都是不需要的验证的,而在页面上显示的都是该用户可以访问的路径,
没有权限访问的路径一般都不会显示,所以我觉的没有必要配置,或者是本人没有碰到需要权限才能访问的的情况吧,总之这里没有配置。
-->
<!-- 未经授权的 -->
<!-- <property name="unauthorizedUrl" value="/login/unauthorized" /> -->
<!--
anon,authc,authcBasic,user 是第一组认证过滤器
perms,port,rest,roles,ssl 是第二组授权过滤器
logout 退出, noSessionCreation 缓存
相对应的功能可以上网查查,这里不细究了
--> <property name="filterChainDefinitions"> <value> /captcha.jpg = anon /static/** = anon /logout = anon /login = anon /member/** = anon /manage/** = user </value> </property>
<-- 也可以自定义 Filter -->
<--
<property name="filters">
<map>
<entry key="backstage" value-ref="backstageAuthenticationFilter"/>
</map>
--> </bean>
<!-- 对应的filters的bean -->
<!--
<bean id="backstageAUthenticationFilter" class="org.apache.shiro.web.filter.authc.FormAuthenticationFilter">
<property name="loginUrl" value="/manage/login"/>
<property name="successUrl" value="/backstage/manage/company/index"/>
</bean>
-->
接下来是安全管理器,三个属性
<!--安全管理器 --> <bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager">
<!-- 自定义realm --> <property name="realm" ref="myRealm"/> <!-- 定义缓存管理器 --> <property name="cacheManager" ref="shiroCacheManager"/> <!-- 会话管理 --> <property name="sessionManager" ref="sessionManager"/> </bean>
然后是管理器里面的三个属性
<!-- 自定realm --> <bean id="myRealm" class="com.conferencerooms.utils.shiro.MyRealm"> <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="shiroCacheManager" class="org.apache.shiro.cache.MemoryConstrainedCacheManager" /> --> <!-- spring-ehcache --> <bean id="shiroCacheManager" class="com.conferencerooms.utils.shiro.cache.ShiroCacheManager"> <property name="cacheManager" ref="cacheManager"/> </bean>
<!-- 会话管理 --> <bean id="sessionManager" class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"> <!-- session的失效时长,单位毫秒 --> <property name="globalSessionTimeout" value="600000"/> <!-- 删除失效的session --> <property name="deleteInvalidSessions" value="true"/> <!-- 可选属性,不加该属性,在服务器重启的时候会报sessionId错误,但不影响正常登陆 --> <!-- session 是创建在服务器,当服务器重启,服务器session会初始化,而页面cookic会保存, 当没有正常退出而再次请求的时候服务器sessionId不存在,所以会报空指针异常 --> <property name="sessionIdCookie" ref="sessionIdCookie"/> </bean>
对应会话管理的最后一条属性,代码里有注释,建议看看
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg name="name" value="ycyintang.session.id"/>
</bean>
会话DAO
<!-- 会话DAO 用于会话的CRUD 待细研究 --> <bean id="sessionDAO" class="org.apache.shiro.session.mgt.eis.EnterpriseCacheSessionDAO"> <property name="activeSessionsCacheName" value="activeSessionCache"/> <property name="cacheManager" ref="shiroCacheManager"/> </bean>
这个是生命周期
<!-- 生命周期 --> <bean id="lifecycleBeanPostProcessor" class="org.apache.shiro.spring.LifecycleBeanPostProcessor"/>
shiro密码加密配置
<!-- shiro密码加密配置 --> <bean id="passwordHash" class="com.conferencerooms.utils.shiro.PasswordHash">
<!-- 这里使用的是md5 也可以使用sha --> <property name="algorithmName" value="md5"/>
<!-- 这里可以更改加密次数,已达到密码的复杂程度 --> <property name="hashIterations" value="1"/> <!-- storedCredentialsHexEncoded默认是true,此时用的是密码加密用的是Hex编码;false时用Base64编码 --> <!-- <property name="storedCredentialsHexEncoded" value="true"/> --> </bean>
密码错误一定次数后锁定账号
<!-- 密码错误5次锁定半小时 自定义 --> <bean id="credentialsMatcher" class="com.conferencerooms.utils.shiro.RetryLimitCredentialsMatcher"> <constructor-arg ref="shiroCacheManager"/> <property name="retryLimitCacheName" value="halfHour"/> <property name="passwordHash" ref="passwordHash"/> </bean>
shiro记住密码功能
<!-- 记住密码Cookie --> <bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie"> <constructor-arg value="rememberMe"/> <property name="httpOnly" value="true" /> <property name="maxAge" value="604800"/> </bean> <!-- rememberMe管理器,记住密码用的是AES加密,生成16位的key 功能是实现了,具体怎么实现的不是很清楚,带研究 --> <bean id="rememberMeManager" class="org.apache.shiro.web.mgt.CookieRememberMeManager"> <property name="cipherKey" value="#{T(org.apache.shiro.codec.Base64).decode('5aaC5qKm5oqA5pyvAAAAAA==')}"/> <property name="cookie" ref="rememberMeCookie"/> </bean>
注入securityManager
<!-- 在方法中 注入 securityManager ,进行代理控制 具体有什么功能待细研究 --> <bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="staticMethod" value="org.apache.shiro.SecurityUtils.setSecurityManager"/> <property name="arguments" ref="securityManager"/> </bean>
<!-- ajax session 超时处理 -->
<!-- ajax session超时时处理 -->
<bean id="ajaxSessionFilter" class="com.conferencerooms.utils.shiro.ShiroAjaxSessionFilter"/>
<! ---------------------------------------------------------------------------------------------------- -->
下面是配置文件需要的一些java类
myRealm 自定义realm
package com.conferencerooms.utils.shiro; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.authc.AccountException; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; 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.apache.shiro.subject.SimplePrincipalCollection; import org.springframework.beans.factory.annotation.Autowired; import com.conferencerooms.entity.User; import com.conferencerooms.service.UserService; /** * 自定义realm * * @author ForeignStudent * @version 2017/9/20 */ public class MyRealm extends AuthorizingRealm { private static final Logger LOGGER = LogManager.getLogger(MyRealm.class); @Autowired private UserService userService; public MyRealm(CacheManager cacheManager, CredentialsMatcher matcher) { super(cacheManager, matcher); } /** * Shiro登录认证(原理:用户提交 用户名和密码 --- shiro 封装令牌 ---- realm 通过用户名将密码查询返回 ---- * shiro 自动去比较查询出密码和用户输入密码是否一致---- 进行登陆控制 ) */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authcToken) throws AuthenticationException { LOGGER.info("Shiro开始登录认证 **********doGetAuthenticationInfo**************"); UsernamePasswordToken token = (UsernamePasswordToken) authcToken; String loginName = token.getUsername(); if (loginName == null) { throw new AccountException("用户名不能为空"); } User user = userService.getUsersByLoginName(loginName); if (user != null && user.getDelFlag() != 0 && user.getStatus() != -1) { ShiroUsers shiroUser = new ShiroUsers(user.getUserId(), user.getLoginName(), user.getUserName(), user); return new SimpleAuthenticationInfo(shiroUser, user.getPassword(), ShiroByteSource.of(user.getSalt()), getName()); } else { throw new AccountException("没有此用户"); } } /** * Shiro权限认证 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { ShiroUsers shiroUsers = (ShiroUsers) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); /* * info.setRoles(shiroUsers.getRoles()); * info.addStringPermissions(shiroUsers.getCodes()); */ return null; } @Override public void onLogout(PrincipalCollection principals) { super.clearCachedAuthorizationInfo(principals); ShiroUsers shiroUser = (ShiroUsers) principals.getPrimaryPrincipal(); removeUserCache(shiroUser); } /** * 清除用户缓存 * * @param shiroUser */ public void removeUserCache(ShiroUsers shiroUsers) { removeUserCache(shiroUsers.getLoginName()); } /** * 清除用户缓存 * * @param loginName */ public void removeUserCache(String loginName) { SimplePrincipalCollection principals = new SimplePrincipalCollection(); principals.add(loginName, super.getName()); super.clearCachedAuthenticationInfo(principals); } }
ShiroCacheManager 自定义缓存管理器
package com.conferencerooms.utils.shiro.cache; import org.apache.logging.log4j.LogManager; import org.apache.logging.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; public class ShiroCacheManager implements CacheManager, Destroyable { private static final Logger logger = LogManager.getLogger(ShiroCacheManager.class); private org.springframework.cache.CacheManager cacheManager; public org.springframework.cache.CacheManager getCacheManager() { return cacheManager; } public void setCacheManager(org.springframework.cache.CacheManager cacheManager) { this.cacheManager = cacheManager; } @Override public <K, V> Cache<K, V> 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 ShiroCache<K, V>(cache); } @Override public void destroy() throws Exception { cacheManager = null; } }
ShiroCacheManager 需要的一个类
ShiroCache
package com.conferencerooms.utils.shiro.cache; import java.util.Collection; import java.util.Collections; import java.util.Set; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.shiro.cache.CacheException; import org.springframework.cache.Cache; import org.springframework.cache.Cache.ValueWrapper; @SuppressWarnings("unchecked") public class ShiroCache<K, V> implements org.apache.shiro.cache.Cache<K, V> { private static final Logger logger = LogManager.getLogger(ShiroCache.class); private final org.springframework.cache.Cache cache; public ShiroCache(Cache cache) { if (cache == null) { throw new IllegalArgumentException("Cache argument cannot be null."); } this.cache = cache; } @Override public V get(K key) throws CacheException { if (logger.isTraceEnabled()) { logger.trace("Getting object from cache [" + this.cache.getName() + "] for key [" + key + "]key type:" + key.getClass()); } ValueWrapper valueWrapper = cache.get(key); if (valueWrapper == null) { if (logger.isTraceEnabled()) { logger.trace("Element for [" + key + "] is null."); } return null; } return (V) valueWrapper.get(); } @Override public V put(K key, V value) throws CacheException { if (logger.isTraceEnabled()) { logger.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 (logger.isTraceEnabled()) { logger.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 (logger.isTraceEnabled()) { logger.trace("Clearing all objects from cache [" + this.cache.getName() + "]"); } cache.clear(); } @Override public int size() { return 0; } @Override public Set<K> keys() { return Collections.emptySet(); } @Override public Collection<V> values() { return Collections.emptySet(); } @Override public String toString() { return "ShiroSpringCache [" + this.cache.getName() + "]"; } }
PasswordHash 密码加密配置
package com.conferencerooms.utils.shiro; import org.apache.shiro.crypto.hash.SimpleHash; import org.springframework.beans.factory.InitializingBean; import org.springframework.util.Assert; /** * shiro密码加密配置 * * @author ForeignStudent * @version 2017/9/20 */ public class PasswordHash implements InitializingBean { private String algorithmName; private int hashIterations; @Override public void afterPropertiesSet() throws Exception { Assert.hasLength(algorithmName, "algorithmName mast be MD5、SHA-1、SHA-256、SHA-384、SHA-512"); } public String toHex(Object source, Object salt) { return new SimpleHash(algorithmName, source, salt, hashIterations).toHex(); } public String getAlgorithmName() { return algorithmName; } public void setAlgorithmName(String algorithmName) { this.algorithmName = algorithmName; } public int getHashIterations() { return hashIterations; } public void setHashIterations(int hashIterations) { this.hashIterations = hashIterations; } }
RetryLimitCredentialsMatcher 锁定 这里锁定的是5次
package com.conferencerooms.utils.shiro; import java.util.concurrent.atomic.AtomicInteger; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; 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; /** * 密码错误5次锁定半小时 * * @author ForeignStudent * @version 2017/9/20 */ public class RetryLimitCredentialsMatcher extends HashedCredentialsMatcher implements InitializingBean { private final static Logger logger = LogManager.getLogger(RetryLimitCredentialsMatcher.class); private final static String DEFAULT_CHACHE_NAME = "retryLimitCache"; private final CacheManager cacheManager; private String retryLimitCacheName; private Cache<String, AtomicInteger> passwordRetryCache; private PasswordHash passwordHash; public RetryLimitCredentialsMatcher(CacheManager cacheManager) { this.cacheManager = cacheManager; this.retryLimitCacheName = DEFAULT_CHACHE_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 authcToken, AuthenticationInfo info) { String username = (String) authcToken.getPrincipal(); // retry count + 1 AtomicInteger retryCount = passwordRetryCache.get(username); if (retryCount == null) { retryCount = new AtomicInteger(0); passwordRetryCache.put(username, retryCount); } if (retryCount.incrementAndGet() > 5) { logger.warn("username: " + username + " tried to login more than 5 times in period"); throw new ExcessiveAttemptsException("用户名: " + username + " 密码连续输入错误超过5次,锁定半小时!"); } boolean matches = super.doCredentialsMatch(authcToken, info); if (matches) { 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); } }
<!-- ajax session超时时处理 -->
package com.conferencerooms.utils.shiro; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.shiro.web.filter.authc.UserFilter; import org.apache.shiro.web.util.WebUtils; import com.conferencerooms.utils.StringUtils; /** * ajax超时处理类 * * @author ForeignStudent * @version 2017/9/20 */ 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 (StringUtils.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); } }
shiro配置到此为之差不多了,以上大部分内容是网上整理来的 都是经过测试可用的,其中多处都是手打,测试时多注意下就OK了
其中有几个地方不是很明白,在注释中也名提了下,如果哪位大神可以告知,感激不尽。