package com.persagy.account.controller; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.TimeUnit; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper; import com.persagy.account.config.ApplicationProperties; import com.persagy.account.manage.SaasAuthHandler; import com.persagy.account.manage.SaasRoleMenuHandler; import com.persagy.account.pojo.dto.SaasAccount; import com.persagy.account.pojo.dto.SaasGroup; import com.persagy.account.pojo.dto.SaasMenu; import com.persagy.account.pojo.vo.account.SaasAccountLoginVO; import com.persagy.account.pojo.vo.account.SaasAccountQueryVO; import com.persagy.account.pojo.vo.menu.SaasMenuQueryVO; import com.persagy.account.service.ISaasAccountService; import com.persagy.account.service.ISaasGroupService; import com.persagy.common.constant.SaasCommonConstant; import com.persagy.common.enums.ResponseCode; import com.persagy.common.exception.BusinessException; import com.persagy.common.utils.ResponseResult; import com.persagy.common.utils.ResponseResultUtil; import com.persagy.common.utils.StringUtil; import com.persagy.log.core.util.RequestUtil; import com.persagy.security.constant.CipherConstants; import com.persagy.security.util.BouncycastleCipher; import com.persagy.security.util.SecureAES; import cn.hutool.core.collection.CollectionUtil; import cn.hutool.crypto.digest.DigestAlgorithm; import cn.hutool.http.HttpUtil; import io.swagger.annotations.Api; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; /** * 登录 * * @version 1.0.0 * @company persagy * @author zhangqiankun * @date 2021-03-13 15:29:50 */ @Slf4j @Api(tags = "账号信息") @RestController public class SaasLoginController { private static final String HTTP_PREFIX = "http://"; private static final String HTTPS_PREFIX = "https://"; @Autowired private SaasAuthHandler saasAuthHandler; @Autowired private ApplicationProperties properties; @Autowired private ISaasGroupService saasGroupService; @Autowired private BouncycastleCipher bouncycastleCipher; @Autowired private ISaasAccountService saasAccountService; @Autowired private SaasRoleMenuHandler saasRoleMenuHandler; @Autowired private RedisTemplate<String, Object> redisTemplate; /** * 运维系统账号登录 * * @throws NumberFormatException * @throws IOException */ @ApiOperation(value = "运维系统账号登录") @RequestMapping(value = "saas/login", method = RequestMethod.POST) public ResponseResult saasAccountLogin(@RequestBody @Validated SaasAccountLoginVO loginVO, HttpServletRequest request, HttpServletResponse response) throws NumberFormatException, IOException { String redirectUrl = this.validSaasSSO(loginVO); LambdaQueryWrapper<SaasAccount> queryWrapper = new SaasAccount.Builder().createQueryWrapper().usernameEq(loginVO.getUsername()).passwordEq(loginVO.getPassword()).builderQueryWrapper(); List<SaasAccount> accounts = this.saasAccountService.list(queryWrapper); if (CollectionUtil.isEmpty(accounts) || accounts.size() > 1) { return ResponseResultUtil.errorResult(ResponseCode.A0220.getCode(), "用户名/密码错误"); } SaasAccount account = accounts.get(0); // 验证此账号是否可用 if (SaasCommonConstant.STATUS_1 != account.getValid()) { return ResponseResultUtil.errorResult(ResponseCode.A0220.getCode(), "此账号已停用"); } /*if (!account.getTerminal().contains(loginVO.getAppId())) { return ResponseResultUtil.errorResult(ResponseCode.A0220.getCode(), "此账号不允许在此端登录"); }*/ // 验证是否在有效期内 if (SaasCommonConstant.STR_STATUS_0.equals(account.getValidLast())) { Date date = new Date(); Date validEndTime = account.getValidEndTime(); if (date.after(validEndTime)) { return ResponseResultUtil.errorResult(ResponseCode.A0220.getCode(), "此账号已停用"); } } // 如果是业务系统账号,集团编码必须存在 if (SaasCommonConstant.STR_STATUS_1.equals(account.getAccountBelong()) && StringUtil.isBlank(account.getGroupCode())) { return ResponseResultUtil.errorResult(ResponseCode.C0341.getCode(), "业务系统账号,集团编码不可为空"); } // 获取集团名称 if (StringUtil.isNotBlank(account.getGroupCode())) { SaasGroup saasGroup = this.saasGroupService.getOne(account.getGroupCode()); account.setGroupName(saasGroup == null ? null : saasGroup.getGroupName()); } // 更新账号登录时间,及IP String address = RequestUtil.getIpAddress(request); LambdaUpdateWrapper<SaasAccount> updateWrapper = new SaasAccount.Builder().createUpdateWrapper() .idEq(account.getId()).groupCodeEq(account.getGroupCode()) .validEq(SaasCommonConstant.STATUS_1).builderUpdateWrapper(); updateWrapper.set(SaasAccount::getLastLoginTime, new Date()); updateWrapper.set(SaasAccount::getLastLoginIp, address); boolean result = this.saasAccountService.update(updateWrapper); if (!result) { return ResponseResultUtil.errorResult("登录信息更新失败"); } account.setPassword(null); // 判断是否需要获取菜单权限树 if (loginVO.isAuths()) { List<SaasMenu> menuTree = this.querySaasMenuTree(account, loginVO.getAppId()); account.setAuths(menuTree); } account.setAppId(loginVO.getAppId()); String token = this.setRedisToken(account, loginVO.getAppId(), loginVO.isRemember(), false); // 判断是否需要重定向 if (StringUtil.isNotBlank(redirectUrl)) { String code = this.saasAuthHandler.getCode(loginVO.getClientId(), loginVO.getRedirectUrl(), account.getId(), account.getGroupCode(), loginVO.getAppId()); redirectUrl = redirectUrl + "&code=" + code; Map<String, String> temp = new HashMap<String, String>(); temp.put("redirectUrl", redirectUrl); return ResponseResultUtil.successResult(temp); } response.setHeader(CipherConstants.TOKEN_HEADER, token); return ResponseResultUtil.successResult(account); } /** * 能够进来,说明已经经过token验证,且通过,查询出账号信息,并返回即可 * * @param queryVO * @return * @throws NumberFormatException * @throws UnsupportedEncodingException */ @ApiOperation(value = "记住登录") @RequestMapping(value = "saas/remember", method = RequestMethod.POST) public ResponseResult saasAccountLogin(@RequestBody @Validated SaasAccountQueryVO queryVO, HttpServletRequest request, HttpServletResponse response) throws NumberFormatException, UnsupportedEncodingException { SaasAccount account = this.saasAccountService.getOne(queryVO.getAccountId(), null, queryVO.getUsername()); if (account == null) { return ResponseResultUtil.errorResult(ResponseCode.A0402.getCode(), "账号信息不存在"); } // 获取集团名称 if (StringUtil.isNotBlank(account.getGroupCode())) { SaasGroup saasGroup = this.saasGroupService.getOne(account.getGroupCode()); account.setGroupName(saasGroup == null ? null : saasGroup.getGroupName()); } String token = this.setRedisToken(account, queryVO.getAppId(), true, true); response.setHeader(CipherConstants.TOKEN_HEADER, token); return ResponseResultUtil.successResult(account); } /** * 退出登录,清空token,因为网关以确保token值必须于此账号,这里直接清空 * * @param queryVO * @return * @throws NumberFormatException * @throws UnsupportedEncodingException */ @ApiOperation(value = "退出登录") @RequestMapping(value = "account/logout", method = RequestMethod.POST) public ResponseResult accountLogout(@RequestBody @Validated SaasAccountQueryVO queryVO) throws NumberFormatException, UnsupportedEncodingException { if (StringUtil.isBlank(queryVO.getAccountId())) { return ResponseResultUtil.errorResult(ResponseCode.A0402.getCode(), "账号ID,不可为空"); } // 从redis中清空此账号的sign信息 boolean remove = this.bouncycastleCipher.remove(queryVO.getAccountId()); return remove ? ResponseResultUtil.successResult("退出成功") : ResponseResultUtil.errorResult("退出失败"); } /** * 外部账号登录验证 * @return */ public boolean validExternalAccount(String accountSource) { if ("201".equals(accountSource)) { // 获取code String url = "http://sso4dev.wanda-dev.cn/oauth2/Authorize?state=53f2495d7b435ac571&client_id=GRP999&response_type=code&scope=openid%20profile&redirect_uri=https://develop.persagy.com/saasframe3/login"; Map<String, Object> params = new HashMap<String, Object>(); params.put("code", "code"); params.put("client_id", "client_id"); params.put("client_secret", "client_secret"); params.put("redirect_uri", "redirect_uri"); params.put("grant_type", "authorization_code"); String post = HttpUtil.post(url, params, 60000); log.info(post); // 获取token url = "http://sso4dev.wanda-dev.cn/oauth2/GetToken"; params = new HashMap<String, Object>(); params.put("code", "code"); params.put("client_id", "client_id"); params.put("client_secret", "client_secret"); params.put("redirect_uri", "redirect_uri"); params.put("grant_type", "authorization_code"); post = HttpUtil.post(url, params, 60000); log.info(post); // 根据token获取用户信息 url = "http://sso4dev.wanda-dev.cn/oauth2/GetUserInfo"; post = HttpUtil.get(url, 60000); log.info(post); } return true; } /** * 验证是否需要重定向 * @param loginVO * @return 为空时,说明不需要 * @throws UnsupportedEncodingException */ private String validSaasSSO(SaasAccountLoginVO loginVO) throws UnsupportedEncodingException { String state = loginVO.getState(); String clientId = loginVO.getClientId(); String redirectUrl = loginVO.getRedirectUrl(); if (StringUtil.isAllBlank(state, clientId, redirectUrl)) { return null; } if (StringUtil.isBlank(clientId)) { throw new BusinessException(ResponseCode.A0402.getCode(), "客户端ID,不可为空"); } if (StringUtil.isBlank(redirectUrl)) { throw new BusinessException(ResponseCode.A0402.getCode(), "重定向URL,不可为空"); } // 验证clientId,是否存在 // 验证redirectUrl的有效性 if (redirectUrl.indexOf(HTTP_PREFIX) >= 0 || redirectUrl.indexOf(HTTPS_PREFIX) >= 0) { redirectUrl = URLEncoder.encode(redirectUrl, StandardCharsets.UTF_8.name()); if (redirectUrl.indexOf("?") > 0) { redirectUrl = redirectUrl + "&clientId=" + clientId + "&state=" + state; } else { redirectUrl = redirectUrl + "?clientId=" + clientId + "&state=" + state; } return redirectUrl; } throw new BusinessException(ResponseCode.A0402.getCode(), "重定向URL,格式错误"); } /** * 获取此账号的菜单权限树 * * @param account * @param loginApp * @return */ private List<SaasMenu> querySaasMenuTree(SaasAccount account, String loginApp) { SaasMenuQueryVO queryVO = new SaasMenuQueryVO(); queryVO.setAccountId(account.getId()); queryVO.setGroupCode(account.getGroupCode()); queryVO.setAppId(loginApp); List<SaasMenu> menuTree = this.saasRoleMenuHandler.querySaasMenuTree(queryVO); return menuTree; } /** * 设置此账号的token,存入redis,如果redis中已存在,只追加有效期 * @param account * @param loginApp * @param remember * @param isEqual 是否验证一致 * @return * @throws UnsupportedEncodingException */ private String setRedisToken(SaasAccount account, String loginApp, boolean remember, boolean isEqual) throws UnsupportedEncodingException { // 生成token,并验证之前登录是否为记住登录 SecureAES aes = new SecureAES(properties.getAesKey(), properties.getAesIv()); // 对称性加密用户信息 String data = aes.encryptAccount(this.tokenAccountInfo(account, loginApp, remember)); // token存在判断 String sign = (String)this.redisTemplate.opsForValue().get(account.getId()); if (sign != null) { if (isEqual) { // 判断是否与redis中一致,不一致,返回错误信息 boolean result = this.bouncycastleCipher.isEqual(sign, account.getId()); if (!result) { throw new BusinessException(ResponseCode.A0301.getCode(), ResponseCode.A0301.getDesc()); } } this.redisTemplate.expire(account.getId(), Long.parseLong(this.properties.getTokenExpire()), TimeUnit.SECONDS); } else { // 加密串MD5加密转为sign,redis:key-账号ID,value-sign(data-MD5加密后) sign = this.bouncycastleCipher.encrypt(this.tokenInfo(data, loginApp), account.getId(), DigestAlgorithm.MD5, Long.parseLong(this.properties.getTokenExpire()), TimeUnit.SECONDS); } // token=data.sign String token = data + CipherConstants.POINT_JOIN_SYMBOL + sign; return token; } /** * 账号信息 * * @param saasAccount * @param loginApp * @param remember * @return */ private JSONObject tokenAccountInfo(SaasAccount saasAccount, String loginApp, boolean remember) { JSONObject object = new JSONObject(); object.put(SaasCommonConstant.REMEMBER, remember); object.put(CipherConstants.APP_ID, loginApp); object.put(CipherConstants.GROUP_CODE, saasAccount.getGroupCode()); object.put(CipherConstants.ACCOUNT_ID, saasAccount.getId()); return object; } /** * 账号信息 * * @param groupCode * @param loginApp * @return */ private String tokenInfo(String content, String loginApp) { JSONObject object = new JSONObject(); object.put(CipherConstants.APP_ID, loginApp); object.put("date", System.currentTimeMillis()); object.put("content", content); return object.toJSONString(); } }