如何完成登录验证(分布式session缓存+验证码)

1. 配置session属性

@Component
@ConfigurationProperties(prefix = "gcloud.session")
public class Nonprofessional {
    private Integer maxLoginUser;
    private Integer maxLoginFail;
    private Integer showCaptchaTime;

   getter / setter.......
    

2.key配置

public class Conts {
    private static final String KEY_PREFIX = "gcloud_seccenter_mgr";
    //项目模块_功能:具体key
    private static final String KEY_FORMAT = "%s_%s:%s";
    public static final class Lock {
        public static final String SUB_PREFIX = "lock";
        public static final String MAX_ONLINE_USER_CHECK_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "max_onlie_user_check");
        public static final int MAX_ONLINE_USER_CHECK_EXPIRE_TIME = 5;
        public static final long MAX_ONLINE_USER_CHECK_APPLY_EXOIRE_TIME = 5000L;
        public static final String USER_LOGIN_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "user_login_{0}");
        public static final int USER_LOGIN_EXPIRE_TIME = 5;
        public static final long USER_LOGIN_APPLY_EXOIRE_TIME = 5000L;
    }
    //!!!!!!String 类型只能是图片验证码的KEY ,其他常量请放其他地方
    public static final class Captcha{
        public static final String SUB_PREFIX = "captcha";
        public static final int DEFAULT_SHOW_CAPTCHA_TIME = 3;
        public static final String USER_LOGIN_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "user_login");
        public static final long USER_LOGIN_EXPIRE = 600000L;
    }
    public static final class RedisCaptcha{
        public static final String SUB_PREFIX = "redisCaptcha";
        public static final String FORGET_PASSWORD_KEY = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "forget_password_{0}");
        public static final long FORGET_PASSWORD_EXPIRE = 600000L;
    }

    public static final class Session{
        public static final String SUB_PREFIX = "session";
        public static final String SESSION_LOGIN_FAIL_TIME = String.format(KEY_FORMAT, KEY_PREFIX, SUB_PREFIX, "login_fail_time");
    }
}

3.登录入参

public class LoginParam {

    @NotBlank(message = "mgr_user_login_0001::username can not be null")
    private String username;
    @NotNull(message = "mgr_user_login_0002::password can not be null")
    private String password;
    private String captcha;//验证码

  getter / setter......  

}

4.session管理

public class SessionUtil {

    private static final String DEFAULT_KEY_PREFIX = "spring:session:";

    //online_user_userid_devicetype_sessionid

    private static final String ONLINE_SESSION_PREFIX = "online_user";

    @SuppressWarnings("unchecked")
   private static RedisTemplate<String, Object> redisTemplate = (RedisTemplate<String, Object>) SpringUtil.getApplicationContext().getBean("redisTemplate");

    public static String getUserDeviceOnlineKey(String userId, DeviceType deviceType, String sessionId){

        return String.format("%s%s", getUserDeviceOnlieKeyPrefix(userId, deviceType), sessionId);
    }

    public static String getUserDeviceOnlieKeyPrefix(String userId, DeviceType deviceType){
        return String.format("%s_%s_%s_", ONLINE_SESSION_PREFIX, userId, deviceType.getValue());
    }

    public static String getSessionKeysPattern(String sessionId){
        return String.format("%s_*_%s", ONLINE_SESSION_PREFIX, sessionId);
    }

    public static String getOnlineUserPattern(DeviceType deviceType){
        String pattern = String.format("%s_*", ONLINE_SESSION_PREFIX);
        if(deviceType != null){
            pattern = String.format("%s_*_%s_*", ONLINE_SESSION_PREFIX, deviceType.getValue());
        }
        return pattern;
    }

    public static String getUserOnlieKeyPrefix(String userId){
        return String.format("%s_%s_", ONLINE_SESSION_PREFIX, userId);
    }

    public static Set<String> getOnlineUserKeys(DeviceType deviceType){
        Set<String> keySet = redisTemplate.keys(getOnlineUserPattern(deviceType));
        return keySet;
    }

    public static int getOnlineUserNumber(DeviceType deviceType){
        Set<String> keySet = redisTemplate.keys(getOnlineUserPattern(deviceType));
        return keySet == null ? 0 : keySet.size();
    }

