好久没更博了...

最近看了个真正全注解实现的 SpringMVC 博客,感觉很不错,终于可以彻底丢弃 web.xml 了。其实这玩意也是老东西了,丢弃 web.xml,是基于 5、6年前发布的 Servlet 3.0 规范,只不过少有人玩而已...现在4.0都快正式发布了...Spring对注解的支持也从09年底就开始支持了...

基础部分我就不仔细讲了,可以先看一下这篇 以及其中提到的另外两篇文章,这三篇文章讲的很不错。

下面开始旧东西新玩~~~

构建

项目是基于 gradle 3.1构建的,这是项目依赖:

dependencies {
def springVersion = '4.3.2.RELEASE' compile "org.springframework:spring-web:$springVersion"
compile "org.springframework:spring-webmvc:$springVersion"
compile "redis.clients:jedis:2.9.0"
compile "javax.servlet:javax.servlet-api:3.1.0"
compile "org.json:json:20160810"
}

编写Java版的web.xml

想要让请求经过Java,少不了配置 web.xml,不过现在我们来写个Java版的~

这里和传统的 web.xml 一样,依次添加 filterservlet

package org.xueliang.loginsecuritybyredis.commons;

import javax.servlet.FilterRegistration;
import javax.servlet.Servlet;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration; import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.filter.CharacterEncodingFilter;
import org.springframework.web.servlet.DispatcherServlet; /**
* 基于注解的/WEB-INF/web.xml
* 依赖 servlet 3.0
* @author XueLiang
* @date 2016年10月24日 下午5:58:45
* @version 1.0
*/
public class CommonInitializer implements WebApplicationInitializer { @Override
public void onStartup(ServletContext servletContext) throws ServletException { // 基于注解配置的Web容器上下文
AnnotationConfigWebApplicationContext context = new AnnotationConfigWebApplicationContext();
context.register(WebAppConfig.class); // 添加编码过滤器并进行映射
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter("UTF-8", true);
FilterRegistration.Dynamic dynamicFilter = servletContext.addFilter("characterEncodingFilter", characterEncodingFilter);
dynamicFilter.addMappingForUrlPatterns(null, true, "/*"); // 添加静态资源映射
ServletRegistration defaultServletRegistration = servletContext.getServletRegistration("default");
defaultServletRegistration.addMapping("*.html"); Servlet dispatcherServlet = new DispatcherServlet(context);
ServletRegistration.Dynamic dynamicServlet = servletContext.addServlet("dispatcher", dispatcherServlet);
dynamicServlet.addMapping("/");
}
}

这一步走完,Spring 基本上启动起来了。

编写Java版的Spring配置

现在Spring已经可以正常启动了,但我们还要给 Spring 做一些配置,以便让它按我们需要的方式工作~

这里因为后端只负责提供数据,而不负责页面渲染,所以只需要配置返回 json 视图即可,个人比较偏爱采用内容协商,所以这里我使用了 ContentNegotiationManagerFactoryBean,但只配置了一个 JSON 格式的视图。

为了避免中文乱码,这里设置了 StringHttpMessageConverter 默认编码格式为 UTF-8,然后将其设置为 RequestMappingHandlerAdapter 的消息转换器。

最后还需要再配置一个欢迎页,类似于 web.xmlwelcome-file-list - welcome-file,因为 Servlet 3.0 规范没有针对欢迎页的Java配置方案,所以目前只能在Java中这样配置,其效果类似于在XML版中配置 <mvc:redirect-view-controller path="/" redirect-url="/index.html"/>

最后注意这里的 @Bean 注解,默认的 name 是方法名。

package org.xueliang.loginsecuritybyredis.commons;

