package com.gci.boot.modules.system.shiro.authc;

import com.alibaba.excel.util.StringUtils;
import com.gci.boot.common.util.CacheUtil;
import com.gci.boot.common.util.JsonUtil;
import com.gci.boot.common.util.SpringContextUtil;
import com.gci.boot.constant.CacheConstant;
import com.gci.boot.modules.system.entity.SysUser;
import com.gci.boot.modules.system.util.JwtUtil;
import com.gci.boot.modules.system.vo.LoginUser;
import lombok.AllArgsConstructor;
import org.apache.shiro.authc.AuthenticationException;
import org.apache.shiro.authc.AuthenticationInfo;
import org.apache.shiro.authc.AuthenticationToken;
import org.apache.shiro.authc.SimpleAuthenticationInfo;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import com.gci.boot.constant.CommonConstant;
import com.gci.boot.common.util.RedisUtil;
import com.gci.boot.modules.system.service.ISysUserService;
import org.springframework.beans.BeanUtils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.util.Set;

/**
 * @Description: 用户登录鉴权和获取用户授权
 * @Author: Scott
 * @Date: 2019-4-23 8:13
 * @Version: 1.1
 */
@SuppressWarnings("SpringJavaInjectionPointsAutowiringInspection")
@Slf4j
@AllArgsConstructor
@Component
public class ShiroRealm extends AuthorizingRealm {

	private ISysUserService sysUserService;

	private long expireTime;

	/**
	 * 必须重写此方法，不然Shiro会报错
	 */
	@Override
	public boolean supports(AuthenticationToken token) {
		return token instanceof JwtToken;
	}