    public static String getSessionId(String userDeviceSessionOnlineKey){

        String sessionId = null;
        if(userDeviceSessionOnlineKey != null){
            String[] userSessionInfo = userDeviceSessionOnlineKey.split("_");
            if(userSessionInfo.length > 0){
                sessionId = userDeviceSessionOnlineKey.split("_")[userSessionInfo.length - 1];
            }
        }
        return sessionId;
    }
//查找在线用户
    public static Set<String> getUserRelateOnlineKeys(String userId, DeviceType deviceType){
        String userSessionKeyPrefix = null;
        if(deviceType == null){
            userSessionKeyPrefix = SessionUtil.getUserOnlieKeyPrefix(userId);
        }else{
            userSessionKeyPrefix = SessionUtil.getUserDeviceOnlieKeyPrefix(userId, DeviceType.WEB);
        }

        Set<String> keySet = redisTemplate.keys(userSessionKeyPrefix + "*");
        return keySet;
    }

    public static Set<String> getSessionRelateOnlineKeys(String sessionId){
        Set<String> keySet = redisTemplate.keys(getSessionKeysPattern(sessionId));
        return keySet;
    }

    public static boolean isUserOnline(String userId){
        Set<String> keySet = getUserRelateOnlineKeys(userId, null);
        boolean isOnline = false;
        if(keySet != null && keySet.size() > 0){
            isOnline = true;
        }
        return isOnline;
    }

    public static void refreshUserDeviceOnlineKey(HttpSession session){

        if(session == null){
            return;
        }

        SessionUser sessionUser = (SessionUser) session.getAttribute(HttpRequestConstant.SESSION_USER_INFO);
        if(sessionUser != null){
            String key = getUserDeviceOnlineKey(sessionUser.getUserId(), sessionUser.getDeviceType(), session.getId());
            long expireTime = session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000;
            Date expireDate = new Date(expireTime);
            redisTemplate.expireAt(key, expireDate);
        }

    }

    public static String getKeyPrefix(String namespace){
        String prefix = DEFAULT_KEY_PREFIX;
        if(StringUtils.isNotBlank(namespace)){
            prefix = prefix + namespace + ":";
        }
        return prefix;
    }

    public static String getExpirationKey(String namespace, long expiration){
        return getKeyPrefix(namespace) + "expirations:" + expiration;
    }
}

5.登录设备

public enum DeviceType implements Serializable {

    WEB(0, "web", "web浏览器"),
    DESKTOP(1, "desktop", "桌面应用"),
    APP(2, "app", "手机APP");

    private int value;
    private String name;
    private String cnName;

    DeviceType(int value, String name, String cnName) {
        this.value = value;
        this.name = name;
        this.cnName = cnName;
    }

    public static DeviceType getByValue(Integer value){
        DeviceType result = null;
        if(value != null){
            for(DeviceType type : DeviceType.values()){
                if(type.getValue() == value){
                    result = type;
                    break;
                }
            }
        }
        return result;
    }

    public static Map<Integer, String> getValueCnMap(){
        Map<Integer, String> result = new HashMap<Integer, String>();
        for(DeviceType type : DeviceType.values()){
            result.put(type.getValue(), type.getCnName());
        }

        return result;
    }

    public int getValue() {
        return value;
    }

    public String getName() {
        return name;
    }

    public String getCnName() {
        return cnName;
    }

}

6.配置redis session

/*
 * @Desccription redis session 配置
 */
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
    //默认1个小时
    @Value("${server.session.timeout:3600}")
    private Integer maxInactiveInterval;

    @Value("${spring.session.redis.namespace:}")
    private String namespace;

    @Autowired
    private RedisOperationsSessionRepository sessionRepository;

    @PostConstruct
    private void afterPropertiesSet() {
        sessionRepository.setDefaultMaxInactiveInterval(maxInactiveInterval);
        if(StringUtils.isNotBlank(namespace)){
            sessionRepository.setRedisKeyNamespace(namespace);
        }
    }
    
}

7.session用户信息

/*
 * @Desccription session用户信息
 */
public class SessionUser implements Serializable {
   private static final long serialVersionUID = 1L;
   
   private String userId;
    private String username;
    private DeviceType deviceType;
 
 getter / setter......

}

8.redis配置(多台redis环境下)

/*
 * @Desccription jedis client 配置 RedisTemplate 的set不能使用 set EX PX [NX|XX] 所以改用jedis
 */