import java.nio.charset.Charset;
import java.util.Collections;
import java.util.Properties; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.http.MediaType;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.accept.ContentNegotiationManager;
import org.springframework.web.accept.ContentNegotiationManagerFactoryBean;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.view.ContentNegotiatingViewResolver; @Configuration
@EnableWebMvc
@ComponentScan(basePackages = "org.xueliang.loginsecuritybyredis")
@PropertySource({"classpath:loginsecuritybyredis.properties"})
public class WebAppConfig extends WebMvcConfigurerAdapter { /**
* 内容协商
* @return
*/
@Bean
public ContentNegotiationManager mvcContentNegotiationManager() {
ContentNegotiationManagerFactoryBean contentNegotiationManagerFactoryBean = new ContentNegotiationManagerFactoryBean();
contentNegotiationManagerFactoryBean.setFavorParameter(true);
contentNegotiationManagerFactoryBean.setIgnoreAcceptHeader(true);
contentNegotiationManagerFactoryBean.setDefaultContentType(MediaType.APPLICATION_JSON_UTF8);
Properties mediaTypesProperties = new Properties();
mediaTypesProperties.setProperty("json", MediaType.APPLICATION_JSON_UTF8_VALUE);
contentNegotiationManagerFactoryBean.setMediaTypes(mediaTypesProperties);
contentNegotiationManagerFactoryBean.afterPropertiesSet();
return contentNegotiationManagerFactoryBean.getObject();
} @Bean
public ContentNegotiatingViewResolver contentNegotiatingViewResolver(@Autowired ContentNegotiationManager mvcContentNegotiationManager) {
ContentNegotiatingViewResolver contentNegotiatingViewResolver = new ContentNegotiatingViewResolver();
contentNegotiatingViewResolver.setOrder(1);
contentNegotiatingViewResolver.setContentNegotiationManager(mvcContentNegotiationManager);
return contentNegotiatingViewResolver;
} /**
* 采用UTF-8编码,防止中文乱码
* @return
*/
@Bean
public StringHttpMessageConverter stringHttpMessageConverter() {
return new StringHttpMessageConverter(Charset.forName("UTF-8"));
} @Bean
public RequestMappingHandlerAdapter requestMappingHandlerAdapter(@Autowired StringHttpMessageConverter stringHttpMessageConverter) {
RequestMappingHandlerAdapter requestMappingHandlerAdapter = new RequestMappingHandlerAdapter();
requestMappingHandlerAdapter.setMessageConverters(Collections.singletonList(stringHttpMessageConverter));
return requestMappingHandlerAdapter;
} /**
* 设置欢迎页
* 相当于web.xml中的 welcome-file-list > welcome-file
*/
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addRedirectViewController("/", "/index.html");
}
}

编写登录认证Api

这里在 init 方法中初始化几个用户,放入 USER_DATA 集合,用于后续模拟登录。然后初始化 jedis 连接信息。init 方法被 @PostConstruct 注解,因此 Spring 创建该类的对象后,将自动执行其 init 方法,进行初始化操作。

然后看 login 方法,首先根据用户名获取最近 MAX_DISABLED_SECONDS 秒内失败的次数,是否超过最大限制 MAX_TRY_COUNT

若超过最大限制,不再对用户名和密码进行认证,直接返回认证失败提示信息,也即账户已被锁定的提示信息。

否则,进行用户认证。

若认证失败,将其添加到 Redis 缓存中,并设置过期默认为 MAX_DISABLED_SECONDS,表示从此刻起,MAX_DISABLED_SECONDS 秒内,该用户已登录失败 count 次。

若Redis缓存中已存在该用户认证失败的计数信息,则刷新 count 值,并将旧值的剩余存活时间设置到新值上,然后返回认证失败提示信息。

否则,返回认证成功提示信息。

package org.xueliang.loginsecuritybyredis.web.controller.api;

