资讯专栏INFORMATION COLUMN

Spring RESTful + Redis全注解实现恶意登录保护机制

wushuiyong / 1685人阅读

摘要:为了避免中文乱码,这里设置了默认编码格式为,然后将其设置为的消息转换器。内容协商采用编码,防止中文乱码设置欢迎页相当于中的编写登录认证这里在方法中初始化几个用户,放入集合,用于后续模拟登录。

好久没更博了...
最近看了个真正全注解实现的 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版中配置
最后注意这里的 @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 USER_DATA = new HashMap();
  @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 中。





登录



源码

最后上下源码地址: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

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/36260.html

相关文章

  • java面试

    摘要:面向切面编程的目标就是分离关注点。不会出现数据不一致或者数据污染。线程不安全就是不提供数据访问保护,有可能出现多个线程先后更改数据造成所得到的数据是脏数据和区别是的轻量级实现非线程安全的实现 spingmvc 和 structs的区别 我们用struts2时采用的传统的配置文件的方式,并没有使用传说中的0配置。 spring3 mvc可以认为已经100%零配置了(除了配置spring ...

    BlackMass 评论0 收藏0
  • Spring Cloud 上手实战-架构解析及实作

    摘要:服务器将要监听的端口不要使用服务进行注册不要在本地缓存注册表信息使用一个新的注解,就可以让我们的服务成为一个服务服务发现客户端配置以为例需要做件事情成为服务发现的客户端配置对应来说我们只需要配置如下启动运行查看。 Spring简介 为什么要使用微服务 单体应用: 目前为止绝大部分的web应用软件采用单体应用,所有的应用的用户UI、业务逻辑、数据库访问都打包在一个应用程序上。 showI...

    HitenDev 评论0 收藏0
  • Spring Cloud 上手实战-架构解析及实作

    摘要:服务器将要监听的端口不要使用服务进行注册不要在本地缓存注册表信息使用一个新的注解,就可以让我们的服务成为一个服务服务发现客户端配置以为例需要做件事情成为服务发现的客户端配置对应来说我们只需要配置如下启动运行查看。 Spring简介 为什么要使用微服务 单体应用: 目前为止绝大部分的web应用软件采用单体应用,所有的应用的用户UI、业务逻辑、数据库访问都打包在一个应用程序上。 showI...

    warmcheng 评论0 收藏0
  • Spring Cloud 上手实战-架构解析及实作

    摘要:服务器将要监听的端口不要使用服务进行注册不要在本地缓存注册表信息使用一个新的注解,就可以让我们的服务成为一个服务服务发现客户端配置以为例需要做件事情成为服务发现的客户端配置对应来说我们只需要配置如下启动运行查看。 Spring简介 为什么要使用微服务 单体应用: 目前为止绝大部分的web应用软件采用单体应用,所有的应用的用户UI、业务逻辑、数据库访问都打包在一个应用程序上。 showI...

    Godtoy 评论0 收藏0
  • Spring 指南(目录)

    摘要:指南无论你正在构建什么,这些指南都旨在让你尽快提高工作效率使用团队推荐的最新项目版本和技术。使用进行消息传递了解如何将用作消息代理。安全架构的主题指南,这些位如何组合以及它们如何与交互。使用的主题指南以及如何为应用程序创建容器镜像。 Spring 指南 无论你正在构建什么,这些指南都旨在让你尽快提高工作效率 — 使用Spring团队推荐的最新Spring项目版本和技术。 入门指南 这些...

    only_do 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<