很久沒更博了...
最近看了個真正全註解實現的 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(); }