	/**
	 * 权限信息认证(包括角色以及权限)是用户访问controller的时候才进行验证(redis存储的此处权限信息)
	 * 触发检测用户权限时才会调用此方法，例如checkRole,checkPermission
	 *
	 * @param principals 身份信息
	 * @return AuthorizationInfo 权限信息
	 */
	@Override
	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
		log.info("===============Shiro权限认证开始============ [ roles、permissions]==========");
		if (principals != null) {
			LoginUser sysUser = (LoginUser) principals.getPrimaryPrincipal();
			SimpleAuthorizationInfo info = CacheUtil.get(CacheConstant.SYS_AUTHORIZATION_CACHE, sysUser.getUsername(), SimpleAuthorizationInfo.class);
			if(info == null) {
				info = new SimpleAuthorizationInfo();
				String username = sysUser.getUsername();
				// 设置用户拥有的角色集合，比如“admin,test”
				Set<String> roleSet = sysUserService.getUserRolesSet(username);
				info.setRoles(roleSet);

				// 设置用户拥有的权限集合，比如“sys:role:add,sys:user:add”
				Set<String> permissionSet = sysUserService.getUserPermissionsSet(username);
				info.addStringPermissions(permissionSet);
				//加入缓存
				CacheUtil.put(CacheConstant.SYS_AUTHORIZATION_CACHE, sysUser.getUsername(), info);
			}
			log.info("===============Shiro权限认证成功==============");
			return info;
		}
		return null;
	}

	/**
	 * 用户信息认证是在用户进行登录的时候进行验证
	 * 也就是说验证用户输入的账号和密码是否正确，错误抛出异常
	 *
	 * @param auth 用户登录的账号密码信息
	 * @return 返回封装了用户信息的 AuthenticationInfo 实例
	 * @throws AuthenticationException
	 */
	@Override
	protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth){
		try {
			String token = (String) auth.getCredentials();
			if (token == null) {
				log.info("————————身份认证失败——————————IP地址:  "+ getIpAddrByRequest(SpringContextUtil.getHttpServletRequest()));
				throw new AuthenticationException("token为空!");
			}
			// 校验token有效性
			LoginUser loginUser = this.checkUserTokenIsEffect(token);
			return new SimpleAuthenticationInfo(loginUser, token, getName());
		}  catch (AuthenticationException e) {
			log.error("", e);
			throw new AuthenticationException(e.getMessage());
		}
	}

	private String getIpAddrByRequest(HttpServletRequest request) {
		String ip = request.getHeader("x-forwarded-for");
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getHeader("WL-Proxy-Client-IP");
		}
		if (ip == null || ip.length() == 0 || "unknown".equalsIgnoreCase(ip)) {
			ip = request.getRemoteAddr();
		}
		return ip;
	}
	/**
	 * 校验token的有效性
	 *
	 * @param token
	 */
	public LoginUser checkUserTokenIsEffect(String token) throws AuthenticationException {
		// 解密获得username，用于和数据库进行对比
		String username = JwtUtil.getUsername(token);
		if (username == null) {
			throw new AuthenticationException("token非法无效!");
		}

		// 查询用户信息
		log.debug("———校验token是否有效————checkUserTokenIsEffect——————— "+ token);
		SysUser user = CacheUtil.get(CacheConstant.SYS_USERS_CACHE, username, SysUser.class);
		if(user == null){
			user = sysUserService.getByUsername(username);
			CacheUtil.put(CacheConstant.SYS_USERS_CACHE, username, user);
		}
		if (user == null) {
			throw new AuthenticationException("用户不存在!");
		}
		// 判断用户状态
		if (user.getStatus() != 1) {
			throw new AuthenticationException("账号已被锁定,请联系管理员!");
		}
		// 校验token是否超时失效 & 或者账号密码是否错误
		jwtTokenRefresh(token, username, user.getPassword());
		LoginUser loginUser = new LoginUser();
		BeanUtils.copyProperties(user, loginUser);
		return loginUser;
	}

	/**
	 * JWTToken刷新生命周期 （实现： 用户在线操作不掉线功能）
	 * 1、登录成功后将用户的JWT生成的Token作为k、v存储到cache缓存里面(这时候k、v值一样)，缓存有效期设置为Jwt有效时间的2倍
	 * 2、当该用户再次请求时，通过JWTFilter层层校验之后会进入到doGetAuthenticationInfo进行身份验证
	 * 3、当该用户这次请求jwt生成的token值已经超时，但该token对应cache中的k还是存在，则表示该用户一直在操作只是JWT的token失效了，程序会给token对应的k映射的v值重新生成JWTToken并覆盖v值，该缓存生命周期重新计算
	 * 4、当该用户这次请求jwt在生成的token值已经超时，并在cache中不存在对应的k，则表示该用户账户空闲超时，返回用户信息已失效，请重新登录。
	 * 注意： 前端请求Header中设置Authorization保持不变，校验有效性以缓存中的token为准。
	 *       用户过期时间 = Jwt有效时间 * 2。
	 *
	 * @return
	 */
	private void jwtTokenRefresh(String token, String username, String secret) {
		String cacheToken = String.valueOf(RedisUtil.get(CommonConstant.PREFIX_USER_TOKEN + token));
		//获取携带的token是否存在缓存，如果不存在，说明token过期,需要判断是自然失效还是互斥影响失效
		if(StringUtils.isBlank(cacheToken) || "null".equalsIgnoreCase(cacheToken)){
			//获取该用户最近登陆保存的token.
			String pcToken = String.valueOf(RedisUtil.get(CommonConstant.PREFIX_PC_USER + username));
			//如果不存在pc-token 或者
			if((StringUtils.isNotBlank(pcToken) || "null".equalsIgnoreCase(pcToken)) && !pcToken.equals(token)){
				throw new AuthenticationException("您的账号已在其他设备登录！");
			}else{
				throw new AuthenticationException("Token失效，请重新登录!");
			}
		}
		//如果存在且未超过有效期
		// 如果token未超过有效期，重设redis过期时间
		if (JwtUtil.verify(token, username)) {
			RedisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, expireTime / 1000);
		} else {//如果token超过有效期，刷新token
			String newToken = JwtUtil.sign(username, secret);
			RedisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, newToken, expireTime / 1000);
		}
		log.debug("——————————用户在线操作，更新token保证不掉线—————————jwtTokenRefresh——————— " + token);
	}

	/**
	 * 清除当前用户的权限认证缓存
	 *
	 * @param principals 权限信息
	 */
	@Override
	public void clearCache(PrincipalCollection principals) {
		super.clearCache(principals);
	}

}
