设为首页 收藏本站
查看: 1101|回复: 0

[经验分享] Spring RESTful + Redis全注解实现恶意登录保护机制

[复制链接]

尚未签到

发表于 2017-12-22 08:06:46 | 显示全部楼层 |阅读模式
  好久没更博了...
  
最近看了个真正全注解实现的 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 一样,依次添加 filter, servlet 。
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>  

  @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.xml 的 welcome-file-list - welcome-file,因为 Servlet 3.0 规范没有针对欢迎页的Java配置方案,所以目前只能在Java中这样配置,其效果类似于在XML版中配置 <mvc:redirect-view-controller path=&quot;/&quot; redirect-url=&quot;/index.html&quot;/> 。
  
最后注意这里的 @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 = &quot;org.xueliang.loginsecuritybyredis&quot;)
  
@PropertySource({&quot;classpath:loginsecuritybyredis.properties&quot;})

  
public>  /**
  * 内容协商
  * @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(&quot;json&quot;, 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(&quot;UTF-8&quot;));
  }
  @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(&quot;/&quot;, &quot;/index.html&quot;);
  }
  
}
  

编写登录认证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(&quot;/api/auth/&quot;)

  
public>  

  private static final Map<String, User> USER_DATA = new HashMap<String, User>();
  @Value(&quot;${auth.max_try_count}&quot;)
  private int MAX_TRY_COUNT = 0;
  @Value(&quot;${auth.max_disabled_seconds}&quot;)
  private int MAX_DISABLED_SECONDS = 0;
  @Value(&quot;${redis.host}&quot;)
  private String host;
  @Value(&quot;${redis.port}&quot;)
  private int port;
  private Jedis jedis;
  @PostConstruct
  public void init() {
  for (int i = 0; i < 3; i++) {
  String username = &quot;username&quot; + 0;
  String password = &quot;password&quot; + 0;
  USER_DATA.put(username + &quot;_&quot; + password, new User(username, &quot;nickname&quot; + i));
  }
  jedis = new Jedis(host, port);
  }
  @RequestMapping(value = {&quot;login&quot;}, method = RequestMethod.POST)
  public String login(@RequestParam(&quot;username&quot;) String username, @RequestParam(&quot;password&quot;) 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 + &quot;_&quot; + password);
  if (user == null) {
  count++;
  int secondsRemain = MAX_DISABLED_SECONDS;
  if (exists && count < 5) {
  secondsRemain = (int)(jedis.pttl(key) / 1000);
  }
  jedis.set(key, count + &quot;&quot;);
  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(&quot;success&quot;, &quot;恭喜,登录成功!&quot;);
  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(&quot;login_disabled&quot;, &quot;登录超过&quot; + MAX_TRY_COUNT + &quot;次,请&quot; + hours + &quot;小时&quot; + minutes + &quot;分&quot; + seconds + &quot;秒后再试!&quot;);
  return;
  }
  jsonResponse.addError(&quot;username_or_password_is_wrong&quot;, &quot;密码错误,您还有 &quot; + (MAX_TRY_COUNT - count) + &quot; 次机会!&quot;);
  }
  
}

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

<!DOCTYPE html>  
<html>
  
<head>
  
<meta charset=&quot;UTF-8&quot;>
  
<title>登录</title>
  
<style>
  span.error {
  color: red;
  }
  span.msg {
  color: green;
  }
  
</style>
  
</head>
  
<body>
  
<form action=&quot;&quot; method=&quot;post&quot;>
  <label>用户名</label><input type=&quot;text&quot; name=&quot;username&quot;>
  <label>密码</label><input type=&quot;text&quot; name=&quot;password&quot;>
  <button type=&quot;submit&quot;>登录</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>  }
  $('div').innerHTML = html;
  }
  }
  var ajax = function(formData) {
  xhr.open('POST', '/api/auth/login.json', true);
  xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; '); // 将请求头设置为表单方式提交
  xhr.send(formData);
  }
  $('form').addEventListener('submit', function(event) {
  event.preventDefault();
  var formData = '';
  for (var elem of ['username', 'password']) {
  var value = $('input[name=&quot;' + elem + '&quot;]').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 = {&quot;login&quot;}, method = RequestMethod.POST)  public String login(@RequestParam(&quot;username&quot;) String username, @RequestParam(&quot;password&quot;) 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 + &quot;_&quot; + password);
  if (user == null) {
  count++;
  
//            int secondsRemain = MAX_DISABLED_SECONDS;
  
//            if (exists && count < 5) {
  
//                secondsRemain = (int)(jedis.pttl(key) / 1000);
  
//            }
  
//            jedis.set(key, count + &quot;&quot;);
  
//            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 + &quot;&quot;);
  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

运维网声明 1、欢迎大家加入本站运维交流群:群②:261659950 群⑤:202807635 群⑦870801961 群⑧679858003
2、本站所有主题由该帖子作者发表,该帖子作者与运维网享有帖子相关版权
3、所有作品的著作权均归原作者享有,请您和我们一样尊重他人的著作权等合法权益。如果您对作品感到满意,请购买正版
4、禁止制作、复制、发布和传播具有反动、淫秽、色情、暴力、凶杀等内容的信息,一经发现立即删除。若您因此触犯法律,一切后果自负,我们对此不承担任何责任
5、所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其内容的准确性、可靠性、正当性、安全性、合法性等负责,亦不承担任何法律责任
6、所有作品仅供您个人学习、研究或欣赏,不得用于商业或者其他用途,否则,一切后果均由您自己承担,我们对此不承担任何法律责任
7、如涉及侵犯版权等问题,请您及时通知我们,我们将立即采取措施予以解决
8、联系人Email:admin@iyunv.com 网址:www.yunweiku.com

所有资源均系网友上传或者通过网络收集,我们仅提供一个展示、介绍、观摩学习的平台,我们不对其承担任何法律责任,如涉及侵犯版权等问题,请您及时通知我们,我们将立即处理,联系人Email:kefu@iyunv.com,QQ:1061981298 本贴地址:https://www.yunweiku.com/thread-426713-1-1.html 上篇帖子: Java中Jedis操作Redis与Spring的整合 下篇帖子: Redis 封装库
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

扫码加入运维网微信交流群X

扫码加入运维网微信交流群

扫描二维码加入运维网微信交流群,最新一手资源尽在官方微信交流群!快快加入我们吧...

扫描微信二维码查看详情

客服E-mail:kefu@iyunv.com 客服QQ:1061981298


QQ群⑦:运维网交流群⑦ QQ群⑧:运维网交流群⑧ k8s群:运维网kubernetes交流群


提醒:禁止发布任何违反国家法律、法规的言论与图片等内容;本站内容均来自个人观点与网络等信息,非本站认同之观点.


本站大部分资源是网友从网上搜集分享而来,其版权均归原作者及其网站所有,我们尊重他人的合法权益,如有内容侵犯您的合法权益,请及时与我们联系进行核实删除!



合作伙伴: 青云cloud

快速回复 返回顶部 返回列表