@Configuration
@ConditionalOnExpression("${gcloud.redis.jedisClient.enable:false} == true")
public class JedisConfig {

    @Value("${spring.redis.host:}")
    private String host;

    @Value("${spring.redis.port:}")
    private Integer port;

    @Value("${spring.redis.timeout:}")
    private Integer timeout;

    @Value("${spring.redis.pool.max-idle:}")
    private Integer maxIdle;

    @Value("${spring.redis.pool.max-wait:}")
    private Long maxWaitMillis;

    @Value("${spring.redis.pool.max-active:}")
    private Integer maxActive;

    @Value("${spring.redis.password:}")
    private String password;


    @Bean
    public ShardedJedisPool shardedJedisPool() {
        JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
        if(maxActive != null){
            jedisPoolConfig.setMaxTotal(maxActive);
        }
        if(maxIdle != null){
            jedisPoolConfig.setMaxIdle(maxIdle);
        }
        if(maxWaitMillis != null){
            jedisPoolConfig.setMaxWaitMillis(maxWaitMillis);
        }
        List<JedisShardInfo> jedisShardInfoList = new ArrayList<JedisShardInfo>();
        jedisShardInfoList.add(new JedisShardInfo(host, port));
        return new ShardedJedisPool(jedisPoolConfig, jedisShardInfoList);
    }

    @Bean
    public JedisClientTemplate jedisClientTemplate(){
        return new JedisClientTemplate();
    }

    @Bean
    public RedisLock redisLock(){
        return new RedisSpinLock();
    }

}

9.jedisclient客户端

/*
 * @Desccription jedisclient客户端
 */
public class JedisClientTemplate {
   @Autowired
   private ShardedJedisPool shardedJedisPool;
     /**
      * 设置单个值
      * @param key
      * @param value
      * @param nxxx NX|XX, NX -- Only set the key if it does not already exist. XX -- Only set the key
      *          if it already exist.
      * @param expx EX|PX, expire time units: EX = seconds; PX = milliseconds
      * @param time expire time in the units of <code>expx</code>
      * @return Status code reply
      */
   public String set(String key, String value, String nxxx, String expx, long time) {
      String result = null;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         result = shardedJedis.set(key, value, nxxx, expx, time);
      }
      return result;
   }
   /**
    * 设置单个值
    *
    * @param key
    * @param value
    * @return
    */
   public String set(String key, String value){
      String result = null;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         result = shardedJedis.set(key, value);
      }
      return result;
   }

   /**
    * 获取单个值
    *
    * @param key
    * @return
    */
   public String get(String key){
      String result = null;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         result = shardedJedis.get(key);

      }
      return result;
   }
   public Boolean exists(String key) throws GCloudException {
      Boolean result = false;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         result = shardedJedis.exists(key);
      }
      return result;
   }
   /**
    *
    * @Title: 非空才新增,并且加上超时时间
    * @Description: 以NX结尾,NX是Not eXists的缩写,如SETNX命令就应该理解为:SET if Not eXists
    * @date 2016-10-18 上午11:37:35
    *
    * @param key
    * @param value
    * @param time  超时时间,单位:秒
    * @return 如返回1,则该客户端获得锁,把lock.foo的键值设置为时间值表示该键已被锁定,该客户端最后可以通过DEL lock.foo来释放该锁。
    *               如返回0,表明该锁已被其他客户端取得,这时我们可以先返回或进行重试等对方完成或等待锁超时。
    * @throws GCloudException
    */
   public Long setnx(String key, String value, long time) throws GCloudException {
      Long result = null;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         String res = shardedJedis.set(key, value, "NX", "EX", time);
         if (StringUtils.isNotBlank(res) && res.equalsIgnoreCase("ok")) {
            result = 1l;
         } else {
            result = 0l;
         }
      }
      return result;
   }

   public Long del(String key) throws GCloudException {
      Long result = null;
      try(ShardedJedis shardedJedis = shardedJedisPool.getResource()){
         result = shardedJedis.del(key);

      }
      return result;
   }
}

10.redis锁

public abstract class RedisLock {

   public abstract String getLock(String lockName, int lockTimeout, long getLockTimeout) throws GCloudException;

   public abstract void releaseLock(String lockName, String value) throws GCloudException;

}
@Slf4j
public class RedisSpinLock extends RedisLock {

