Prechádzať zdrojové kódy

Merge branch '20211018_yw_check' into test_qiyuan

songxinlu 3 rokov pred
rodič
commit
193955ffe1

+ 24 - 0
doc/036.20211122_2.1.3/qc_initv2.1.3.sql

@@ -0,0 +1,24 @@
+use `qc`;
+
+/**
+执行脚本前请先看注意事项:
+  
+ */
+/**
+sys_user表新增locked字段
+ */
+ALTER TABLE `sys_user` ADD COLUMN `locked` char(1) NOT NULL DEFAULT '1' COMMENT '锁定状态0禁用;1启用' AFTER `type`;
+
+/**
+sys_dictionary_info表新增token、用户锁定、解锁时间配置
+ */
+INSERT INTO `sys_dictionary_info` (`group_type`, `name`, `val`, `return_type`, `remark`) VALUES ('31', 'accessToken', '86400', '2', 'accessToken有效期(单位秒)');
+INSERT INTO `sys_dictionary_info` (`group_type`, `name`, `val`, `return_type`, `remark`) VALUES ('31', 'refreshToken', '604800', '2', 'refreshToken有效期(单位秒)');
+INSERT INTO `sys_dictionary_info` (`group_type`, `name`, `val`, `return_type`, `remark`) VALUES ('31', 'unlockTime', '600', '2', '自动解锁时间');
+INSERT INTO `sys_dictionary_info` (`group_type`, `name`, `val`, `return_type`, `remark`) VALUES ('31', 'lockTime', '60', '2', '锁定时间');
+INSERT INTO `sys_dictionary_info` (`group_type`, `name`, `val`, `return_type`, `remark`) VALUES ('31', 'lockNum', '5', '2', '连续密码输入错误次数');
+
+/**
+sys_hospital_set表新增是否开启用户锁定配置
+ */
+INSERT INTO `sys_hospital_set` (`hospital_id`, `name`, `code`, `value`, `remark`) VALUES ('14', '密码错误锁定机制', 'lock_user', 'true', '密码错误锁定机制');

+ 25 - 0
src/main/java/com/diagbot/config/CustomExceptionTranslator.java

@@ -0,0 +1,25 @@
+package com.diagbot.config;
+
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
+import org.springframework.security.oauth2.provider.error.DefaultWebResponseExceptionTranslator;
+
+/**
+ * @Description:
+ * @Author songxl
+ * @Date 2021/11/22
+ */
+public class CustomExceptionTranslator extends DefaultWebResponseExceptionTranslator {
+
+    @Override
+    public ResponseEntity<OAuth2Exception> translate(Exception e) throws Exception {
+        ResponseEntity<OAuth2Exception> translate = super.translate(e);
+        OAuth2Exception body = translate.getBody();
+        CustomOauthException customOauthException = new CustomOauthException(body.getMessage(),body.getOAuth2ErrorCode(),
+                body.getHttpErrorCode());
+        ResponseEntity<OAuth2Exception> response = new ResponseEntity<>(customOauthException, translate.getHeaders(),
+                translate.getStatusCode());
+        return response;
+    }
+
+}

+ 31 - 0
src/main/java/com/diagbot/config/CustomOauthException.java

@@ -0,0 +1,31 @@
+package com.diagbot.config;
+
+import com.fasterxml.jackson.databind.annotation.JsonSerialize;
+import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
+
+/**
+ * @Description:
+ * @Author songxl
+ * @Date 2021/11/22
+ */
+@JsonSerialize(using = CustomOauthExceptionSerializer.class)
+public class CustomOauthException extends OAuth2Exception {
+
+    private String oAuth2ErrorCode;
+
+    private int httpErrorCode;
+
+    public CustomOauthException(String msg, String oAuth2ErrorCode, int httpErrorCode) {
+        super(msg);
+        this.oAuth2ErrorCode = oAuth2ErrorCode;
+        this.httpErrorCode = httpErrorCode;
+    }
+
+    public String getoAuth2ErrorCode() {
+        return oAuth2ErrorCode;
+    }
+
+    public int getHttpErrorCode() {
+        return httpErrorCode;
+    }
+}

+ 28 - 0
src/main/java/com/diagbot/config/CustomOauthExceptionSerializer.java

@@ -0,0 +1,28 @@
+package com.diagbot.config;
+
+import com.fasterxml.jackson.core.JsonGenerator;
+import com.fasterxml.jackson.databind.SerializerProvider;
+import com.fasterxml.jackson.databind.ser.std.StdSerializer;
+
+import java.io.IOException;
+
+/**
+ * @Description:
+ * @Author songxl
+ * @Date 2021/11/22
+ */
+public class CustomOauthExceptionSerializer extends StdSerializer<CustomOauthException> {
+
+    public CustomOauthExceptionSerializer() {
+        super(CustomOauthException.class);
+    }
+
+    @Override
+    public void serialize(CustomOauthException value, JsonGenerator gen, SerializerProvider provider) throws IOException {
+        //value内容适当的做一些错误类型判断
+        gen.writeStartObject();
+        gen.writeObjectField("code","10020011");
+        gen.writeObjectField("msg","登录超时。为确保您的账户安全,系统已自动退出,请重新登录。");
+        gen.writeEndObject();
+    }
+}

