Browse Source

非法登录处理

yingp 7 years ago
parent
commit
6db021e5c6
31 changed files with 759 additions and 32 deletions
  1. 10 0
      base-servers/account/account-api/src/main/java/com/usoftchina/saas/account/cache/AccountCache.java
  2. 10 0
      base-servers/account/account-api/src/main/java/com/usoftchina/saas/account/cache/ResourceCache.java
  3. 1 1
      base-servers/account/account-server/src/main/java/com/usoftchina/saas/account/controller/ResourceController.java
  4. 3 0
      base-servers/account/account-server/src/main/resources/application.yml
  5. 9 0
      base-servers/auth/auth-server/pom.xml
  6. 2 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/AuthApplication.java
  7. 44 1
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/controller/AuthController.java
  8. 47 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/dto/AuthorizeLogDTO.java
  9. 29 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/mapper/AuthorizeLogMapper.java
  10. 117 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/po/AuthorizeLog.java
  11. 68 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/AuthorizeCountService.java
  12. 28 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/AuthorizeLogService.java
  13. 42 0
      base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/impl/AuthorizeLogServiceImpl.java
  14. 3 0
      base-servers/auth/auth-server/src/main/resources/application.yml
  15. 21 0
      base-servers/auth/auth-server/src/main/resources/mapper/AuthorizeLogMapper.xml
  16. 48 0
      base-servers/auth/auth-server/src/test/java/com/usoftchina/saas/auth/controller/AuthControllerTest.java
  17. 2 2
      base-servers/gateway-server/src/main/java/com/usoftchina/saas/gateway/config/AuthFilter.java
  18. 3 0
      base-servers/gateway-server/src/main/resources/application.yml
  19. 3 0
      base-servers/ui-server/src/main/resources/application.yml
  20. 6 0
      framework/core/pom.xml
  21. 15 2
      framework/core/src/main/java/com/usoftchina/saas/base/Result.java
  22. 2 11
      framework/core/src/main/java/com/usoftchina/saas/cache/BaseRedisCache.java
  23. 40 0
      framework/core/src/main/java/com/usoftchina/saas/cache/CacheKeyHelper.java
  24. 5 0
      framework/core/src/main/java/com/usoftchina/saas/exception/ExceptionCode.java
  25. 23 0
      framework/core/src/main/java/com/usoftchina/saas/page/PageDefault.java
  26. 12 0
      framework/core/src/main/java/com/usoftchina/saas/page/PageRequest.java
  27. 96 0
      framework/core/src/main/java/com/usoftchina/saas/page/PageRequestArgumentResolver.java
  28. 13 13
      framework/core/src/main/java/com/usoftchina/saas/utils/JsonUtils.java
  29. 29 0
      framework/server-starter/src/main/java/com/usoftchina/saas/server/web/DefaultWebMvcConfig.java
  30. 17 2
      framework/test-starter/src/main/java/com.usoftchina.saas.test/BaseControllerTest.java
  31. 11 0
      script/mysql/init/auth.sql

+ 10 - 0
base-servers/account/account-api/src/main/java/com/usoftchina/saas/account/cache/AccountCache.java

@@ -4,6 +4,7 @@ import com.usoftchina.saas.account.api.AccountApi;
 import com.usoftchina.saas.account.dto.AccountDTO;
 import com.usoftchina.saas.base.Result;
 import com.usoftchina.saas.cache.RedisHashCache;
+import com.usoftchina.saas.context.BaseContextHolder;
 import com.usoftchina.saas.context.SpringContextHolder;
 import com.usoftchina.saas.exception.BizException;
 import com.usoftchina.saas.utils.JsonUtils;
@@ -31,6 +32,15 @@ public class AccountCache extends RedisHashCache<String, String, String> {
         return new AccountCache(id);
     }
 