   @Autowired
   private JedisClientTemplate jedisClientTemplate;

   @Override
   public String getLock(String lockName, int lockTimeout, long getLockTimeout) throws GCloudException {

      boolean isSucc = false;
      boolean isGetTimerout = false;

      String value = UUID.randomUUID().toString(); // 这个值用于删除
      long startTime = System.currentTimeMillis();
      do {
         if (jedisClientTemplate.setnx(lockName, value, lockTimeout) == 1L) {
            isSucc = true;
         } else {
            isSucc = false;
         }

         if (getLockTimeout >= 0) {
            isGetTimerout = System.currentTimeMillis() - startTime > getLockTimeout;
         }

      } while (!isSucc && !isGetTimerout);

      // 不成功直接抛错
      if (!isSucc) {
         throw new GCloudException("::get lock fail");
      }
      return value;
   }

   @Override
   public void releaseLock(String lockName, String value) throws GCloudException {

      try{
         String v = jedisClientTemplate.get(lockName);
         if (v != null && v.equals(value)) {
            jedisClientTemplate.del(lockName);
         }
      }catch (Exception ex){
         log.error("释放锁失败", ex);
      }
   }
}

11.登录业务

@Service
@Slf4j
@Transactional
public class UserServiceImpl implements UserService {


    @Autowired
    private UserDao userDao;

    @Autowired
    private UserRoleDao userRoleDao;

    @Autowired
    private SessionRepository<? extends Session> sessionRepository;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private GcloudSessionProp gcloudSessionProp;

    @Autowired
    private UserRoleService userRoleService;

    @Autowired
    private UserInfoService userInfoService;

    @Autowired
    private RedisOperationsSessionRepository redisRepository;

    @Autowired
    private RedisSessionProp redisSessionProp;

    @Autowired
    private MenuDao menuDao;

    private final int DEFAULT_MAX_FAIL_TIME = 5;

