很久没更博了...
最近看了个真正全注解实现的 SpringMVC 博客,感受很不错,终于能够完全丢弃 web.xml
了。其实这玩意也是老东西了,丢弃 web.xml
,是基于 五、6年前发布的 Servlet 3.0 规范,只不过少有人玩而已...如今4.0都快正式发布了...Spring对注解的支持也从09年末就开始支持了...
基础部分我就不仔细讲了,能够先看一下这篇 以及其中提到的另外两篇文章,这三篇文章讲的很不错。
下面开始旧东西新玩~~~html
项目是基于 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
同样,依次添加 filter
, servlet
。java
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 基本上启动起来了。git
如今Spring已经能够正常启动了,但咱们还要给 Spring 作一些配置,以便让它按咱们须要的方式工做~
这里由于后端只负责提供数据,而不负责页面渲染,因此只须要配置返回 json
视图便可,我的比较偏心采用内容协商,因此这里我使用了 ContentNegotiationManagerFactoryBean
,但只配置了一个 JSON 格式的视图。
为了不中文乱码,这里设置了 StringHttpMessageConverter
默认编码格式为 UTF-8
,而后将其设置为 RequestMappingHandlerAdapter
的消息转换器。
最后还须要再配置一个欢迎页,相似于 web.xml
的 welcome-file-list - welcome-file
,由于 Servlet 3.0 规范没有针对欢迎页的Java配置方案,因此目前只能在Java中这样配置,其效果相似于在XML版中配置 <mvc:redirect-view-controller path="/" redirect-url="/index.html"/>
。
最后注意这里的 @Bean
注解,默认的 name
是方法名。github
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"); } }
这里在 init
方法中初始化几个用户,放入 USER_DATA
集合,用于后续模拟登陆。而后初始化 jedis
链接信息。init
方法被 @PostConstruct
注解,所以 Spring
建立该类的对象后,将自动执行其 init
方法,进行初始化操做。
而后看 login
方法,首先根据用户名获取最近 MAX_DISABLED_SECONDS
秒内失败的次数,是否超过最大限制 MAX_TRY_COUNT
。web
若超过最大限制,再也不对用户名和密码进行认证,直接返回认证失败提示信息,也即帐户已被锁定的提示信息。ajax
不然,进行用户认证。redis
若认证失败,将其添加到 Redis 缓存中,并设置过时默认为 MAX_DISABLED_SECONDS
,表示今后刻起,MAX_DISABLED_SECONDS
秒内,该用户已登陆失败 count
次。spring
若Redis缓存中已存在该用户认证失败的计数信息,则刷新 count
值,并将旧值的剩余存活时间设置到新值上,而后返回认证失败提示信息。json
不然,返回认证成功提示信息。
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(); }