package com.gihon.sso.service.impl; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Service; import org.springframework.util.Base64Utils; import org.springframework.util.StringUtils; import com.fasterxml.jackson.core.type.TypeReference; import com.gihon.common.auth.AuthConstans; import com.gihon.common.entity.GihonRole; import com.gihon.common.enums.ModuleType; import com.gihon.common.properties.GihonCommonProperties; import com.gihon.common.properties.RedisConstants; import com.gihon.common.util.JacksonJsonUtils; import com.gihon.common.util.UUIDGenerater; import com.gihon.sso.entity.vo.LoginUser; import com.gihon.sso.entity.vo.RefreshTokenVal; import com.gihon.sso.entity.vo.SsoToken; import com.gihon.sso.entity.vo.TokenVal; import com.gihon.sso.entity.vo.UserInfo; import com.gihon.sso.service.GihonRoleService; import com.gihon.sso.service.LoginUserService; import com.gihon.sso.service.TokenService; import lombok.extern.slf4j.Slf4j; /** * TODO 清理token和refreshToken 需要分开 最好TokenStore:有各种实现,类似Cas中的统一认证中心 * 与SecurityTokenServiceImpl互斥不要同时出现 * @author baihe * */ @ConditionalOnMissingClass("org.springframework.security.core.userdetails.UserDetails") @Slf4j @Service("tokenService") @EnableConfigurationProperties(GihonCommonProperties.class) public class TokenServiceImpl implements TokenService { @Autowired protected LoginUserService loginUserService; @Autowired protected StringRedisTemplate stringRedisTemplate; @Autowired protected GihonCommonProperties gihonCommonProperties; @Autowired protected GihonRoleService gihonRoleService; protected String parseToken(String originToken) { return Base64Utils.encodeToUrlSafeString(originToken.getBytes()); } @Override public String getUserName(String securityToken) { String token = new String(Base64Utils.decodeFromUrlSafeString(securityToken)); return token.split(RedisConstants.SEP)[0]; } @Override public SsoToken createToken(UserInfo userInfo, int moduleType) { if (userInfo == null || !StringUtils.hasText(userInfo.getUsername())) { return null; } ModuleType type = ModuleType.parse(moduleType); boolean mutiLogin = gihonCommonProperties.isMutiLogin(); //暂时只有WEB端互踢 if (type != ModuleType.WEB) { mutiLogin = false; } // 超级管理员只允许WEB登录并且互踢 if (userInfo.getUsername().equals(AuthConstans.ADMIN)) { if (type != ModuleType.WEB) { return null; } mutiLogin = false; } SsoToken tokenEntity = new SsoToken(); tokenEntity.setModuleType(moduleType); tokenEntity.setUserInfo(userInfo); tokenEntity.setToken(parseToken(userInfo.getUsername() + RedisConstants.SEP + UUIDGenerater.genUUID())); tokenEntity.setRefreshToken(parseToken(userInfo.getUsername() + RedisConstants.SEP + UUIDGenerater.genUUID())); long now = System.currentTimeMillis(); tokenEntity.setRefreshTokenExpire(now + gihonCommonProperties.getRefreshTokenExpired()); tokenEntity.setTokenExpire(now + gihonCommonProperties.getTokenExpired()); saveToken(tokenEntity, mutiLogin); return tokenEntity; } @Override public SsoToken refreshToken(String refreshToken) { String rtvJsonStr = stringRedisTemplate.opsForValue().get(REFRESH_TOKEN_PRE + refreshToken); if (!StringUtils.hasText(rtvJsonStr)) { log.warn("refresh_token({})已经失效", refreshToken); return null; } RefreshTokenVal rtv = JacksonJsonUtils.readObject(rtvJsonStr, RefreshTokenVal.class); if (rtv == null) { log.warn("refresh_token({})解析失败({})", refreshToken, rtvJsonStr); return null; } // 获取tokenEntity,通过账号获取账号信息 UserInfo userInfo = this.getUserInfo(rtv.getUserAccount()); long now = System.currentTimeMillis(); // 主动删除refresh 和 token TokenVal tv = TokenVal.builder().expireTime(rtv.getExpireTime()).userAccount(rtv.getUserAccount()).refreshToken(refreshToken).token(rtv.getToken()) .moduleType(rtv.getModuleType()).build(); this.removeToken(tv); if (rtv.getExpireTime() < now) { log.warn("refresh_token({})已经过期({})", refreshToken, rtv.getExpireTime()); return null; } SsoToken tokenEntityNew = this.createToken(userInfo, rtv.getModuleType()); // 发送通知更新Token notifyRefreshToken(rtv, tokenEntityNew); return tokenEntityNew; } @Override public TokenVal getTokenVal(String token) { String tvJsonStr = stringRedisTemplate.opsForValue().get(TOKEN_PRE + token); TokenVal tokenEntity = null; if (StringUtils.hasText(tvJsonStr)) { tokenEntity = JacksonJsonUtils.readObject(tvJsonStr, TokenVal.class); } return tokenEntity; } @Override public List getTokenByAccount(String account) { String value = (String) stringRedisTemplate.opsForHash().get(USER_TOKEN_STORE, account); List list = null; if (StringUtils.hasText(value)) { TypeReference> type = new TypeReference>() { }; list = JacksonJsonUtils.readObject(value, type); } return list; } @Override public void clearToken(String token) { String tvJsonStr = stringRedisTemplate.opsForValue().get(TOKEN_PRE + token); TokenVal tokenEntity = null; if (StringUtils.hasText(tvJsonStr)) { tokenEntity = JacksonJsonUtils.readObject(tvJsonStr, TokenVal.class); } else { log.warn("{} token is missing", token); return; } if (tokenEntity != null) { this.removeToken(tokenEntity); } } @Override public void autoClearRefreshToken(String refreshToken, String account) { List tl = getTokenByAccount(account); if (tl == null || tl.isEmpty()) { return; } Optional a = tl.stream().filter(t -> t.getRefreshToken().equals(refreshToken)).findFirst(); if (a.isPresent()) { this.removeTokenList(a.get(), tl); } } @Override public UserInfo checkToken(LoginUser loginUser) { return checkTokenInfo(loginUser.getToken()); } @Override public UserInfo checkTokenInfo(String token) { String tokenValue = stringRedisTemplate.opsForValue().get(TOKEN_PRE + token); TokenVal tokenEntity = null; if (StringUtils.hasText(tokenValue)) { tokenEntity = JacksonJsonUtils.readObject(tokenValue, TokenVal.class); if (tokenEntity != null) { return this.getUserInfo(tokenEntity.getUserAccount()); } else { stringRedisTemplate.delete(TOKEN_PRE + token); } } return null; } /** * 从UserStore中获取用户基本信息 TODO 定时刷新或者AOP通知 * * @param account * @return */ public UserInfo getUserInfo(String account) { String userAccount = (String) stringRedisTemplate.opsForHash().get(USER_STORE, account); UserInfo userInfo = null; long now = System.currentTimeMillis(); if (StringUtils.hasText(userAccount)) { userInfo = JacksonJsonUtils.readObject(userAccount, UserInfo.class); if((userInfo.getCreatedTime() + gihonCommonProperties.getRefreshTokenExpired()) >= now) {//超时重新获取,有AOP同步账号信息 userInfo = null; } } if(userInfo==null) { userInfo = loginUserService.queryUserByUserAccount(account); if (userInfo == null) { return null; } // add roleList; List rl = gihonRoleService.getRoleList(userInfo.getId()); userInfo.setRoleList(rl.stream().map(r -> r.getCompanyId()+RedisConstants.SEP+r.getRoleCode()).collect(Collectors.toList())); userInfo.setCreatedTime(now); stringRedisTemplate.opsForHash().put(USER_STORE, account, JacksonJsonUtils.writeObject(userInfo)); } return userInfo; } private void saveToken(SsoToken tokenEntity, boolean mutiLogin) { String userAccount = tokenEntity.getUserInfo().getUsername(); List tokenList = getTokenByAccount(userAccount); if (tokenList == null) { tokenList = new ArrayList(); } if (!mutiLogin) { // 强制踢掉其他同类型的token List removeList = tokenList.stream().filter(t -> t.getModuleType() == tokenEntity.getModuleType()).collect(Collectors.toList()); //此处只删除token,不回写List removeList.forEach(t -> removeToken(t)); tokenList.removeAll(removeList); } TokenVal tv = TokenVal.builder().expireTime(tokenEntity.getTokenExpire()).userAccount(userAccount).refreshToken(tokenEntity.getRefreshToken()) .token(tokenEntity.getToken()).moduleType(tokenEntity.getModuleType()).build(); RefreshTokenVal rtv = RefreshTokenVal.builder().expireTime(tokenEntity.getTokenExpire()).userAccount(userAccount) .refreshToken(tokenEntity.getRefreshToken()).token(tokenEntity.getToken()).moduleType(tokenEntity.getModuleType()).build(); tokenList.remove(tv); tokenList.add(tv); boolean flag = stringRedisTemplate.opsForHash().hasKey(USER_STORE, userAccount); if (!flag) { stringRedisTemplate.opsForHash().putIfAbsent(USER_STORE, userAccount, JacksonJsonUtils.writeObject(tokenEntity.getUserInfo())); } stringRedisTemplate.opsForValue().set(TOKEN_PRE + tokenEntity.getToken(), JacksonJsonUtils.writeObject(tv), gihonCommonProperties.getTokenExpired() + gihonCommonProperties.getExipredMore(), TimeUnit.MILLISECONDS); stringRedisTemplate.opsForValue().set(REFRESH_TOKEN_PRE + tokenEntity.getRefreshToken(), JacksonJsonUtils.writeObject(rtv), gihonCommonProperties.getRefreshTokenExpired() + gihonCommonProperties.getExipredMore(), TimeUnit.MILLISECONDS); stringRedisTemplate.opsForHash().put(USER_TOKEN_STORE, userAccount, JacksonJsonUtils.writeObject(tokenList)); } // 移除Redis中token private void removeTokenToStore(String userAccount, TokenVal tokenVal, List tokenList) { if (tokenList != null) { tokenList.remove(tokenVal); } stringRedisTemplate.opsForHash().put(USER_TOKEN_STORE, userAccount, JacksonJsonUtils.writeObject(tokenList)); } // 移除Redis中token private void removeToken(TokenVal tokenVal) { stringRedisTemplate.delete(REFRESH_TOKEN_PRE + tokenVal.getRefreshToken()); stringRedisTemplate.delete(TOKEN_PRE + tokenVal.getToken()); notifyToken(tokenVal); } // 移除Redis中token 多线程有问题 private void removeTokenList(TokenVal tokenVal, List tokenList) { removeToken(tokenVal); String userAccount = tokenVal.getUserAccount(); removeTokenToStore(userAccount, tokenVal, tokenList); } /** * 通知其他系统Token失效,使用同一个Token的时候有用。需要其他系统重新登录 * * @param tokenEntity */ private void notifyToken(TokenVal tokenEntity) { // TODO return; } /** * 通知各个系统refreshToken,需要其他系统重新登录 * * @param tokenEntity * @param tokenEntityNew */ private void notifyRefreshToken(RefreshTokenVal rtv, SsoToken tokenEntityNew) { // TODO return; } }