    //接入cas时,此方法不使用
    @Override
    @Transactional(noRollbackFor = GcTransactionException.class)
    public LoginResponse login(LoginParam param, HttpServletRequest request) throws GCloudException {
        HttpSession session = request.getSession();
        Captcha captcha = (Captcha) session.getAttribute(Conts.Captcha.USER_LOGIN_KEY);
        Integer sessionLoginFailTime = session.getAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME) == null ? 0 : (Integer) session.getAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME);
        Integer showTime = gcloudSessionProp.getShowCaptchaTime() == null ? Conts.Captcha.DEFAULT_SHOW_CAPTCHA_TIME : gcloudSessionProp.getShowCaptchaTime();
        //有验证码或者已经达到失败次数,都应该进行验证码验证
        if(captcha != null || (showTime > 0 && sessionLoginFailTime >= showTime)){

            if(captcha == null){
                throw new GCloudException("mgr_user_login_0013::captcha is not correct");
            }

            if(StringUtils.isBlank(param.getCaptcha())){
                throw new GCloudException("mgr_user_login_0012::captcha can not be null");
            }

            if(System.currentTimeMillis() > captcha.getTimeout()){
                throw new GCloudException("mgr_user_login_0010::captcha is invalidate");
            }

            if(!param.getCaptcha().equals(captcha.getCode())){
                throw new GCloudException("mgr_user_login_0011::captcha is not correct");
            }

            //验证成功,则去掉验证码
            removeSessionCaptcha(session, Conts.Captcha.USER_LOGIN_KEY);

        }


        //同一个用户需要计算失败次数,并锁定用户,所以需要锁定, 后面需要update,这里使用数据库锁
        User user = userDao.getValidateUserForUpdate(param.getUsername());

        if(user == null){
            throw new GCloudException("mgr_user_login_0003::user does not exist");
        }

        if(user.getIsLock() != null && user.getIsLock()){
            throw new GCloudException("mgr_user_login_0009::user was locked");
        }


        String inputPwdMd5 = null;
        try {
            inputPwdMd5 = MD5Util.encrypt(param.getPassword());
        } catch (Exception ex) {
            log.error("mgr_user_login_0004,密码md5加密失败", ex);
            throw new GCloudException("mgr_user_login_0004::password encrypt error");
        }


        if (inputPwdMd5 == null || !inputPwdMd5.equalsIgnoreCase(user.getPassword())) {
            //session的失败次数,用于判断是否显示验证码
            sessionLoginFailTime++;
            session.setAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME, sessionLoginFailTime);

            int totalFail = user.getLoginFailCount() == null ? 1 : user.getLoginFailCount() + 1;
            int maxFailTime = gcloudSessionProp.getMaxLoginFail() == null ? DEFAULT_MAX_FAIL_TIME : gcloudSessionProp.getMaxLoginFail();
            boolean isLock = maxFailTime > 0 && totalFail > maxFailTime;

            userDao.loginFail(user.getId(), isLock);

            throw new GcTransactionException("mgr_user_login_0005::password is not correct");
        }

        Integer loginNumLimit = gcloudSessionProp.getMaxLoginUser();
        //做最大在线人数限制
        String lockId = "";

        if(loginNumLimit != null && loginNumLimit >= 0){
            //防止并发导致最大在线人数超过限制,需要需要同步锁
            lockId = LockUtil.spinLock(Conts.Lock.MAX_ONLINE_USER_CHECK_KEY, Conts.Lock.MAX_ONLINE_USER_CHECK_EXPIRE_TIME, Conts.Lock.MAX_ONLINE_USER_CHECK_APPLY_EXOIRE_TIME, "mgr_user_login_0006::The number of logged users has reached the maximum");
        }

        try{

            //web端
            //同一端的不同浏览器,已经登录,则踢出。同时因为已经登录,所以不需要检测最大在线人数
            Set<String> userDeviceSessionKeys = SessionUtil.getUserRelateOnlineKeys(user.getId(), DeviceType.WEB);
            if(userDeviceSessionKeys != null && userDeviceSessionKeys.size() > 0){
                for(String userDeviceSessionKey : userDeviceSessionKeys){
                    String sessionId = SessionUtil.getSessionId(userDeviceSessionKey);
                    Session redisSession = sessionRepository.getSession(sessionId);
                    if(redisSession != null){
                        redisSession.removeAttribute(HttpRequestConstant.SESSION_USER_INFO);
                        removeSessionAllCaptcha(redisSession);
                        redisRepository.delete(sessionId);
                        deleteExpirations((ExpiringSession) redisSession);
//                        sessionRepository.save(redisSession);
                    }
                    redisTemplate.delete(userDeviceSessionKey);
                }

            }else if(loginNumLimit != null && loginNumLimit >= 0){
                int onlineUserNumber = SessionUtil.getOnlineUserNumber(DeviceType.WEB);
                if(loginNumLimit <= onlineUserNumber){
                    throw new GCloudException("mgr_user_login_0007::The number of logged users has reached the maximum");
                }
            }

            userDao.loginSuccess(user.getId());

            if(session != null){
                session.invalidate();
            }
            session = request.getSession();

            SessionUser sessionUser = new SessionUser();
            sessionUser.setUserId(user.getId());
            sessionUser.setUsername(user.getUsername());
            sessionUser.setDeviceType(DeviceType.WEB);
            session.setAttribute(HttpRequestConstant.SESSION_USER_INFO, sessionUser);

            //登录成功,去掉登录失败次数
            session.removeAttribute(Conts.Session.SESSION_LOGIN_FAIL_TIME);

            String userSessionKey = SessionUtil.getUserDeviceOnlineKey(user.getId(), DeviceType.WEB, session.getId());
            BoundValueOperations<String, Object> valueOperations = redisTemplate.boundValueOps(userSessionKey);
            valueOperations.set(userSessionKey);
            long expireTime = session.getLastAccessedTime() + session.getMaxInactiveInterval() * 1000;
            Date expireDate = new Date(expireTime);
            valueOperations.expireAt(expireDate);

        }catch(Exception ex){

            if(ex instanceof GCloudException){
                throw ex;
            }else{
                log.error("未知错误", ex);
                throw new GCloudException("mgr_user_login_0008::login fail");
            }

        }finally {
            if(StringUtils.isNotBlank(lockId)){
                LockUtil.releaseSpinLock(Conts.Lock.MAX_ONLINE_USER_CHECK_KEY, lockId);
            }
        }

        LoginResponse response = new LoginResponse();
        response.setId(user.getId());
        response.setUsername(user.getUsername());

        return response;
    }

猜你喜欢

转载自blog.csdn.net/qq_35261162/article/details/80745136