import java.util.HashMap;
import java.util.Map; import javax.annotation.PostConstruct; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.xueliang.loginsecuritybyredis.web.model.JSONResponse;
import org.xueliang.loginsecuritybyredis.web.model.User; import redis.clients.jedis.Jedis; /**
* 认证类
* @author XueLiang
* @date 2016年11月1日 下午4:11:59
* @version 1.0
*/
@RestController
@RequestMapping("/api/auth/")
public class AuthApi { private static final Map<String, User> USER_DATA = new HashMap<String, User>();
@Value("${auth.max_try_count}")
private int MAX_TRY_COUNT = 0;
@Value("${auth.max_disabled_seconds}")
private int MAX_DISABLED_SECONDS = 0; @Value("${redis.host}")
private String host;
@Value("${redis.port}")
private int port;
private Jedis jedis; @PostConstruct
public void init() {
for (int i = 0; i < 3; i++) {
String username = "username" + 0;
String password = "password" + 0;
USER_DATA.put(username + "_" + password, new User(username, "nickname" + i));
}
jedis = new Jedis(host, port);
} @RequestMapping(value = {"login"}, method = RequestMethod.POST)
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
JSONResponse jsonResponse = new JSONResponse();
String key = username;
String countString = jedis.get(key);
boolean exists = countString != null;
int count = exists ? Integer.parseInt(countString) : 0;
if (count >= MAX_TRY_COUNT) {
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
User user = USER_DATA.get(username + "_" + password);
if (user == null) {
count++;
int secondsRemain = MAX_DISABLED_SECONDS;
if (exists && count < 5) {
secondsRemain = (int)(jedis.pttl(key) / 1000);
}
jedis.set(key, count + "");
jedis.expire(key, secondsRemain);
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
count = 0;
if (exists) {
jedis.del(key);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
} /**
*
* @param key
* @param count 尝试次数,也可以改为从redis里直接读
* @param jsonResponse
* @return
*/
private void checkoutMessage(String key, int count, JSONResponse jsonResponse) {
if (count == 0) {
jsonResponse.setCode(0);
jsonResponse.addMsg("success", "恭喜,登录成功!");
return;
}
jsonResponse.setCode(1);
if (count >= MAX_TRY_COUNT) {
long pttlSeconds = jedis.pttl(key) / 1000;
long hours = pttlSeconds / 3600;
long sencondsRemain = pttlSeconds - hours * 3600;
long minutes = sencondsRemain / 60;
long seconds = sencondsRemain - minutes * 60;
jsonResponse.addError("login_disabled", "登录超过" + MAX_TRY_COUNT + "次,请" + hours + "小时" + minutes + "分" + seconds + "秒后再试!");
return;
}
jsonResponse.addError("username_or_password_is_wrong", "密码错误,您还有 " + (MAX_TRY_COUNT - count) + " 次机会!");
}
}

编写前端页面

页面很简单,监听表单提交事件,用 ajax 提交表单数据,然后将认证结果显示到 div 中。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>登录</title>
<style>
span.error {
color: red;
}
span.msg {
color: green;
}
</style>
</head>
<body>
<form action="" method="post">
<label>用户名</label><input type="text" name="username">
<label>密码</label><input type="text" name="password">
<button type="submit">登录</button>
<div></div>
</form> <script>
(function($) {
var $ = (selector) => document.querySelector(selector);
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var response = JSON.parse(this.responseText);
var html = '';
var msgNode = '';
if (response.code != 0) {
msgNode = 'error';
} else {
msgNode = 'msg';
}
for (var key in response[msgNode]) {
html += '<span class="' + msgNode + '">' + response[msgNode][key] + '</span>';
}
$('div').innerHTML = html;
}
} var ajax = function(formData) {
xhr.open('POST', '/api/auth/login.json', true);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8'); // 将请求头设置为表单方式提交
xhr.send(formData);
}
$('form').addEventListener('submit', function(event) {
event.preventDefault();
var formData = '';
for (var elem of ['username', 'password']) {
var value = $('input[name="' + elem + '"]').value;
formData += (elem + '=' + value + '&');
}
ajax(formData);
});
})();
</script>
</body>
</html>

源码

最后上下源码地址:https://github.com/liangzai-cool/loginsecuritybyredis

更新

2016年11月29日 更新,代码优化,增加原子操作,org.xueliang.loginsecuritybyredis.web.controller.api.AuthApi#login 函数作如下优化:

    @RequestMapping(value = {"login"}, method = RequestMethod.POST)