+ 15 - 2
src/main/java/com/diagbot/config/OAuth2Configurer.java

@@ -1,5 +1,6 @@
 package com.diagbot.config;
 
+import com.diagbot.facade.SysDictionaryFacade;
 import com.diagbot.service.UrlUserService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
@@ -33,17 +34,29 @@ import java.util.Arrays;
 public class OAuth2Configurer extends AuthorizationServerConfigurerAdapter {
     @Autowired
     private UrlUserService urlUserService;
+    @Autowired
+    private SysDictionaryFacade sysDictionaryFacade;
 
     @Override
     public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
+        int accessToken = 24 * 3600;
+        int refreshToken = 30 * 24 * 3600;
+        if (sysDictionaryFacade.getDictionaryWithKey() != null
+                && sysDictionaryFacade.getDictionaryWithKey().containsKey("31")
+                && sysDictionaryFacade.getDictionaryWithKey().get("31").containsKey("accessToken")
+                && sysDictionaryFacade.getDictionaryWithKey().get("31").containsKey("refreshToken")) {
+            accessToken = Integer.parseInt(sysDictionaryFacade.getDictionaryWithKey().get("31").get("accessToken"));
+            refreshToken = Integer.parseInt(sysDictionaryFacade.getDictionaryWithKey().get("31").get("refreshToken"));
+        }
+        sysDictionaryFacade.getDictionaryWithKey().get("31").get("");
         clients.inMemory()
                 .withClient("uaa-service")
                 .secret("{noop}123456")
                 .scopes("service")
                 .autoApprove(true)
                 .authorizedGrantTypes("implicit", "refresh_token", "password", "authorization_code")
-                .accessTokenValiditySeconds(24 * 3600)
-                .refreshTokenValiditySeconds(30 * 24 * 3600);
+                .accessTokenValiditySeconds(accessToken)
+                .refreshTokenValiditySeconds(refreshToken);
     }
 
     /**

+ 6 - 0
src/main/java/com/diagbot/config/ResourceServerConfigurer.java

@@ -13,6 +13,7 @@ import org.springframework.security.jwt.crypto.sign.RsaVerifier;
 import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
 import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
 import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
+import org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint;
 import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
 import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
 import org.springframework.util.FileCopyUtils;
@@ -38,6 +39,8 @@ public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
                 .authorizeRequests()
                 .regexMatchers(".*swagger.*", ".*v2.*", ".*webjars.*", "/druid.*", "/actuator.*", "/hystrix.*").permitAll()
                 .antMatchers("/sys/user/getJwt").permitAll()
+                .antMatchers("/sys/user/logout").permitAll()
+                .antMatchers("/sys/user/unlock").permitAll()
                 .antMatchers("/sys/user/getCaptcha").permitAll()
                 .antMatchers("/sys/user/getHospitalMark").permitAll()
                 .antMatchers("/sys/user/getJwtNoPass").permitAll()
@@ -258,6 +261,9 @@ public class ResourceServerConfigurer extends ResourceServerConfigurerAdapter {
     public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
         log.info("Configuring ResourceServerSecurityConfigurer");
         resources.resourceId("user-service").tokenStore(new JwtTokenStore(jwtTokenEnhancerClient()));
+        OAuth2AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
+        authenticationEntryPoint.setExceptionTranslator(new CustomExceptionTranslator());
+        resources.authenticationEntryPoint(authenticationEntryPoint);
     }
     @Autowired
     private CustomAccessTokenConverter customAccessTokenConverter;

+ 44 - 4
src/main/java/com/diagbot/config/security/UrlAccessDecisionManager.java

@@ -1,5 +1,7 @@
 package com.diagbot.config.security;
 
+import com.diagbot.exception.CommonErrorCode;
+import com.diagbot.exception.CommonException;
 import com.diagbot.facade.TokenFacade;
 import com.diagbot.util.HttpUtils;
 import com.diagbot.util.StringUtil;
@@ -32,17 +34,29 @@ public class UrlAccessDecisionManager implements AccessDecisionManager {
     public void decide(Authentication authentication, Object object, Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
         HttpServletRequest request = ((FilterInvocation) object).getHttpRequest();
         String url, method;
+        String tokenStr = HttpUtils.getHeaders(request).get("Authorization");
+        //用户是否被顶掉校验
+        if (StringUtil.isNotEmpty(tokenStr) && !matchNotCheckUrl(request)) {
+            tokenStr = tokenStr.replaceFirst("Bearer ", "");
+            int res = tokenFacade.newVerifyToken(tokenStr, 1);
+            if (-1 == res) {
+                throw new CommonException(CommonErrorCode.SERVER_IS_ERROR, "该账号在其他地方登录。");
+            }
+        }
         if (matchPermitAllUrl(request)) {
             return;
         }
         if ("anonymousUser".equals(authentication.getPrincipal())) {
             throw new AccessDeniedException("no right");
         } else {
-            String tokenStr = HttpUtils.getHeaders(request).get("Authorization");
+
             if (StringUtil.isNotEmpty(tokenStr)) {
                 tokenStr = tokenStr.replaceFirst("Bearer ", "");
-                Boolean res = tokenFacade.verifyToken(tokenStr, 1);
-                if (!res) {
+//                Boolean res = tokenFacade.verifyToken(tokenStr, 1);
+                int res = tokenFacade.newVerifyToken(tokenStr, 1);
+                if (-1 == res) {
+                    throw new CommonException(CommonErrorCode.SERVER_IS_ERROR, "该账号在其他地方登录。");
+                } else if (1 != res) {
                     throw new AccountExpiredException("token expire");
                 }
             }
@@ -57,9 +71,33 @@ public class UrlAccessDecisionManager implements AccessDecisionManager {
                 }
             }
         }
-        throw new AccessDeniedException("no right");
+        throw new AccessDeniedException("无接口访问权限!");
     }
 
+    private boolean matchNotCheckUrl(HttpServletRequest request) {
+        if (matchers("/swagger/**", request)
+                || matchers("/v2/**", request)
+                || matchers("/swagger-ui.html/**", request)
+                || matchers("/swagger-resources/**", request)
+                || matchers("/webjars/**", request)
+                || matchers("/druid/**", request)
+                || matchers("/actuator/**", request)
+                || matchers("/hystrix/**", request)
+                || matchers("/sys/user/getJwt", request)
+                || matchers("/sys/user/logout", request)
+                || matchers("/sys/user/getCaptcha", request)
+                || matchers("/sys/user/getHospitalMark", request)
+                || matchers("/sys/user/getJwtNoPass", request)
+                || matchers("/sys/user/refreshJwt", request)
+                || matchers("/sys/dictionaryInfo/getDictionary", request)
+                || matchers("/sys/user/checkToken", request)
+                || matchers("/oauth/token", request)
+                || matchers("/oauth/check_token", request)
+                || matchers("/cache/clear", request)) {
+            return true;
+        }
+        return false;
+    }
 
     @Override
     public boolean supports(ConfigAttribute attribute) {
@@ -81,6 +119,8 @@ public class UrlAccessDecisionManager implements AccessDecisionManager {
                 || matchers("/actuator/**", request)
                 || matchers("/hystrix/**", request)
                 || matchers("/sys/user/getJwt", request)
+                || matchers("/sys/user/logout", request)
+                || matchers("/sys/user/unlock", request)
                 || matchers("/sys/user/getCaptcha", request)
                 || matchers("/sys/user/getHospitalMark", request)
                 || matchers("/sys/user/getJwtNoPass", request)

+ 13 - 1
src/main/java/com/diagbot/config/security/UrlFilterSecurityInterceptor.java

@@ -1,5 +1,7 @@
 package com.diagbot.config.security;
 
+import com.alibaba.fastjson.JSONObject;
+import com.diagbot.exception.CommonException;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.SecurityMetadataSource;
 import org.springframework.security.access.intercept.AbstractSecurityInterceptor;
@@ -43,7 +45,17 @@ public class UrlFilterSecurityInterceptor extends AbstractSecurityInterceptor im
     public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
 
         FilterInvocation fi = new FilterInvocation(request, response, chain);
-        invoke(fi);
+        try {
+            invoke(fi);
+        }catch (CommonException e)
+        {
+            JSONObject error = new JSONObject();
+            response.setContentType("text/html;charset=utf-8");
+            error.put("code",e.getCode());
+            error.put("msg", e.getMessage());
+            response.getWriter().println(error);
+            response.flushBuffer();
+        }
     }
 
 

+ 1 - 0
src/main/java/com/diagbot/dto/SysUserQueryDTO.java

@@ -19,6 +19,7 @@ public class SysUserQueryDTO {
     private String roleName;
     private Integer status;
     private String statusName;
+    private String locked;
 
 
     public String getStatusName() {

+ 6 - 0
src/main/java/com/diagbot/entity/SysUser.java

@@ -3,6 +3,7 @@ package com.diagbot.entity;
 import com.baomidou.mybatisplus.annotation.IdType;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
 
 import java.io.Serializable;
 import java.util.Date;
@@ -16,6 +17,7 @@ import java.util.Date;
  * @since 2020-04-09
  */
 @TableName("sys_user")
