实现登陆次数限制,RemberMe功能以及验证码功能的整合
登陆次数限制
实现:通过重写HashedCredentialsMatcher
这个类实现,并且使用Ehcache管理内存
实现类:
public class RetryLimitHashedCredentialsMatcher extends HashedCredentialsMatcher {
private Cache<String, AtomicInteger> passwordRetryCache;
/**
* @param cacheManager 传进一个缓存池
*/
public RetryLimitHashedCredentialsMatcher(CacheManager cacheManager) {
passwordRetryCache = cacheManager.getCache("passwordRetryCache");
}
@Override
public boolean doCredentialsMatch(AuthenticationToken token, AuthenticationInfo info) {
String username = (String) token.getPrincipal();
AtomicInteger retryCount = passwordRetryCache.get(username);
if (retryCount == null) {
// 高并发下使用的线程安全的int类
retryCount = new AtomicInteger(0);
passwordRetryCache.put(username, retryCount);
}
// 自定义一个验证过程:当用户连续输入密码错误2次以上禁止用户登录一段时间
if (retryCount.incrementAndGet() > 2) {
throw new ExcessiveAttemptsException();
}
boolean match = super.doCredentialsMatch(token, info);
log.info(String.valueOf(match));
if (match) {
passwordRetryCache.remove(username);
}
return match;
}
Ehcache.xml
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="http://www.ehcache.org/ehcache.xsd">
<diskStore path="java.io.tmpdir/ehcache" />
<!-- 默认缓存 -->
<defaultCache
maxElementsInMemory="1000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="false" />
<!--
配置自定义缓存
maxElementsInMemory:缓存中允许创建的最大对象数
eternal:缓存中对象是否为永久的,如果是,超时设置将被忽略,对象从不过期。
timeToIdleSeconds:缓存数据的钝化时间,也就是在一个元素消亡之前,
两次访问时间的最大时间间隔值,这只能在元素不是永久驻留时有效,
如果该值是 0 就意味着元素可以停顿无穷长的时间。
timeToLiveSeconds:缓存数据的生存时间,也就是一个元素从构建到消亡的最大时间间隔值,
这只能在元素不是永久驻留时有效,如果该值是0就意味着元素可以停顿无穷长的时间。
overflowToDisk:内存不足时,是否启用磁盘缓存。
memoryStoreEvictionPolicy:缓存满了之后的淘汰算法。
-->
<!-- 密码尝试次数的缓存 -->
<cache name="passwordRetryCache"
maxElementsInMemory="2000"
eternal="false"
timeToIdleSeconds="60"
timeToLiveSeconds="0"
overflowToDisk="false"
statistics="true">
</cache>
</ehcache>
spring-shiro.xml
<!-- 凭证匹配器 自定义实现了密码次数限制 MD5算法实现 -->
<bean id="credentialsMatcher"
class="com.mdy.student.shiro.RetryLimitHashedCredentialsMatcher">
<constructor-arg ref="cacheManager" />
<property name="hashAlgorithmName" value="MD5" />
<property name="hashIterations" value="2" />
</bean>
(也可以自己使用注解注入)
RemberMe功能实现
直接在xml配置就可以了
spring-shiro.xml
<!-- 会话Cookie模板 -->
<bean id="sessionIdCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="sid" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="-1" />
</bean>
<bean id="rememberMeCookie" class="org.apache.shiro.web.servlet.SimpleCookie">
<constructor-arg value="rememberMe" />
<property name="httpOnly" value="true" />
<property name="maxAge" value="604800" /><!-- 7天 -->
</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>
验证码功能实现
实现:在后台使用Graphic类生成随机验证码图像,再使用response传回前端,使用sess,保存验证码,接受前端传回来的验证码进行比较
图像生成类的实现:
public final class GraphicsUtil {
/**
* @param width 验证码图片宽
* @param height 验证码图片长
* @param imgType 验证码图片类型
* @param sb 验证码
* @return 生成的验证码图片对象
*/
public static BufferedImage create(final int width, final int height, final String imgType,StringBuffer sb) {
Random random = new Random();
BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
Graphics graphic = image.getGraphics();
graphic.setColor(Color.getColor("F8F8F8"));
graphic.fillRect(0, 0, width, height);
Color[] colors = new Color[] { Color.BLUE, Color.GRAY, Color.GREEN, Color.RED, Color.BLACK, Color.ORANGE,
Color.CYAN };
// 在 "画板"上生成干扰线条 ( 50 是线条个数)
for (int i = 0; i < 50; i++) {
graphic.setColor(colors[random.nextInt(colors.length)]);
final int x = random.nextInt(width);
final int y = random.nextInt(height);
final int w = random.nextInt(20);
final int h = random.nextInt(20);
final int signA = random.nextBoolean() ? 1 : -1;
final int signB = random.nextBoolean() ? 1 : -1;
graphic.drawLine(x, y, x + w * signA, y + h * signB);
}
// 在 "画板"上绘制字母
graphic.setFont(new Font("Comic Sans MS", Font.BOLD, 35));
for (int i = 0; i < 4; i++) {
final int temp = random.nextInt(26) + 97;
String s = String.valueOf((char) temp);
sb.append(s);
graphic.setColor(colors[random.nextInt(colors.length)]);
graphic.drawString(s, i * (width / 4), height - (height / 3));
}
graphic.dispose();
return image;
}
}
controller传回验证码的实现:
@RequestMapping(value = "/setImage", method = RequestMethod.GET)
public void setImage(HttpServletResponse response, HttpServletRequest request) {
StringBuffer code = new StringBuffer();
BufferedImage image = GraphicsUtil.create(WIDTH, HEIGHT, "jpg", code);
request.getSession().setAttribute("code", code);
// 禁止图像缓存。
response.setHeader("Pragma", "no-cache");
response.setHeader("Cache-Control", "no-cache");
response.setDateHeader("Expires", 0);
response.setContentType("image/jpeg");
// 将图像输出到Servlet输出流中。
try {
ServletOutputStream sos = response.getOutputStream();
ImageIO.write(image, "jpeg", sos);
sos.close();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
前端的显示:(js在地址后加时间是为了确保img的src的地址不同,不然图片不会更换)
<script type="text/javascript">
function changeImage(img) {
img.src = img.src + '?' +new Date() ;
}
</script>
<label class="form-label">验证码</label>
<input type="text" name="code" required="required" maxlength="6" style="width: 50px">
<img src="/setImage" onclick="changeImage(this)" id="image" alt="换一张"/>
判断代码则是直接去session值进行比较,虽然整合在shiro里面,但是要重写表单验证类FormAuthenticationFilter
,博主尝试后发现验证成功后会自动跳转到之前的页面,导致验证验证码两次,并没有找到有效的解决方法。。。还是直接在控制层判断方便。。。