+    /**
+     * 当前登录用户的缓存信息
+     *
+     * @return
+     */
+    public static AccountCache current() {
+        return new AccountCache(BaseContextHolder.getUserId());
+    }
+
     @Override
     protected String field() {
         return String.valueOf(id);

+ 10 - 0
base-servers/account/account-api/src/main/java/com/usoftchina/saas/account/cache/ResourceCache.java

@@ -4,6 +4,7 @@ import com.usoftchina.saas.account.api.ResourceApi;
 import com.usoftchina.saas.account.dto.UrlResourceDTO;
 import com.usoftchina.saas.base.Result;
 import com.usoftchina.saas.cache.RedisHashCache;
+import com.usoftchina.saas.context.BaseContextHolder;
 import com.usoftchina.saas.context.SpringContextHolder;
 import com.usoftchina.saas.exception.BizException;
 import com.usoftchina.saas.utils.JsonUtils;
@@ -33,6 +34,15 @@ public class ResourceCache extends RedisHashCache<String, String, String> {
         return new ResourceCache(appId);
     }
 
+    /**
+     * 当前登录应用的资源缓存
+     *
+     * @return
+     */
+    public static ResourceCache current() {
+        return new ResourceCache(BaseContextHolder.getAppId());
+    }
+
     @Override
     protected String field() {
         return appId;

+ 1 - 1
base-servers/account/account-server/src/main/java/com/usoftchina/saas/account/controller/ResourceController.java

@@ -61,7 +61,7 @@ public class ResourceController {
      */
     @GetMapping("/cache/clear")
     public Result clearCache() {
-        ResourceCache.of(BaseContextHolder.getAppId()).hdel();
+        ResourceCache.current().hdel();
         return Result.success();
     }
 

+ 3 - 0
base-servers/account/account-server/src/main/resources/application.yml

@@ -38,6 +38,9 @@ spring:
   redis:
     host: 192.168.253.12
     port: 6379
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    time-zone: GMT+8
 eureka:
   instance:
     leaseRenewalIntervalInSeconds: 10

+ 9 - 0
base-servers/auth/auth-server/pom.xml

@@ -38,6 +38,10 @@
             <groupId>org.mybatis.spring.boot</groupId>
             <artifactId>mybatis-spring-boot-starter</artifactId>
         </dependency>
+        <dependency>
+            <groupId>com.github.pagehelper</groupId>
+            <artifactId>pagehelper-spring-boot-starter</artifactId>
+        </dependency>
         <!-- api doc -->
         <dependency>
             <groupId>io.springfox</groupId>
@@ -61,6 +65,11 @@
             <groupId>net.logstash.logback</groupId>
             <artifactId>logstash-logback-encoder</artifactId>
         </dependency>
+        <!-- test -->
+        <dependency>
+            <groupId>com.usoftchina.saas</groupId>
+            <artifactId>test-starter</artifactId>
+        </dependency>
     </dependencies>
 
     <build>

+ 2 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/AuthApplication.java

@@ -1,5 +1,6 @@
 package com.usoftchina.saas.auth;
 
+import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
 import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
@@ -12,6 +13,7 @@ import org.springframework.cloud.openfeign.EnableFeignClients;
 @SpringBootApplication
 @EnableEurekaClient
 @EnableFeignClients(basePackages = "com.usoftchina.saas.account.api")
+@MapperScan(basePackages = "com.usoftchina.saas.auth.mapper")
 public class AuthApplication {
     public static void main(String[] args) {
         SpringApplication.run(AuthApplication.class, args);

+ 44 - 1
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/controller/AuthController.java

@@ -1,14 +1,21 @@
 package com.usoftchina.saas.auth.controller;
 
+import com.github.pagehelper.PageInfo;
 import com.usoftchina.saas.account.api.AccountApi;
 import com.usoftchina.saas.account.dto.AccountDTO;
 import com.usoftchina.saas.auth.common.jwt.JwtHelper;
 import com.usoftchina.saas.auth.common.jwt.JwtInfo;
 import com.usoftchina.saas.auth.common.jwt.TokenVO;
 import com.usoftchina.saas.auth.dto.AuthDTO;
+import com.usoftchina.saas.auth.dto.AuthorizeLogDTO;
 import com.usoftchina.saas.auth.dto.TokenDTO;
+import com.usoftchina.saas.auth.po.AuthorizeLog;
+import com.usoftchina.saas.auth.service.AuthorizeCountService;
+import com.usoftchina.saas.auth.service.AuthorizeLogService;
 import com.usoftchina.saas.base.Result;
 import com.usoftchina.saas.exception.ExceptionCode;
+import com.usoftchina.saas.page.PageDefault;
+import com.usoftchina.saas.page.PageRequest;
 import com.usoftchina.saas.utils.BeanMapper;
 import com.usoftchina.saas.utils.CollectionUtils;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -40,6 +47,15 @@ public class AuthController {
     @Value("${auth.expire:18000}")
     private int expire;
 
+    @Value("${auth.max-errors:5}")
+    private int maxErrors;
+
+    @Autowired
+    private AuthorizeLogService authorizeLogService;
+
+    @Autowired
+    private AuthorizeCountService authorizeCountService;
+
     /**
      * 登录认证获取token
      *
@@ -48,9 +64,16 @@ public class AuthController {
      * @return
      */
     @PostMapping("/authorize")
-    public Result<AuthDTO> authorize(@RequestParam String username, @RequestParam String password) {
+    public Result<AuthDTO> authorize(HttpServletRequest request, @RequestParam String username, @RequestParam String password) {
+        // 非法操作(登录失败次数过多...)导致被冻结
+        if (authorizeCountService.isFrozen(username)) {
+            return Result.error(ExceptionCode.AUTH_FROZEN);
+        }
+
         Result<AccountDTO> result = accountApi.validByUsernameAndPwd(username, password);
         if (result.isSuccess()) {
+            authorizeCountService.clear(username);
+
             AccountDTO accountDTO = result.getData();
             Long companyId = null;
             if (!CollectionUtils.isEmpty(accountDTO.getCompanies())) {
@@ -62,7 +85,16 @@ public class AuthController {
             JwtInfo info = new JwtInfo(appId, companyId, accountDTO.getId(), accountDTO.getUsername());
             TokenVO tokenVO = JwtHelper.generateToken(info, privateKeyPath, expire);
             TokenDTO tokenDTO = BeanMapper.map(tokenVO, TokenDTO.class);
+            // 登录日志
+            authorizeLogService.save(AuthorizeLog.from(request)
+                    .setAccountId(accountDTO.getId())
+                    .setAppId(appId).build());
             return Result.success(new AuthDTO(tokenDTO, accountDTO));
+        } else {
+            // 失败次数超过最大限制
+            if (authorizeCountService.increaseAndGet(username) > maxErrors) {
+                return Result.error(ExceptionCode.AUTH_MAX_ERRORS);
+            }
         }
         return Result.error(result.getCode(), result.getMessage());
     }
@@ -121,4 +153,15 @@ public class AuthController {
         }
         return false;
     }
+
+    /**
+     * 查询当前用户登录日志
+     *
+     * @param page
+     * @return
+     */
+    @GetMapping("/log")
+    public Result<PageInfo<AuthorizeLogDTO>> getLogs(@PageDefault PageRequest page) {
+        return Result.success(authorizeLogService.findByPage(page));
+    }
 }

+ 47 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/dto/AuthorizeLogDTO.java

@@ -0,0 +1,47 @@
+package com.usoftchina.saas.auth.dto;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author yingp
+ * @date 2018/11/7
+ */
+public class AuthorizeLogDTO implements Serializable{
+    private String clientIp;
+    private String userAgent;
+    private Date loginTime;
+
+    public String getClientIp() {
+        return clientIp;
+    }
+
+    public void setClientIp(String clientIp) {
+        this.clientIp = clientIp;
+    }
+
+    public String getUserAgent() {
+        return userAgent;
+    }
+
+    public void setUserAgent(String userAgent) {
+        this.userAgent = userAgent;
+    }
+
+    public Date getLoginTime() {
+        return loginTime;
+    }
+
+    public void setLoginTime(Date loginTime) {
+        this.loginTime = loginTime;
+    }
+
+    @Override
+    public String toString() {
+        return "AuthorizeLogDTO{" +
+                "clientIp='" + clientIp + '\'' +
+                ", userAgent='" + userAgent + '\'' +
+                ", loginTime=" + loginTime +
+                '}';
+    }
+}

+ 29 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/mapper/AuthorizeLogMapper.java

@@ -0,0 +1,29 @@
+package com.usoftchina.saas.auth.mapper;
+
+import com.usoftchina.saas.auth.po.AuthorizeLog;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ * @date 2018/11/6
+ */
+public interface AuthorizeLogMapper {
+    /**
+     * 保存
+     *
+     * @param authorizeLog
+     * @return
+     */
+    int insert(AuthorizeLog authorizeLog);
+
+    /**
+     * 查找个人日志
+     *
+     * @param appId
+     * @param accountId
+     * @return
+     */
+    List<AuthorizeLog> selectByAppIdAndAccountId(@Param("appId") String appId, @Param("accountId") Long accountId);
+}

+ 117 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/po/AuthorizeLog.java

@@ -0,0 +1,117 @@
+package com.usoftchina.saas.auth.po;
+
+import javax.servlet.http.HttpServletRequest;
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * @author yingp
+ * @date 2018/11/6
+ */
+public class AuthorizeLog implements Serializable{
+    private Long id;
+    private Long accountId;
+    private String clientIp;
+    private String userAgent;
+    private Date loginTime;
+    private String appId;
+
+    public AuthorizeLog() {
+    }
+
+    public AuthorizeLog(Long accountId, String clientIp, String userAgent, String appId) {
+        this.accountId = accountId;
+        this.clientIp = clientIp;
+        this.userAgent = userAgent;
+        this.loginTime = new Date();
+        this.appId = appId;
+    }
+
+    public Long getId() {
+        return id;
+    }
+
+    public void setId(Long id) {
+        this.id = id;
+    }
+
+    public Long getAccountId() {
+        return accountId;
+    }
+
+    public void setAccountId(Long accountId) {
+        this.accountId = accountId;
+    }
+
+    public String getClientIp() {
+        return clientIp;
+    }
+
+    public void setClientIp(String clientIp) {
+        this.clientIp = clientIp;
+    }
+
+    public String getUserAgent() {
+        return userAgent;
+    }
+
+    public void setUserAgent(String userAgent) {
+        this.userAgent = userAgent;
+    }
+
+    public Date getLoginTime() {
+        return loginTime;
+    }
+
+    public void setLoginTime(Date loginTime) {
+        this.loginTime = loginTime;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+
+    public static Builder from(HttpServletRequest request) {
+        return new Builder(request);
+    }
+
+    public static class Builder {
+        private Long accountId;
+        private String clientIp;
+        private String userAgent;
+        private String appId;
+
+        public Builder(HttpServletRequest request) {
+            this.clientIp = request.getRemoteAddr();
+            this.userAgent = request.getHeader("User-Agent");
+        }
+
+        public Builder setAccountId(Long accountId) {
+            this.accountId = accountId;
+            return this;
+        }
+
+        public Builder setClientIp(String clientIp) {
+            this.clientIp = clientIp;
+            return this;
+        }
+
+        public Builder setUserAgent(String userAgent) {
+            this.userAgent = userAgent;
+            return this;
+        }
+
+        public Builder setAppId(String appId) {
+            this.appId = appId;
+            return this;
+        }
+
+        public AuthorizeLog build() {
+            return new AuthorizeLog(accountId, clientIp, userAgent, appId);
+        }
+    }
+}

+ 68 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/AuthorizeCountService.java

@@ -0,0 +1,68 @@
+package com.usoftchina.saas.auth.service;
+
+import com.usoftchina.saas.cache.CacheKeyHelper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Service;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 登录失败记录
+ *
+ * @author yingp
+ * @date 2018/11/6
+ */
+@Service
+public class AuthorizeCountService {
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;
+
+    @Value("${auth.max-errors:5}")
+    private int maxErrors;
+
+    /**
+     * 账户锁定时间
+     */
+    @Value("${auth.error-lock-time:30}")
+    private int lockTime;
+
+    /**
+     * 记录一次
+     *
+     * @param username
+     * @return
+     */
+    public Long increaseAndGet(String username) {
+        String key = generateKey(username);
+        Long value = redisTemplate.opsForValue().increment(key, 1);
+        redisTemplate.expire(key, lockTime, TimeUnit.MINUTES);
+        return value;
+    }
+
+    private String generateKey(String username) {
+        return CacheKeyHelper.generatePrivateKey("auth", "authorize", username);
+    }
+
+    /**
+     * 清零
+     *
+     * @param username
+     */
+    public void clear(String username) {
+        redisTemplate.delete(generateKey(username));
+    }
+
+    /**
+     * 账户是否冻结
+     *
+     * @param username
+     * @return
+     */
+    public boolean isFrozen(String username) {
+        Object value = redisTemplate.opsForValue().get(generateKey(username));
+        return null != value && Integer.parseInt(value.toString()) > maxErrors;
+    }
+}

+ 28 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/AuthorizeLogService.java

@@ -0,0 +1,28 @@
+package com.usoftchina.saas.auth.service;
+
+import com.github.pagehelper.PageInfo;
+import com.usoftchina.saas.auth.dto.AuthorizeLogDTO;
+import com.usoftchina.saas.auth.po.AuthorizeLog;
+import com.usoftchina.saas.page.PageRequest;
+
+/**
+ * @author yingp
+ * @date 2018/11/6
+ */
+public interface AuthorizeLogService {
+    /**
+     * 保存
+     *
+     * @param authorizeLog
+     * @return
+     */
+    boolean save(AuthorizeLog authorizeLog);
+
+    /**
+     * 分页查询
+     *
+     * @param page
+     * @return
+     */
+    PageInfo<AuthorizeLogDTO> findByPage(PageRequest page);
+}

+ 42 - 0
base-servers/auth/auth-server/src/main/java/com/usoftchina/saas/auth/service/impl/AuthorizeLogServiceImpl.java

@@ -0,0 +1,42 @@
+package com.usoftchina.saas.auth.service.impl;
+
+import com.github.pagehelper.PageHelper;
+import com.github.pagehelper.PageInfo;
+import com.usoftchina.saas.auth.dto.AuthorizeLogDTO;
+import com.usoftchina.saas.auth.mapper.AuthorizeLogMapper;
+import com.usoftchina.saas.auth.po.AuthorizeLog;
+import com.usoftchina.saas.auth.service.AuthorizeLogService;
+import com.usoftchina.saas.context.BaseContextHolder;
+import com.usoftchina.saas.page.PageRequest;
+import com.usoftchina.saas.utils.BeanMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ * @date 2018/11/6
+ */
+@Service
+public class AuthorizeLogServiceImpl implements AuthorizeLogService{
+
+    @Autowired
+    private AuthorizeLogMapper authorizeLogMapper;
+
+    @Override
+    @Async
+    public boolean save(AuthorizeLog authorizeLog) {
+        return authorizeLogMapper.insert(authorizeLog) > 0;
+    }
+
+    @Override
+    public PageInfo<AuthorizeLogDTO> findByPage(PageRequest page) {
+        PageHelper.startPage(page.getNumber(), page.getSize());
+        List<AuthorizeLog> logs = authorizeLogMapper.selectByAppIdAndAccountId(
+                BaseContextHolder.getAppId(), BaseContextHolder.getCompanyId()
+        );
+        return new PageInfo<>(BeanMapper.mapList(logs, AuthorizeLogDTO.class));
+    }
+}

+ 3 - 0
base-servers/auth/auth-server/src/main/resources/application.yml

@@ -38,6 +38,9 @@ spring:
   redis:
     host: 192.168.253.12
     port: 6379
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    time-zone: GMT+8
 eureka:
   instance:
     leaseRenewalIntervalInSeconds: 10

+ 21 - 0
base-servers/auth/auth-server/src/main/resources/mapper/AuthorizeLogMapper.xml

@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
+<mapper namespace="com.usoftchina.saas.auth.mapper.AuthorizeLogMapper">
+    <insert id="insert">
+        insert into au_authorize_log(app_id,account_id,client_ip,user_agent,login_time)
+        values (#{appId,jdbcType=VARCHAR}, #{accountId,jdbcType=BIGINT}, #{clientIp,jdbcType=VARCHAR},
+        #{userAgent,jdbcType=VARCHAR}, #{loginTime,jdbcType=TIMESTAMP})
+    </insert>
+    <resultMap id="BaseResultMap" type="com.usoftchina.saas.auth.po.AuthorizeLog">
+        <id column="id" property="id" jdbcType="BIGINT" />
+        <result column="account_id" property="accountId" jdbcType="BIGINT" />
+        <result column="client_ip" property="clientIp" jdbcType="VARCHAR" />
+        <result column="user_agent" property="userAgent" jdbcType="VARCHAR" />
+        <result column="login_time" property="loginTime" jdbcType="TIMESTAMP" />
+        <result column="app_id" property="appId" jdbcType="VARCHAR" />
+    </resultMap>
+    <select id="selectByAppIdAndAccountId" resultMap="BaseResultMap">
+        select * from au_authorize_log where app_id=#{appId} and account_id=#{accountId}
+        order by login_time desc
+    </select>
+</mapper>

+ 48 - 0
base-servers/auth/auth-server/src/test/java/com/usoftchina/saas/auth/controller/AuthControllerTest.java

@@ -0,0 +1,48 @@
+package com.usoftchina.saas.auth.controller;
+
+import com.github.pagehelper.PageInfo;
+import com.usoftchina.saas.auth.dto.AuthDTO;
+import com.usoftchina.saas.auth.dto.AuthorizeLogDTO;
+import com.usoftchina.saas.base.Result;
+import com.usoftchina.saas.test.BaseControllerTest;
+import com.usoftchina.saas.utils.JsonUtils;
+import org.junit.FixMethodOrder;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.MethodSorters;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+import org.springframework.test.web.servlet.MvcResult;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+public class AuthControllerTest extends BaseControllerTest {
+
+    private static String TOKEN;
+
+    @Test
+    public void testA_authorize() throws Exception {
+        MvcResult mvcResult = mockMvc.perform(post("/authorize")
+                .param("username", "18888888888")
+                .param("password", "select111***"))
+                .andExpect(isSuccess())
+                .andReturn();
+        Result<AuthDTO> result = result(mvcResult, AuthDTO.class);
+        System.out.println(result.getData());
+        TOKEN = result.getData().getToken().getToken();
+    }
+
+    @Test
+    public void testB_getLogs() throws Exception {
+        MvcResult mvcResult = mockMvc.perform(get("/log")
+                .param("size", "5")
+                .header("Authorization", TOKEN))
+                .andExpect(isSuccess())
+                .andReturn();
+        Result<PageInfo<AuthorizeLogDTO>> result = result(mvcResult,
+                JsonUtils.getJavaType(PageInfo.class, AuthorizeLogDTO.class));
+        System.out.println(result.getData());
+    }
+
+}

+ 2 - 2
base-servers/gateway-server/src/main/java/com/usoftchina/saas/gateway/config/AuthFilter.java

@@ -50,7 +50,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
                 BaseContextHolder.setUserId(jwt.getUserId());
                 BaseContextHolder.setCompanyId(jwt.getCompanyId());
                 BaseContextHolder.setToken(token);
-                AccountDTO accountDTO = AccountCache.of(jwt.getUserId()).getAccount();
+                AccountDTO accountDTO = AccountCache.current().getAccount();
                 if (null == accountDTO) {
                     throw new BizException(ExceptionCode.USER_NOT_EXIST);
                 }
@@ -73,7 +73,7 @@ public class AuthFilter implements GlobalFilter, Ordered {
     private void checkPermission(ServerHttpRequest request, JwtInfo jwt, AccountDTO accountDTO) {
         if (!accountDTO.isAdmin(jwt.getCompanyId())) {
             // 非管理账户,需要鉴权
-            List<UrlResourceDTO> resources = ResourceCache.of(jwt.getAppId()).getUrlResources();
+            List<UrlResourceDTO> resources = ResourceCache.current().getUrlResources();
             if (!CollectionUtils.isEmpty(resources)) {
                 // 本次请求相关的资源
                 List<UrlResourceDTO> permissions = resources.parallelStream().filter(resource -> {

+ 3 - 0
base-servers/gateway-server/src/main/resources/application.yml

@@ -107,6 +107,9 @@ spring:
   redis:
     host: 192.168.253.12
     port: 6379
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    time-zone: GMT+8
 server:
   port: 8560
   tomcat:

+ 3 - 0
base-servers/ui-server/src/main/resources/application.yml

@@ -25,6 +25,9 @@ spring:
   data:
     mongodb:
       uri: mongodb://192.168.253.12:27017/saas_ui
+  jackson:
+    date-format: yyyy-MM-dd HH:mm:ss
+    time-zone: GMT+8
 server:
   port: 8620
   tomcat:

+ 6 - 0
framework/core/pom.xml

@@ -34,6 +34,12 @@
             <scope>provided</scope>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>org.springframework</groupId>
+            <artifactId>spring-web</artifactId>
+            <scope>provided</scope>
+            <optional>true</optional>
+        </dependency>
         <dependency>
             <groupId>org.apache.commons</groupId>
             <artifactId>commons-lang3</artifactId>

+ 15 - 2
framework/core/src/main/java/com/usoftchina/saas/base/Result.java

@@ -1,5 +1,6 @@
 package com.usoftchina.saas.base;
 
+import com.fasterxml.jackson.databind.JavaType;
 import com.usoftchina.saas.exception.BaseException;
 import com.usoftchina.saas.exception.BaseExceptionCode;
 import com.usoftchina.saas.utils.JsonUtils;
@@ -138,7 +139,19 @@ public class Result<T> implements Serializable {
      * @param <T>
      * @return
      */
-    public static <T> Result<T> fromJsonString(String jsonString, Class<T> targetCls) {
-        return JsonUtils.fromJsonString(jsonString, Result.class, targetCls);
+    public static <T> Result<T> fromJsonString(String jsonString, Class<T> targetClass) {
+        return JsonUtils.fromJsonString(jsonString, Result.class, targetClass);
+    }
+
+    /**
+     * json字符串转换Result对象
+     *
+     * @param jsonString
+     * @param targetType
+     * @param <T>
+     * @return
+     */
+    public static <T> Result<T> fromJsonString(String jsonString, JavaType targetType) {
+        return JsonUtils.fromJsonString(jsonString, JsonUtils.getJavaType(Result.class, targetType));
     }
 }

+ 2 - 11
framework/core/src/main/java/com/usoftchina/saas/cache/BaseRedisCache.java

@@ -1,11 +1,9 @@
 package com.usoftchina.saas.cache;
 
-import com.usoftchina.saas.context.SpringContextHolder;
 import org.springframework.data.redis.core.RedisTemplate;
 
 import java.util.Objects;
 import java.util.Optional;
-import java.util.StringJoiner;
 import java.util.concurrent.TimeUnit;
 import java.util.function.Supplier;
 
@@ -108,11 +106,7 @@ public abstract class BaseRedisCache<K, V> implements Cache<V> {
      * @return
      */
     protected String generatePrivateKey(CharSequence... values) {
-        return new StringJoiner(":")
-                .add(SpringContextHolder.getActiveProfile())
-                .add(SpringContextHolder.getApplicationName())
-                .add(String.join(":", values))
-                .toString();
+        return CacheKeyHelper.generatePrivateKey(values);
     }
 
     /**
@@ -123,9 +117,6 @@ public abstract class BaseRedisCache<K, V> implements Cache<V> {
      * @return
      */
     protected String generatePublicKey(CharSequence... values) {
-        return new StringJoiner(":")
-                .add(SpringContextHolder.getActiveProfile())
-                .add(String.join(":", values))
-                .toString();
+        return CacheKeyHelper.generatePublicKey(values);
     }
 }

+ 40 - 0
framework/core/src/main/java/com/usoftchina/saas/cache/CacheKeyHelper.java

@@ -0,0 +1,40 @@
+package com.usoftchina.saas.cache;
+
+import com.usoftchina.saas.context.SpringContextHolder;
+
+import java.util.StringJoiner;
+
+/**
+ * @author yingp
+ * @date 2018/11/6
+ */
+public class CacheKeyHelper {
+    /**
+     * 产生key (每个应用会不一样)
+     * 规则:[env profile]:[application name]:[business key]
+     *
+     * @param values
+     * @return
+     */
+    public static String generatePrivateKey(CharSequence... values) {
+        return new StringJoiner(":")
+                .add(SpringContextHolder.getActiveProfile())
+                .add(SpringContextHolder.getApplicationName())
+                .add(String.join(":", values))
+                .toString();
+    }
+
+    /**
+     * 产生key (所有应用一样)
+     * 规则:[env profile]:[business key]
+     *
+     * @param values
+     * @return
+     */
+    public static String generatePublicKey(CharSequence... values) {
+        return new StringJoiner(":")
+                .add(SpringContextHolder.getActiveProfile())
+                .add(String.join(":", values))
+                .toString();
+    }
+}

+ 5 - 0
framework/core/src/main/java/com/usoftchina/saas/exception/ExceptionCode.java

@@ -9,6 +9,7 @@ public enum ExceptionCode implements BaseExceptionCode {
     SYSTEM_BUSY(-1, "系统繁忙,请稍候再试"),
     SYSTEM_TIMEOUT(-2, "系统超时,请稍候再试"),
 
+    INVALID_DEFAULT_PAGE(10001, "无效默认分页参数"),
     // jwt token 相关 start
     // 过期
     JWT_TOKEN_EXPIRED(40001, "token超时,请检查 token 的有效期"),
@@ -21,6 +22,10 @@ public enum ExceptionCode implements BaseExceptionCode {
     JWT_APPID_SECRET_INVALID(40006, "获取 access_token 时 AppSecret 错误,或者 AppId 无效!"),
     JWT_APPID_ENABLED(40007, "AppId 已经被禁用!请联系管理员"),
 
+    // authorize相关
+    AUTH_MAX_ERRORS(43001, "超过登录次数限制,账户已被冻结,请30分钟后再尝试"),
+    AUTH_FROZEN(43002, "账户已被冻结,请30分钟后再尝试"),
+
     // 账户管理相关
     COMPANY_NAME_EXIST(52000, "公司名称已注册"),
     COMPANY_CODE_EXIST(52001, "公司商业登记证号已注册"),

+ 23 - 0
framework/core/src/main/java/com/usoftchina/saas/page/PageDefault.java

@@ -0,0 +1,23 @@
+package com.usoftchina.saas.page;
+
+import java.lang.annotation.*;
+
+/**
+ * 默认分页参数
+ *
+ * @author yingp
+ * @date 2018/11/7
+ */
+@Documented
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface PageDefault {
+    /**
+     * 页码
+     */
+    int number() default 1;
+    /**
+     * 每页条数
+     */
+    int size() default 20;
+}

+ 12 - 0
framework/core/src/main/java/com/usoftchina/saas/page/PageRequest.java

@@ -16,6 +16,18 @@ public class PageRequest implements Serializable{
      */
     private int size;
 
+    public PageRequest() {
+    }
+
+    public PageRequest(int number, int size) {
+        this.number = number;
+        this.size = size;
+    }
+
+    public static PageRequest of(int number, int size) {
+        return new PageRequest(number, size);
+    }
+
     public int getNumber() {
         return number;
     }

+ 96 - 0
framework/core/src/main/java/com/usoftchina/saas/page/PageRequestArgumentResolver.java

@@ -0,0 +1,96 @@
+package com.usoftchina.saas.page;
+
+import com.usoftchina.saas.exception.BizException;
+import com.usoftchina.saas.exception.ExceptionCode;
+import org.springframework.core.MethodParameter;
+import org.springframework.lang.Nullable;
+import org.springframework.util.StringUtils;
+import org.springframework.web.bind.support.WebDataBinderFactory;
+import org.springframework.web.context.request.NativeWebRequest;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.method.support.ModelAndViewContainer;
+
+import java.util.Optional;
+
+/**
+ * 分页参数处理器,设置分页默认参数
+ *
+ * @author yingp
+ * @date 2018/11/7
+ */
+public class PageRequestArgumentResolver implements HandlerMethodArgumentResolver {
+
+    static final PageRequest DEFAULT_PAGE_REQUEST = PageRequest.of(1, 20);
+
+    private PageRequest fallbackPageable = DEFAULT_PAGE_REQUEST;
+
+    private static final int DEFAULT_MAX_PAGE_SIZE = 2000;
+
+    private int maxPageSize = DEFAULT_MAX_PAGE_SIZE;
+
+    @Nullable
+    @Override
+    public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer, NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
+        PageRequest defaultOrFallback = getDefaultFromAnnotationOrFallback(parameter);
+
+        Optional<Integer> pageNumber = parseAndApplyBoundaries(webRequest.getParameter("number"),
+                Integer.MAX_VALUE);
+        Optional<Integer> pageSize = parseAndApplyBoundaries(webRequest.getParameter("size"),
+                maxPageSize);
+
+        int pn = pageNumber.orElseGet(defaultOrFallback::getNumber);
+        int ps = pageSize.orElseGet(defaultOrFallback::getSize);
+
+        return PageRequest.of(pn, ps);
+    }
+
+    @Override
+    public boolean supportsParameter(MethodParameter parameter) {
+        return parameter.hasParameterAnnotation(PageDefault.class);
+    }
+
+    private PageRequest getDefaultFromAnnotationOrFallback(MethodParameter parameter) {
+        PageDefault pageDefault = parameter.getParameterAnnotation(PageDefault.class);
+        if (pageDefault != null) {
+            return getDefaultPageRequestFrom(parameter, pageDefault);
+        }
+        return fallbackPageable;
+    }
+
+    private static PageRequest getDefaultPageRequestFrom(MethodParameter parameter, PageDefault pageDefault) {
+        Integer defaultPageNumber = pageDefault.number();
+        Integer defaultPageSize = pageDefault.size();
+
+        if (defaultPageNumber < 1 || defaultPageSize < 1) {
+            throw new BizException(ExceptionCode.INVALID_DEFAULT_PAGE);
+        }
+
+        return new PageRequest(defaultPageNumber, defaultPageSize);
+    }
+
+    /**
+     * Tries to parse the given {@link String} into an integer and applies the given boundaries. Will return 0 if the
+     * {@link String} cannot be parsed.
+     *
+     * @param parameter the parameter value.
+     * @param upper the upper bound to be applied.
+     * @return
+     */
+    private Optional<Integer> parseAndApplyBoundaries(@Nullable String parameter, int upper) {
+
+        if (!StringUtils.hasText(parameter)) {
+            return Optional.empty();
+        }
+
+        try {
+            int parsed = Integer.parseInt(parameter);
+            return Optional.of(parsed < 0 ? 0 : parsed > upper ? upper : parsed);
+        } catch (NumberFormatException e) {
+            return Optional.of(0);
+        }
+    }
+
+    public void setMaxPageSize(int maxPageSize) {
+        this.maxPageSize = maxPageSize;
+    }
+}

+ 13 - 13
framework/core/src/main/java/com/usoftchina/saas/utils/JsonUtils.java

@@ -4,7 +4,9 @@ import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.JavaType;
 import com.fasterxml.jackson.databind.ObjectMapper;
 
+import java.text.SimpleDateFormat;
 import java.util.List;
+import java.util.TimeZone;
 
 /**
  * @author yingp
@@ -12,7 +14,13 @@ import java.util.List;
  */
 public class JsonUtils {
 
-    private static ObjectMapper mapper = new ObjectMapper();
+    private static ObjectMapper mapper;
+
+    static {
+        mapper = new ObjectMapper()
+                .setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"))
+                .setTimeZone(TimeZone.getTimeZone("GMT+8"));
+    }
 
     public static String toJsonString(Object object) {
         try {
@@ -33,22 +41,14 @@ public class JsonUtils {
         }
     }
 
-    public static <T> T fromJsonString(String json, TypeReference valueTypeRef) {
-        if (StringUtils.isEmpty(json)) {
-            return null;
-        }
-        try {
-            return mapper.readValue(json, valueTypeRef);
-        } catch (Exception e) {
-            e.printStackTrace();
-            return null;
-        }
-    }
-
     public static JavaType getJavaType(Class<?> targetClass, Class<?>... elementClasses) {
         return mapper.getTypeFactory().constructParametricType(targetClass, elementClasses);
     }
 
+    public static JavaType getJavaType(Class<?> targetClass, JavaType... parameterTypes) {
+        return mapper.getTypeFactory().constructParametricType(targetClass, parameterTypes);
+    }
+
     public static <T> T fromJsonString(String json, Class<?> targetClass, Class<?>... elementClasses) {
         if (StringUtils.isEmpty(json)) {
             return null;

+ 29 - 0
framework/server-starter/src/main/java/com/usoftchina/saas/server/web/DefaultWebMvcConfig.java

@@ -0,0 +1,29 @@
+package com.usoftchina.saas.server.web;
+
+import com.usoftchina.saas.page.PageRequestArgumentResolver;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.method.support.HandlerMethodArgumentResolver;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+import java.util.List;
+
+/**
+ * @author yingp
+ * @date 2018/11/7
+ */
+@Configuration
+public class DefaultWebMvcConfig implements WebMvcConfigurer{
+
+    @Bean
+    @ConditionalOnMissingBean
+    public PageRequestArgumentResolver pageRequestArgumentResolver() {
+        return new PageRequestArgumentResolver();
+    }
+
+    @Override
+    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
+        resolvers.add(pageRequestArgumentResolver());
+    }
+}

+ 17 - 2
framework/test-starter/src/main/java/com.usoftchina.saas.test/BaseControllerTest.java

@@ -1,5 +1,6 @@
 package com.usoftchina.saas.test;
 
+import com.fasterxml.jackson.databind.JavaType;
 import com.usoftchina.saas.base.Result;
 import com.usoftchina.saas.utils.JsonUtils;
 import org.junit.Before;
@@ -110,9 +111,23 @@ public abstract class BaseControllerTest {
      * @return
      * @throws Exception
      */
-    public static <T> Result<T> result(MvcResult mvcResult, Class<T> targetCls) throws Exception {
+    public static <T> Result<T> result(MvcResult mvcResult, Class<T> targetClass) throws Exception {
         String content = mvcResult.getResponse().getContentAsString();
-        return Result.fromJsonString(content, targetCls);
+        return Result.fromJsonString(content, targetClass);
+    }
+
+    /**
+     * Result转换
+     *
+     * @param mvcResult
+     * @param targetType
+     * @param <T>
+     * @return
+     * @throws Exception
+     */
+    public static <T> Result<T> result(MvcResult mvcResult, JavaType targetType) throws Exception {
+        String content = mvcResult.getResponse().getContentAsString();
+        return Result.fromJsonString(content, targetType);
     }
 
 }

+ 11 - 0
script/mysql/init/auth.sql

@@ -0,0 +1,11 @@
+CREATE DATABASE `saas_auth` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci;
+use `saas_auth`;
+
+create table `au_authorize_log` (
+  id int unsigned primary key not null auto_increment,
+  app_id varchar(30) comment '应用',
+  account_id int unsigned not null comment '账号',
+  client_ip varchar(100) comment '客户端IP',
+  user_agent varchar(300) comment 'User Agent',
+  login_time datetime comment '登录时间'
+) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='登录日志';