+@Data
 public class SysUser implements Serializable {
 
     private static final long serialVersionUID = 1L;
@@ -80,6 +82,10 @@ public class SysUser implements Serializable {
      * 备注
      */
     private String remark;
+    /**
+     * 是否删除,N:未删除,Y:删除
+     */
+    private String locked;
 
     public Long getId() {
         return id;

+ 47 - 0
src/main/java/com/diagbot/enums/LockEnum.java

@@ -0,0 +1,47 @@
+package com.diagbot.enums;
+
+import lombok.Setter;
+
+/**
+ * @author wangfeng
+ * @Description: TODO
+ * @date 2018年11月21日 下午2:31:42
+ */
+public enum LockEnum{
+    LOCK("0", "锁定"),
+    UNLOCK("1", "解锁");
+
+    @Setter
+    private String key;
+
+    @Setter
+    private String name;
+
+    LockEnum(String key, String name) {
+        this.key = key;
+        this.name = name;
+    }
+
+    public static LockEnum getEnum(String key) {
+        for (LockEnum item : LockEnum.values()) {
+            if (item.key == key) {
+                return item;
+            }
+        }
+        return null;
+    }
+
+    public static String getName(String key) {
+        LockEnum item = getEnum(key);
+        return item != null ? item.name : null;
+    }
+
+    public String getKey() {
+        return key;
+    }
+
+    public String getName() {
+        return name;
+    }
+}
+

+ 197 - 43
src/main/java/com/diagbot/facade/SysUserFacade.java

@@ -17,6 +17,7 @@ import com.diagbot.entity.SysUserRole;
 import com.diagbot.entity.wrapper.SysMenuWrapper;
 import com.diagbot.enums.ConstantEnum;
 import com.diagbot.enums.IsDeleteEnum;
+import com.diagbot.enums.LockEnum;
 import com.diagbot.enums.StatusEnum;
 import com.diagbot.exception.CommonErrorCode;
 import com.diagbot.exception.CommonException;
@@ -24,6 +25,7 @@ import com.diagbot.exception.ServiceErrorCode;
 import com.diagbot.service.impl.SysUserDeptServiceImpl;
 import com.diagbot.service.impl.SysUserRoleServiceImpl;
 import com.diagbot.service.impl.SysUserServiceImpl;
+import com.diagbot.task.LockTask;
 import com.diagbot.util.BeanUtil;
 import com.diagbot.util.DateUtil;
 import com.diagbot.util.EntityUtil;
@@ -52,9 +54,9 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import javax.servlet.http.HttpSession;
 import java.awt.image.BufferedImage;
-import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.util.*;
+import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 /**
@@ -101,8 +103,8 @@ public class SysUserFacade extends SysUserServiceImpl {
     private SysHospitalSetFacade sysHospitalSetFacade;
     @Autowired
     private RedisUtils redisUtils;
-
-
+    @Autowired
+    private LockTask lockTask;
 
 
     public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException {
@@ -125,6 +127,7 @@ public class SysUserFacade extends SysUserServiceImpl {
 
     /**
      * 获取标识--选择登录页面
+     *
      * @return java.lang.Long
      */
     public Long getHospitalMark() {
@@ -133,16 +136,18 @@ public class SysUserFacade extends SysUserServiceImpl {
                 .eq(SysHospitalSet::getHospitalId, 35)
                 .eq(SysHospitalSet::getIsDeleted, IsDeleteEnum.N.getKey())
                 .eq(SysHospitalSet::getCode, "special_page_csxy").one();
-        if(null != sysHospitalSet){
+        if (null != sysHospitalSet) {
             String val = sysHospitalSet.getValue();
-            if(StringUtil.isNotBlank(val) && "1".equals(val)){
-               mark =  Long.valueOf(val);
+            if (StringUtil.isNotBlank(val) && "1".equals(val)) {
+                mark = Long.valueOf(val);
             }
         }
         return mark;
     }
+
     /**
      * 获取jwt
+     *
      * @param username 用户名
      * @param password 密码
      * @return jwt
@@ -161,12 +166,10 @@ public class SysUserFacade extends SysUserServiceImpl {
             throw new CommonException(CommonErrorCode.PARAM_IS_NULL,
                     "请输入验证码");
         }
+
         // 验证码校验
         String captchaId = request.getSession().getId();
-        Object captchaObject = redisUtils.get("user:captchaIds:" + captchaId);
-        if (null == captchaObject || StringUtil.isBlank(captchaObject.toString()) || !captchaObject.toString().trim().equalsIgnoreCase(captcha)) {
-            throw new CommonException(CommonErrorCode.PARAM_IS_ERROR, "验证码错误");
-        }
+        checkCaptcha(captchaId, captcha, redisUtils);
         //使用MD5对密码进行加密
         String MD5Password = DigestUtils.md5DigestAsHex(password.getBytes());
         QueryWrapper<SysUser> userQueryWrapper = new QueryWrapper<>();
@@ -174,13 +177,46 @@ public class SysUserFacade extends SysUserServiceImpl {
                 .eq("status", StatusEnum.Enable.getKey())
                 .eq("is_deleted", IsDeleteEnum.N.getKey());
         SysUser user = this.getOne(userQueryWrapper, false);
+        //判断用户是否存在
         if (null == user) {
             throw new CommonException(ServiceErrorCode.USER_NOT_FOUND);
         }
+        //获取用户所在医院
+        Long id = user.getId();
+        QueryWrapper<SysUserHospital> UserHospitalQueryWrapper = new QueryWrapper<>();
+        UserHospitalQueryWrapper
+                .eq("user_id", id)
+                .eq("is_deleted", IsDeleteEnum.N.getKey());
+        SysUserHospital userHospital = sysUserHospitalFacade.getOne(UserHospitalQueryWrapper, false);
+        Long hospitalId = userHospital.getHospitalId();
+
+        //判断医院是否启用用户锁定校验
+        QueryWrapper<SysHospitalSet> hospitalSetQueryWrapper = new QueryWrapper<>();
+        hospitalSetQueryWrapper.eq("is_deleted", 'N')
+                .eq("hospital_id", hospitalId)
+                .eq("code", "lock_user");
+        SysHospitalSet hospitalSet = sysHospitalSetFacade.getOne(hospitalSetQueryWrapper);
+        boolean lockFlag = false;
+        if (hospitalSet != null) {
+            lockFlag = Boolean.parseBoolean(hospitalSet.getValue());
+        }
+
+
+        //判断用户是否锁定
+        if (lockFlag) {
+            lockCheck(redisUtils, user);
+        }
+        //密码是否正确
         PasswordEncoder passwordEncoder = PasswordEncoderFactories.createDelegatingPasswordEncoder();
         if (!passwordEncoder.matches(MD5Password, user.getPassword())) {
-            throw new CommonException(ServiceErrorCode.USER_PASSWORD_ERROR);
+            if (!lockFlag) {
+                throw new CommonException(ServiceErrorCode.USER_PASSWORD_ERROR);
+            } else {
+                //用户获取错误次数
+                lockNumCheck(redisUtils, user, sysDictionaryFacade);
+            }
         }
+
         JWT jwt = authServiceClient.getToken("Basic dWFhLXNlcnZpY2U6MTIzNDU2",
                 "password", username, MD5Password);
         if (null == jwt) {
@@ -195,15 +231,12 @@ public class SysUserFacade extends SysUserServiceImpl {
         try {
             SysUserBaseVO sysUserBaseVO = new SysUserBaseVO();
             sysUserBaseVO.setUserId(user.getId());
-            List<SysRoleDTO> selRoles =getlocalUserRoles(sysUserBaseVO);
-            if(selRoles!=null&&!selRoles.isEmpty())
-            {
+            List<SysRoleDTO> selRoles = getlocalUserRoles(sysUserBaseVO);
+            if (selRoles != null && !selRoles.isEmpty()) {
                 data.setSelRoles(selRoles);
             }
 
-        }
-        catch (Exception e)
-        {
+        } catch (Exception e) {
             e.printStackTrace();
         }
         //token存入redis
@@ -214,23 +247,124 @@ public class SysUserFacade extends SysUserServiceImpl {
         /***
          * 未经过MD5加密密码复杂度判断
          */
-     //   获取用户医院id
-   //     String hospitalID = SysUserUtils.getCurrentHospitalID();
-        Long id = user.getId();
-        QueryWrapper<SysUserHospital> UserHospitalQueryWrapper = new QueryWrapper<>();
-        UserHospitalQueryWrapper
-                .eq("user_id", id)
-                .eq("is_deleted", IsDeleteEnum.N.getKey());
-        SysUserHospital userHospital = sysUserHospitalFacade.getOne(UserHospitalQueryWrapper, false);
-        Long hospitalId = userHospital.getHospitalId();
+        //   获取用户医院id
+        //     String hospitalID = SysUserUtils.getCurrentHospitalID();
+
         String idStr = String.valueOf(hospitalId);
-        Boolean passwordRegular = passwordRegular(password,idStr);
-        if(!passwordRegular){
+        Boolean passwordRegular = passwordRegular(password, idStr);
+        if (!passwordRegular) {
             data.setPasswordComplexity("未修改初始密码,请及时修改密码");
         }
         return data;
     }
 
+    /**
+     * @param redisUtils
+     * @param user
+     * @param sysDictionaryFacade
+     * @Description锁定次数校验
+     * @Return void
+     */
+    private void lockNumCheck(RedisUtils redisUtils, SysUser user, SysDictionaryFacade sysDictionaryFacade) {
+        //获取锁定时间、解锁时间配置
+        int unlockTime = 180;
+        int lockTime = 60;
+        int lockNum = 5;
+        if (sysDictionaryFacade.getDictionaryWithKey() != null
+                && sysDictionaryFacade.getDictionaryWithKey().containsKey("31")
+                && sysDictionaryFacade.getDictionaryWithKey().get("31").containsKey("unlockTime")
+                && sysDictionaryFacade.getDictionaryWithKey().get("31").containsKey("lockNum")
+                && sysDictionaryFacade.getDictionaryWithKey().get("31").containsKey("lockTime")) {
+            unlockTime = Integer.parseInt(sysDictionaryFacade.getDictionaryWithKey().get("31").get("unlockTime"));
+            lockTime = Integer.parseInt(sysDictionaryFacade.getDictionaryWithKey().get("31").get("lockTime"));
+            lockNum = Integer.parseInt(sysDictionaryFacade.getDictionaryWithKey().get("31").get("lockNum"));
+        }
+
+        Object numObj = redisUtils.get("user:pasError_" + user.getId());
+        int num = 1;
+        if (numObj != null) {
+            num = (Integer) numObj;
+            num = num + 1;
+            if (num < lockNum) {
+                redisUtils.opsForValue("user:pasError_" + user.getId(), num);
+            } else {
+                //锁定账号
+                lockUser(user.getId(), LockEnum.LOCK.getKey());
+                //启动定时任务解锁
+                runTimerTask(user.getId(), LockEnum.UNLOCK.getKey(), unlockTime);
+                //redis存入解锁账号(用户解锁剩余时间)
+                redisUtils.set("user:lockTime_" + user.getId(), "lock", unlockTime);
+                //获取锁定时间
+                getLockError(user.getId(), redisUtils);
+            }
+
+        } else {
+            redisUtils.set("user:pasError_" + user.getId(), num, lockTime);
+        }
+        String error = "账号或密码不正确,剩%s次机会,之后账号将被锁定10分钟";
+        throw new CommonException(ServiceErrorCode.USER_PASSWORD_ERROR, String.format(error, lockNum - num));
+    }
+
+    /**
+     * @param redisUtils
+     * @param user
+     * @Description锁定校验
+     * @Return void
+     */
+    private void lockCheck(RedisUtils redisUtils, SysUser user) {
+        if (LockEnum.LOCK.getKey().equals(user.getLocked())) {
+            //获取锁定时间
+            getLockError(user.getId(), redisUtils);
+        }
+    }
+
+    /**
+     * @param captchaId
+     * @param captcha
+     * @param redisUtils
+     * @Description验证码校验
+     * @Return void
+     */
+    private void checkCaptcha(String captchaId, String captcha, RedisUtils redisUtils) {
+        Object captchaObject = redisUtils.get("user:captchaIds:" + captchaId);
+        if (null == captchaObject || StringUtil.isBlank(captchaObject.toString()) || !captchaObject.toString().trim().equalsIgnoreCase(captcha)) {
+            throw new CommonException(CommonErrorCode.PARAM_IS_ERROR, "验证码错误");
+        }
+    }
+
+    private void getLockError(Long id, RedisUtils redisUtils) {
+        Long endTime = redisUtils.getExpire("user:lockTime_" + id);
+        long mint = endTime / 60;
+        if (endTime % 60 > 0) {
+            mint = mint + 1;
+        }
+        if (endTime >= 0) {
+            String error = "账号已被锁定,%s分钟后将自动解锁或联系管理员解锁";
+            throw new CommonException(CommonErrorCode.SERVER_IS_ERROR, String.format(error, mint));
+        }
+        throw new CommonException(CommonErrorCode.SERVER_IS_ERROR, "用户被锁定,自动解锁异常请联系管理员!!!");
+    }
+
+    private void runTimerTask(Long id, String lock, int time) {
+        lockTask.getMScheduledExecutorService().schedule(new Runnable() {
+            @Override
+            public void run() {
+                try {
+                    lockUser(id, lock);
+                } catch (Exception e) {
+                    e.printStackTrace();
+                }
+            }
+        }, time, TimeUnit.SECONDS);
+    }
+
+    public boolean lockUser(Long id, String key) {
+        SysUser user = new SysUser();
+        user.setId(id);
+        user.setLocked(key);
+        return this.updateById(user);
+    }
+
     /**
      * 外部获取jwt
      *
@@ -337,8 +471,8 @@ public class SysUserFacade extends SysUserServiceImpl {
         }
         //对传入的密码进行格式验证
         String hospitalID = SysUserUtils.getCurrentHospitalID();
-        Boolean regularBoolean = passwordRegular(modifyPassword,hospitalID);
-        if(!regularBoolean){
+        Boolean regularBoolean = passwordRegular(modifyPassword, hospitalID);
+        if (!regularBoolean) {
             throw new CommonException(CommonErrorCode.PARAM_IS_ERROR, "请输入正确格式的新密码");
         }
         String userId = SysUserUtils.getCurrentPrincipleID();
@@ -368,23 +502,25 @@ public class SysUserFacade extends SysUserServiceImpl {
 
     /**
      * 未加密密文正则表达式  至少8个字符,1个大写字母,1个小写字母,1个数字和1个特殊字符:
+     *
      * @param password
      * @return
      */
-    public Boolean passwordRegular(String password,String hospitalId){
-        boolean check=true;
+    public Boolean passwordRegular(String password, String hospitalId) {
+        boolean check = true;
         Map<String, Map<String, String>> dictionaryWithKey = sysDictionaryFacade.getDictionaryWithKey();
-        if(dictionaryWithKey!=null){
+        if (dictionaryWithKey != null) {
             Map<String, String> stringStringMap = dictionaryWithKey.get("30");
-            if(stringStringMap!=null) {
+            if (stringStringMap != null) {
                 String regular = stringStringMap.get(hospitalId);
-                if(StringUtil.isNotEmpty(regular)) {
+                if (StringUtil.isNotEmpty(regular)) {
                     check = password.matches(regular);
                 }
             }
         }
         return check;
     }
+
     /**
      * 登录
      *
@@ -399,13 +535,13 @@ public class SysUserFacade extends SysUserServiceImpl {
                 .eq("status", StatusEnum.Enable.getKey())
                 .eq("id", userId), false);
         QueryWrapper<SysUserRole> sysUserRoleQueryWrapper = new QueryWrapper<>();
-        sysUserRoleQueryWrapper.eq("user_id",userId);
-        sysUserRoleQueryWrapper .eq("is_deleted", IsDeleteEnum.N.getKey());
+        sysUserRoleQueryWrapper.eq("user_id", userId);
+        sysUserRoleQueryWrapper.eq("is_deleted", IsDeleteEnum.N.getKey());
         List<SysUserRole> sysUserRoleList = sysUserRoleFacade.list(sysUserRoleQueryWrapper);
-        if(ListUtil.isNotEmpty(sysUserRoleList)){
-            sysUserRoleList.forEach(sysUserRole ->{
-                roleSet.add(sysUserRole.getRoleId()+"");
-            } );
+        if (ListUtil.isNotEmpty(sysUserRoleList)) {
+            sysUserRoleList.forEach(sysUserRole -> {
+                roleSet.add(sysUserRole.getRoleId() + "");
+            });
         }
         if (user == null) {
             throw new CommonException(CommonErrorCode.SERVER_IS_ERROR,
@@ -428,8 +564,8 @@ public class SysUserFacade extends SysUserServiceImpl {
         }
 
         //添加菜单信息
-        List<SysMenuWrapper> menuList = sysMenuFacade.getByRole(user.getId(),roleSet);
-        List<SysUserPermissionDTO> sysUserPermissionDTOList = sysMenuFacade.getByRolePermission(user.getId(),roleSet);
+        List<SysMenuWrapper> menuList = sysMenuFacade.getByRole(user.getId(), roleSet);
+        List<SysUserPermissionDTO> sysUserPermissionDTOList = sysMenuFacade.getByRolePermission(user.getId(), roleSet);
         Map<Long, List<SysMenuWrapper>> menuMap = EntityUtil.makeEntityListMap(menuList, "parentId");
         Map<Long, List<SysUserPermissionDTO>> menuPermissionMap = EntityUtil.makeEntityListMap(sysUserPermissionDTOList, "menuId");
         List<SysMenuWrapper> menuRes = menuMap.get(-1L);
@@ -585,6 +721,7 @@ public class SysUserFacade extends SysUserServiceImpl {
 
         return sysUserRoleDTO;
     }
+
     /**
      * 获取用户角色
      *
@@ -766,6 +903,23 @@ public class SysUserFacade extends SysUserServiceImpl {
             throw new CommonException(CommonErrorCode.PARAM_IS_ERROR, "该医院下无该用户");
         }
     }
+
     //-------------用户维护END---------------------------
 
+    /**
+     * @param
+     * @Description登出功能
+     * @Return java.lang.Boolean
+     */
+    public Boolean logout() {
+        try {
+            //清除缓存
+
+            //销毁token
+            redisUtils.del("user_tokens_" + SysUserUtils.getCurrentPrincipleID());
+        } catch (Exception e) {
+            throw new CommonException(CommonErrorCode.FAIL, "登出失败");
+        }
+        return true;
+    }
 }

+ 8 - 0
src/main/java/com/diagbot/service/SysTokenService.java

@@ -27,6 +27,14 @@ public interface SysTokenService {
      * @return
      */
     Boolean verifyToken(String token, Integer type);
+    /**
+     * 验证token是否有效
+     *
+     * @param token 待验证的token
+     * @param type  1:accessToken,2:refreshToken
+     * @return
+     */
+    int newVerifyToken(String token, Integer type);
 
     /**
      * 删除用户token

+ 54 - 0
src/main/java/com/diagbot/service/impl/SysTokenServiceImpl.java

@@ -127,6 +127,60 @@ public class SysTokenServiceImpl implements SysTokenService {
         return res;
     }
 
+    /**
+     * 验证token是否有效
+     *
+     * @param token 待验证的token
+     * @param type  1:accessToken,2:refreshToken
+     * @return -1:token无效(与服务器token不一致,异地登录),1:token有效,0:其他
+     */
+    @Override
+    public int newVerifyToken(String token, Integer type) {
+        Integer res = 0;
+        if (null == token) {
+            return 0;
+        }
+        String userId = JwtUtil.getUserId(token);
+        //从redis中取出
+        final byte[] redis_key = getUserTokenKey(userId);
+        JwtStore tokenStore = (JwtStore) redisForToken.execute(new RedisCallback<JwtStore>() {
+            @Override
+            public JwtStore doInRedis(RedisConnection connection) throws DataAccessException {
+                byte[] bytes = connection.get(redis_key);
+                if (bytes == null) {
+                    return null;
+                }
+                return (JwtStore) deserializeValue(bytes);
+            }
+        });
+
+        if (null != tokenStore) {
+            if (type == 1) {
+                if (null != tokenStore.getAccessToken()) {
+                    if (tokenStore.getAccessToken().equals(token)) {
+                        res = 1;
+                    } else {
+                        res = -1;
+                    }
+                }
+            }
+
+            if (type == 2) {
+                if (null != tokenStore.getRefreshToken()) {
+                    if (tokenStore.getRefreshToken().equals(token)) {
+                        res = 1;
+                    } else {
+                        res = -1;
+                    }
+                }
+            }
+        } else {
+            res = -1;
+        }
+
+        return res;
+    }
+
     /**
      * 删除用户token
      *

+ 19 - 0
src/main/java/com/diagbot/task/LockTask.java

@@ -0,0 +1,19 @@
+package com.diagbot.task;
+
+import lombok.Data;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+
+/**
+ * @Description:
+ * @Author songxl
+ * @Date 2021/11/24
+ */
+@Component
+@Data
+public class LockTask {
+    //定时执行线程池
+    private ScheduledExecutorService mScheduledExecutorService = Executors.newScheduledThreadPool(10);
+}

+ 21 - 0
src/main/java/com/diagbot/util/RedisUtils.java

@@ -8,7 +8,9 @@ import org.springframework.transaction.annotation.Propagation;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
+import java.time.Instant;
 import java.util.Collection;
+import java.util.Date;
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
@@ -52,6 +54,25 @@ public class RedisUtils {
             return false;
         }
     }
+    /**
+     * 插入值失效时间不会重置
+     *
+     * @param key  键
+     * @param val 值
+     * @return
+     */
+    public boolean opsForValue(String key, Object val) {
+        try {
+            Long time = redisTemplate.getExpire(key);
+            if (time > 0) {
+                redisTemplate.opsForValue().set(key, val, time, TimeUnit.SECONDS);
+            }
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
 
     /**
      * 根据key 获取过期时间

+ 18 - 1
src/main/java/com/diagbot/web/SysUserController.java

@@ -10,6 +10,7 @@ import com.diagbot.dto.SysUserDeptDTO;
 import com.diagbot.dto.SysUserQueryDTO;
 import com.diagbot.dto.SysUserRoleDTO;
 import com.diagbot.entity.Token;
+import com.diagbot.enums.LockEnum;
 import com.diagbot.facade.SysUserFacade;
 import com.diagbot.facade.TokenFacade;
 import com.diagbot.vo.JwtVO;
@@ -28,6 +29,7 @@ import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 import springfox.documentation.annotations.ApiIgnore;
 
@@ -73,7 +75,22 @@ public class SysUserController {
         JwtDTO data = userFacade.getJwt(request,userLoginVO.getUsername(), userLoginVO.getPassword(),userLoginVO.getCaptcha());
         return RespDTO.onSuc(data);
     }
-
+    @ApiOperation(value = "登出[by:gaodm]",
+            notes = "")
+    @PostMapping("/logout")
+    @SysLogger("logout")
+    public RespDTO<Boolean> logout() {
+        Boolean data = userFacade.logout();
+        return RespDTO.onSuc(data);
+    }
+    @ApiOperation(value = "解锁[by:gaodm]",
+            notes = "")
+    @PostMapping("/unlock")
+    @SysLogger("unlock")
+    public RespDTO<Boolean> unlock(@RequestParam Long id) {
+        Boolean data = userFacade.lockUser(id, LockEnum.UNLOCK.getKey());
+        return RespDTO.onSuc(data);
+    }
     @ApiOperation(value = "获取验证码[by:cy]")
     @GetMapping("/getCaptcha")
     @SysLogger("getCaptcha")

+ 7 - 2
src/main/resources/mapper/SysUserMapper.xml

@@ -15,6 +15,7 @@
         <result column="password" property="password"/>
         <result column="linkman" property="linkman"/>
         <result column="type" property="type"/>
+        <result column="locked" property="locked"/>
         <result column="remark" property="remark"/>
     </resultMap>
 
@@ -26,7 +27,8 @@
         t3.linkman AS linkman,
         t6.deptName AS deptName,
         t10.roleName AS roleName,
-        t3.`status` AS `status`
+        t3.`status` AS `status`,
+        t3.locked
         FROM
         (
         SELECT
@@ -34,7 +36,8 @@
         t1.username AS userName,
         t1.linkman AS linkman,
         t1.`status` AS `status`,
-        t2.hospital_id AS hospitalId
+        t2.hospital_id AS hospitalId,
+        t1.locked
         FROM
         sys_user t1,
         sys_user_hospital t2
@@ -125,5 +128,7 @@
         <if test="linkman != null and linkman != ''">
             and t3.linkman like CONCAT('%',#{linkman},'%')
         </if>
+        ORDER BY
+        t3.locked
     </select>
 </mapper>