public String login(@RequestParam("username") String username, @RequestParam("password") String password) {
JSONResponse jsonResponse = new JSONResponse();
String key = username;
String countString = jedis.get(key);
boolean exists = countString != null;
int count = exists ? Integer.parseInt(countString) : 0;
if (count >= MAX_TRY_COUNT) {
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
User user = USER_DATA.get(username + "_" + password);
if (user == null) {
count++;
// int secondsRemain = MAX_DISABLED_SECONDS;
// if (exists && count < 5) {
// secondsRemain = (int)(jedis.pttl(key) / 1000);
// }
// jedis.set(key, count + "");
// jedis.expire(key, secondsRemain);
if (exists) {
jedis.incr(key);
if (count >= MAX_TRY_COUNT) {
jedis.expire(key, MAX_DISABLED_SECONDS);
}
} else {
jedis.set(key, count + "");
jedis.expire(key, MAX_DISABLED_SECONDS);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}
count = 0;
if (exists) {
jedis.del(key);
}
checkoutMessage(key, count, jsonResponse);
return jsonResponse.toString();
}

原文链接http://xueliang.org/article/detail/20161102173458963

Spring RESTful + Redis全注解实现恶意登录保护机制的更多相关文章

  1. Spring 反射注入+全注解注入

    Spring IoC容器会先把所有的Bean都进行实例化,不管是要用到的火鼠用不到的,如果你想暂时不进行Bean的实例化,要用到属性lazy-init="true". Spring ...

  2. Spring集成Redis使用注解

    转载:http://blog.csdn.net/u013725455/article/details/52129283 使用Maven项目,添加jar文件依赖: <project xmlns=& ...

  3. spring mvc+redis实现微信小程序登录

    本文将详细的介绍微信小程序的登录流程以及在ssm框架下如何实现小程序用户登录 登录流程概要 主要的登录流程可以参考官方提供的一张流程图: 1.微信前台页面: 在微信版本更新之后,提高了安全机制,我们需 ...

  4. 详解spring boot mybatis全注解化

    本文重点介绍spring boot mybatis 注解化的实例代码 1.pom.xml //引入mybatis <dependency> <groupId>org.mybat ...

  5. SpringMVC 全注解实现 (1) servlet3.0以上的容器支持

    一. Spring MVC入门 1.1 request的处理过程 用户每次点击浏览器界面的一个按钮,都发出一个web请求(request).一个web请求的工作就像一个快递员,负责将信息从一个地方运送 ...

  6. 容错保护机制:Spring Cloud Hystrix

    最近在学习Spring Cloud的知识,现将容错保护机制Spring Cloud Hystrix 的相关知识笔记整理如下.[采用 oneNote格式排版]

  7. 基于IDEA 最新Spirng3.2+hibernate4+struts2.3 全注解配置 登录

    原文 基于IDEA 最新Spirng3.2+hibernate4+struts2.3 全注解配置 登录 首先说说 IDEA 12,由于myeclipse越来越卡,我改用idea12 了,发现其功能强悍 ...

  8. spring配置redis注解缓存

    前几天在spring整合Redis的时候使用了手动的方式,也就是可以手动的向redis添加缓存与清除缓存,参考:http://www.cnblogs.com/qlqwjy/p/8562703.html ...

  9. Spring MVC 3.0.5+Spring 3.0.5+MyBatis3.0.4全注解实例详解(一)

    Spring更新到3.0之后,其MVC框架加入了一个非常不错的东西——那就是REST.它的开放式特性,与Spring的无缝集成,以及Spring框架的优秀表现,使得现在很多公司将其作为新的系统开发框架 ...

随机推荐

  1. I/O流

     转自:http://www.cnblogs.com/dolphin0520/p/3791327.html 一.什么是IO Java中I/O操作主要是指使用Java进行输入,输出操作. Java所有的 ...

  2. 理解FMS中的实例

    FMS服务器端安装后,唯一需要注意的是设置端口,默认的访问端口是1935和80,如果服务器上安装了IIS提供 WEB服务,那么需要将80修改为其他端口如8080,否则,IIS将会无法工作.如果愿意,也 ...

  3. CAReplicatorLayer复制Layer和动画, 实现神奇的效果

    今天我们看下CAReplicatorLayer, 官方的解释是一个高效处理复制图层的中间层.他能复制图层的所有属性,包括动画. 一样我们先看下头文件 @interface CAReplicatorLa ...

  4. loadrunner:web services接口测试

    本文以实例讲解web services接口测试操作,内容包括:脚本生成.参数化和接口与接口间的取值关联操作. 网站"http://www.webxml.com.cn/zh_cn/web_se ...

  5. C语言,使用宏来传数字参数

    a.h #define xglue(x, y) x ## y #define glue(x, y) xglue(x, y) static int glue(load_elf, SZ)(void) { ...

  6. .net判断System.Data.DataRow中是否包含某列

    大家对将DataRow转成实体对象并不陌生,转成实体的时候一般都会加上这个判断  if (row["字段名"] != null && row["字段名&q ...

  7. 支付宝ios支付请求Java服务端签名报的一个错(ALI40247) 原创

    今天做app的支付宝支付,遇到些问题,以前做支付宝支付签名都是直接在客户端App进行,今天下了最新版本ios的支付宝支付demo,运行demo时底部有红色的显眼字体,告知用户签名必须在服务端进行... ...

  8. ArcGIS三种方式打断相交线------拓扑法

    拓扑法:有多个layer图层相交线,选用拓扑法,将多个图层相交线打断. 新建拓扑结构: (1)单击新建"Nfg.gdb"数据库文件: (2)单击新建"XX"集合 ...

  9. gRPC中Any类型的使用(Java和NodeJs端)

    工作中要把原来Java服务端基于SpringMVC的服务改为使用gRPC直接调用.由于原Service的返回值为动态的Map类型,key值不确定,且value的类型不唯一,因此使用了protobuf ...

  10. java udp 发送小数数字(较难)

    代码全部来自:http://825635381.iteye.com/blog/2046882,在这里非常感谢了,我运行测试了下,非常正确,谢谢啊 服务端程序: package udpServer; i ...