package org.jeecg.modules.system.controller; import com.alibaba.fastjson.JSONObject; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.xkcoding.justauth.AuthRequestFactory; import io.swagger.annotations.ApiOperation; import lombok.extern.slf4j.Slf4j; import me.zhyd.oauth.model.AuthCallback; import me.zhyd.oauth.model.AuthResponse; import me.zhyd.oauth.request.AuthRequest; import me.zhyd.oauth.utils.AuthStateUtils; import org.apache.shiro.SecurityUtils; import org.jeecg.common.api.vo.Result; import org.jeecg.common.constant.CommonConstant; import org.jeecg.common.system.util.JwtUtil; import org.jeecg.common.util.PasswordUtil; import org.jeecg.common.util.RedisUtil; import org.jeecg.common.util.RestUtil; import org.jeecg.common.util.oConvertUtils; import org.jeecg.config.shiro.JwtToken; import org.jeecg.config.thirdapp.ThirdAppConfig; import org.jeecg.config.thirdapp.ThirdAppTypeItemVo; import org.jeecg.modules.base.service.BaseCommonService; import org.jeecg.modules.system.entity.SysThirdAccount; import org.jeecg.modules.system.entity.SysUser; import org.jeecg.modules.system.model.ThirdLoginModel; import org.jeecg.modules.system.service.ISysThirdAccountService; import org.jeecg.modules.system.service.ISysUserService; import org.jeecg.modules.system.service.impl.ThirdAppDingtalkServiceImpl; import org.jeecg.modules.system.service.impl.ThirdAppWechatEnterpriseServiceImpl; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.*; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.URLEncoder; import java.util.List; /** * @Author scott * @since 2018-12-17 */ @Controller @RequestMapping("/sys/thirdLogin") @Slf4j public class ThirdLoginController { @Autowired private ISysUserService sysUserService; @Autowired private ISysThirdAccountService sysThirdAccountService; @Autowired private BaseCommonService baseCommonService; @Autowired private RedisUtil redisUtil; @Autowired private AuthRequestFactory factory; @Autowired ThirdAppConfig thirdAppConfig; @Autowired private ThirdAppWechatEnterpriseServiceImpl thirdAppWechatEnterpriseService; @Autowired private ThirdAppDingtalkServiceImpl thirdAppDingtalkService; /**token有效时间,目前现场要求企业微信有效时间*/ public static final long EXPIRE_TIME = 30L * 24 * 60 * 60 * 1000; @RequestMapping("/render/{source}") public void render(@PathVariable("source") String source, HttpServletResponse response) throws IOException { log.info("第三方登录进入render:" + source); AuthRequest authRequest = factory.get(source); String authorizeUrl = authRequest.authorize(AuthStateUtils.createState()); log.info("第三方登录认证地址:" + authorizeUrl); response.sendRedirect(authorizeUrl); } @RequestMapping("/{source}/callback") public String loginThird(@PathVariable("source") String source, AuthCallback callback,ModelMap modelMap) { log.info("第三方登录进入callback:" + source + " params:" + JSONObject.toJSONString(callback)); AuthRequest authRequest = factory.get(source); AuthResponse response = authRequest.login(callback); log.info(JSONObject.toJSONString(response)); Result result = new Result(); if(response.getCode()==2000) { JSONObject data = JSONObject.parseObject(JSONObject.toJSONString(response.getData())); String username = data.getString("username"); String avatar = data.getString("avatar"); String uuid = data.getString("uuid"); //构造第三方登录信息存储对象 ThirdLoginModel tlm = new ThirdLoginModel(source, uuid, username, avatar); //判断有没有这个人 //update-begin-author:wangshuai date:20201118 for:修改成查询第三方账户表 LambdaQueryWrapper query = new LambdaQueryWrapper(); query.eq(SysThirdAccount::getThirdUserUuid, uuid); query.eq(SysThirdAccount::getThirdType, source); List thridList = sysThirdAccountService.list(query); SysThirdAccount user = null; if(thridList==null || thridList.size()==0) { //否则直接创建新账号 user = sysThirdAccountService.saveThirdUser(tlm); }else { //已存在 只设置用户名 不设置头像 user = thridList.get(0); } // 生成token //update-begin-author:wangshuai date:20201118 for:从第三方登录查询是否存在用户id,不存在绑定手机号 if(oConvertUtils.isNotEmpty(user.getSysUserId())) { String sysUserId = user.getSysUserId(); SysUser sysUser = sysUserService.getById(sysUserId); String token = saveToken(sysUser); // 使用token进行Shiro登录 JwtToken jwtToken = new JwtToken(token); SecurityUtils.getSubject().login(jwtToken); // 此行代码会触发Realm的认证方法,将用户信息存入Shiro的会话 modelMap.addAttribute("token", token); }else{ modelMap.addAttribute("token", "绑定手机号,"+""+uuid); } //update-end-author:wangshuai date:20201118 for:从第三方登录查询是否存在用户id,不存在绑定手机号 //update-begin--Author:wangshuai Date:20200729 for:接口在签名校验失败时返回失败的标识码 issues#1441-------------------- }else{ modelMap.addAttribute("token", "登录失败"); } //update-end--Author:wangshuai Date:20200729 for:接口在签名校验失败时返回失败的标识码 issues#1441-------------------- result.setSuccess(false); result.setMessage("第三方登录异常,请联系管理员"); return "thirdLogin"; } /** * 创建新账号 * @param model * @return */ @PostMapping("/user/create") @ResponseBody public Result thirdUserCreate(@RequestBody ThirdLoginModel model) { log.info("第三方登录创建新账号:" ); Result res = new Result<>(); Object operateCode = redisUtil.get(CommonConstant.THIRD_LOGIN_CODE); if(operateCode==null || !operateCode.toString().equals(model.getOperateCode())){ res.setSuccess(false); res.setMessage("校验失败"); return res; } //创建新账号 //update-begin-author:wangshuai date:20201118 for:修改成从第三方登录查出来的user_id,在查询用户表尽行token SysThirdAccount user = sysThirdAccountService.saveThirdUser(model); if(oConvertUtils.isNotEmpty(user.getSysUserId())){ String sysUserId = user.getSysUserId(); SysUser sysUser = sysUserService.getById(sysUserId); // 生成token String token = saveToken(sysUser); //update-end-author:wangshuai date:20201118 for:修改成从第三方登录查出来的user_id,在查询用户表尽行token res.setResult(token); res.setSuccess(true); } return res; } /** * 绑定账号 需要设置密码 需要走一遍校验 * @param json * @return */ @PostMapping("/user/checkPassword") @ResponseBody public Result checkPassword(@RequestBody JSONObject json) { Result result = new Result<>(); Object operateCode = redisUtil.get(CommonConstant.THIRD_LOGIN_CODE); if(operateCode==null || !operateCode.toString().equals(json.getString("operateCode"))){ result.setSuccess(false); result.setMessage("校验失败"); return result; } String username = json.getString("uuid"); SysUser user = this.sysUserService.getUserByName(username); if(user==null){ result.setMessage("用户未找到"); result.setSuccess(false); return result; } String password = json.getString("password"); String salt = user.getSalt(); String passwordEncode = PasswordUtil.encrypt(user.getUsername(), password, salt); if(!passwordEncode.equals(user.getPassword())){ result.setMessage("密码不正确"); result.setSuccess(false); return result; } sysUserService.updateById(user); result.setSuccess(true); // 生成token String token = saveToken(user); result.setResult(token); return result; } private String saveToken(SysUser user) { // 生成token String token = JwtUtil.sign(user.getUsername(), user.getPassword()); redisUtil.set(CommonConstant.PREFIX_USER_TOKEN + token, token); // 设置超时时间 redisUtil.expire(CommonConstant.PREFIX_USER_TOKEN + token, EXPIRE_TIME / 1000); return token; } /** * 第三方登录回调接口 * @param token * @param thirdType * @return * @throws Exception */ @SuppressWarnings("unchecked") @RequestMapping(value = "/getLoginUser/{token}/{thirdType}", method = RequestMethod.GET) @ResponseBody public Result getThirdLoginUser(@PathVariable("token") String token,@PathVariable("thirdType") String thirdType) throws Exception { Result result = new Result(); String username = JwtUtil.getUsername(token); //1. 校验用户是否有效 SysUser sysUser = sysUserService.getUserByName(username); result = sysUserService.checkUserIsEffective(sysUser); if(!result.isSuccess()) { return result; } //update-begin-author:wangshuai date:20201118 for:如果真实姓名和头像不存在就取第三方登录的 LambdaQueryWrapper query = new LambdaQueryWrapper<>(); query.eq(SysThirdAccount::getSysUserId,sysUser.getId()); query.eq(SysThirdAccount::getThirdType,thirdType); SysThirdAccount account = sysThirdAccountService.getOne(query); if(oConvertUtils.isEmpty(sysUser.getRealname())){ sysUser.setRealname(account.getRealname()); } if(oConvertUtils.isEmpty(sysUser.getAvatar())){ sysUser.setAvatar(account.getAvatar()); } //update-end-author:wangshuai date:20201118 for:如果真实姓名和头像不存在就取第三方登录的 JSONObject obj = new JSONObject(); //用户登录信息 obj.put("userInfo", sysUser); //token 信息 obj.put("token", token); result.setResult(obj); result.setSuccess(true); result.setCode(200); baseCommonService.addLog("用户名: " + username + ",登录成功[第三方用户]!", CommonConstant.LOG_TYPE_1, null); return result; } /** * 第三方绑定手机号返回token * * @param jsonObject * @return */ @ApiOperation("手机号登录接口") @PostMapping("/bindingThirdPhone") @ResponseBody public Result bindingThirdPhone(@RequestBody JSONObject jsonObject) { Result result = new Result(); String phone = jsonObject.getString("mobile"); String thirdUserUuid = jsonObject.getString("thirdUserUuid"); // 校验验证码 String captcha = jsonObject.getString("captcha"); //update-begin-author:taoyan date:2022-9-13 for: VUEN-2245 【漏洞】发现新漏洞待处理20220906 String redisKey = CommonConstant.PHONE_REDIS_KEY_PRE+phone; Object captchaCache = redisUtil.get(redisKey); //update-end-author:taoyan date:2022-9-13 for: VUEN-2245 【漏洞】发现新漏洞待处理20220906 if (oConvertUtils.isEmpty(captcha) || !captcha.equals(captchaCache)) { result.setMessage("验证码错误"); result.setSuccess(false); return result; } //校验用户有效性 SysUser sysUser = sysUserService.getUserByPhone(phone); if(sysUser != null){ // 存在用户,直接绑定 sysThirdAccountService.updateThirdUserId(sysUser,thirdUserUuid); }else{ // 不存在手机号,创建用户 sysUser = sysThirdAccountService.createUser(phone,thirdUserUuid); } String token = saveToken(sysUser); result.setSuccess(true); result.setResult(token); return result; } /** * 企业微信/钉钉 OAuth2登录 * * @param source * @param state * @return */ @ResponseBody @GetMapping("/oauth2/{source}/login") public String oauth2LoginCallback(@PathVariable("source") String source, @RequestParam("state") String state, HttpServletResponse response) throws Exception { String url; if (ThirdAppConfig.WECHAT_ENTERPRISE.equalsIgnoreCase(source)) { ThirdAppTypeItemVo config = thirdAppConfig.getWechatEnterprise(); StringBuilder builder = new StringBuilder(); // 构造企业微信OAuth2登录授权地址 builder.append("https://open.weixin.qq.com/connect/oauth2/authorize"); // 企业的CorpID builder.append("?appid=").append(config.getClientId()); // 授权后重定向的回调链接地址,请使用urlencode对链接进行处理 String redirectUri ="https://fastwoke.cn:8087/jeecg-boot/sys/thirdLogin/oauth2/wechat_enterprise/callback"; builder.append("&redirect_uri=").append(URLEncoder.encode(redirectUri, "UTF-8")); // 返回类型,此时固定为:code builder.append("&response_type=code"); // 应用授权作用域。 // snsapi_base:静默授权,可获取成员的的基础信息(UserId与DeviceId); builder.append("&scope=snsapi_base");//静默授权 //builder.append("&scope=snsapi_privateinfo"); // 手动授权作用域 // 重定向后会带上state参数,长度不可超过128个字节 builder.append("&state=").append(state); //builder.append("&agentid=").append(config.getAgentId()); // 补充AgentID(手动授权时需要) // 终端使用此参数判断是否需要带上身份信息 builder.append("#wechat_redirect"); url = builder.toString(); } else if (ThirdAppConfig.DINGTALK.equalsIgnoreCase(source)) { ThirdAppTypeItemVo config = thirdAppConfig.getDingtalk(); StringBuilder builder = new StringBuilder(); // 构造钉钉OAuth2登录授权地址 builder.append("https://login.dingtalk.com/oauth2/auth"); // 授权通过/拒绝后回调地址。 // 注意 需要与注册应用时登记的域名保持一致。 String redirectUri = RestUtil.getBaseUrl() + "/sys/thirdLogin/oauth2/dingtalk/callback"; builder.append("?redirect_uri=").append(URLEncoder.encode(redirectUri, "UTF-8")); // 固定值为code。 // 授权通过后返回authCode。 builder.append("&response_type=code"); // 步骤一中创建的应用详情中获取。 // 企业内部应用:client_id为应用的AppKey。 builder.append("&client_id=").append(config.getClientId()); // 授权范围,授权页面显示的授权信息以应用注册时配置的为准。 // openid:授权后可获得用户userid builder.append("&scope=openid"); // 跟随authCode原样返回。 builder.append("&state=").append(state); //update-begin---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录------------ builder.append("&prompt=").append("consent"); //update-end---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录-------------- url = builder.toString(); } else { return "不支持的source"; } log.info("oauth2 login url:" + url); response.sendRedirect(url); return "login…"; } /** * 企业微信/钉钉 OAuth2登录回调 * * @param code * @param state * @param response * @return */ @ResponseBody @GetMapping("/oauth2/{source}/callback") public String oauth2LoginCallback( @PathVariable("source") String source, // 企业微信返回的code @RequestParam(value = "code", required = false) String code, // 钉钉返回的code @RequestParam(value = "authCode", required = false) String authCode, @RequestParam("state") String state, HttpServletResponse response) { SysUser loginUser; if (ThirdAppConfig.WECHAT_ENTERPRISE.equalsIgnoreCase(source)) { log.info("【企业微信】OAuth2登录进入callback:code=" + code + ", state=" + state); if (code == null) { log.info("用户取消了企业微信授权"); return "用户取消了授权"; } loginUser = thirdAppWechatEnterpriseService.oauth2Login(code); if (loginUser == null) { return "登录失败"; } } else if (ThirdAppConfig.DINGTALK.equalsIgnoreCase(source)) { log.info("【钉钉】OAuth2登录进入callback:authCode=" + authCode + ", state=" + state); loginUser = thirdAppDingtalkService.oauth2Login(authCode); if (loginUser == null) { return "登录失败"; } } else { return "不支持的source"; } try { //update-begin-author:taoyan date:2022-6-30 for: 工作流发送消息 点击消息链接跳转办理页面 String redirect = ""; if (state.indexOf("?") > 0) { String[] arr = state.split("\\?"); state = arr[0]; if(arr.length>1){ redirect = arr[1]; } } String token = saveToken(loginUser); // ============ 新增 Shiro 登录逻辑 ============ JwtToken jwtToken = new JwtToken(token); SecurityUtils.getSubject().login(jwtToken); state += "/h5/oauth2-app/login?oauth2LoginToken=" + URLEncoder.encode(token, "UTF-8"); //update-begin---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录------------ state += "&thirdType=" + source; //state += "&thirdType=" + "wechat_enterprise"; if (redirect != null && redirect.length() > 0) { state += "&" + redirect; } //update-end-author:taoyan date:2022-6-30 for: 工作流发送消息 点击消息链接跳转办理页面 //update-end---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录------------ log.info("OAuth2登录重定向地址: " + state); try { response.sendRedirect(state); return "ok"; } catch (IOException e) { e.printStackTrace(); return "重定向失败"; } } catch (UnsupportedEncodingException e) { e.printStackTrace(); return "解码失败"; } } //企业微信适配手动授权接口调整 ///** // * 企业微信/钉钉 OAuth2登录回调 // * // * @param code // * @param state // * @param response // * @return // */ //@ResponseBody //@GetMapping("/oauth2/{source}/callback") //public String oauth2LoginCallback( // @PathVariable("source") String source, // // 企业微信返回的code // @RequestParam(value = "code", required = false) String code, // // 钉钉返回的code // @RequestParam(value = "authCode", required = false) String authCode, // @RequestParam("state") String state, // @RequestParam(value = "is_reject", required = false) String isReject, // HttpServletResponse response) { // SysUser loginUser; // if (ThirdAppConfig.WECHAT_ENTERPRISE.equalsIgnoreCase(source)) { // log.info("【企业微信】OAuth2登录进入callback:code=" + code + ", state=" + state); // // 1. 判定用户拒绝授权的3种场景: // // - 场景1:is_reject=true(企业微信明确标识拒绝) // // - 场景2:code为null(未返回有效授权凭证) // // - 场景3:code存在但无效(调用API失败) // boolean isUserReject = "true".equals(isReject) || code == null; // if (isUserReject) { // log.info("用户明确拒绝企业微信授权(is_reject={}, code={}", isReject, code); // // 构造含error=access_denied的重定向地址,前端据此识别 // String errorRedirect = buildErrorRedirect(state, "access_denied"); // try { // response.sendRedirect(errorRedirect); // return "用户拒绝授权,已重定向"; // } catch (IOException e) { // log.error("拒绝授权重定向失败", e); // return "重定向失败"; // } // } // // // 2. 尝试用code获取用户信息(code存在但可能无效) // loginUser = thirdAppWechatEnterpriseService.oauth2Login(code); // if (loginUser == null) { // log.info("企业微信授权失败(code无效)"); // String errorRedirect = buildErrorRedirect(state, "invalid_code"); // try { // response.sendRedirect(errorRedirect); // return "授权失败,已重定向"; // } catch (IOException e) { // log.error("授权失败重定向失败", e); // return "重定向失败"; // } // } // loginUser = thirdAppWechatEnterpriseService.oauth2Login(code); // if (loginUser == null) { // return "登录失败"; // } // } else if (ThirdAppConfig.DINGTALK.equalsIgnoreCase(source)) { // log.info("【钉钉】OAuth2登录进入callback:authCode=" + authCode + ", state=" + state); // loginUser = thirdAppDingtalkService.oauth2Login(authCode); // if (loginUser == null) { // return "登录失败"; // } // } else { // return "不支持的source"; // } // try { // // //update-begin-author:taoyan date:2022-6-30 for: 工作流发送消息 点击消息链接跳转办理页面 // String redirect = ""; // if (state.indexOf("?") > 0) { // String[] arr = state.split("\\?"); // state = arr[0]; // if(arr.length>1){ // redirect = arr[1]; // } // } // // String token = saveToken(loginUser); // // ============ 新增 Shiro 登录逻辑 ============ // JwtToken jwtToken = new JwtToken(token); // SecurityUtils.getSubject().login(jwtToken); // state += "/h5/oauth2-app/login?oauth2LoginToken=" + URLEncoder.encode(token, "UTF-8"); // //update-begin---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录------------ // state += "&thirdType=" + source; // //state += "&thirdType=" + "wechat_enterprise"; // if (redirect != null && redirect.length() > 0) { // state += "&" + redirect; // } // //update-end-author:taoyan date:2022-6-30 for: 工作流发送消息 点击消息链接跳转办理页面 // // //update-end---author:wangshuai ---date:20220613 for:[issues/I5BOUF]oauth2 钉钉无法登录------------ // log.info("OAuth2登录重定向地址: " + state); // try { // response.sendRedirect(state); // return "ok"; // } catch (IOException e) { // e.printStackTrace(); // return "重定向失败"; // } // } catch (UnsupportedEncodingException e) { // e.printStackTrace(); // return "解码失败"; // } //} // ///** // * 构造企业微信授权拒绝时的错误重定向地址 // * 适配规则:state参数长度≤128字节,保留原始业务参数,附加error标识 // */ //private String buildErrorRedirect(String originalState, String errorCode) { // // 1. 拆分原始state中的基础路径和业务参数(避免破坏原有state结构) // String baseState = originalState; // String businessParams = ""; // if (originalState.contains("?")) { // String[] stateParts = originalState.split("\\?", 2); // 只拆分成两部分 // baseState = stateParts[0]; // 基础路径(如https://fastwoke.cn:8087) // businessParams = stateParts[1]; // 原始业务参数(如redirect=xxx) // } // // // 2. 构造错误参数(error=xxx),并拼接原始业务参数 // StringBuilder errorParams = new StringBuilder(); // errorParams.append("error=").append(errorCode); // 核心错误标识 // if (!businessParams.isEmpty()) { // errorParams.append("&").append(businessParams); // 附加原始业务参数 // } // // // 3. 拼接完整的重定向地址(格式:baseState/h5/oauth2-app/login?errorParams) // // 注意:企业微信要求重定向地址需与授权链接中的redirect_uri域名一致 // StringBuilder errorRedirect = new StringBuilder(baseState); // errorRedirect.append("/h5/oauth2-app/login?").append(errorParams); // // // 4. 校验state长度(企业微信限制≤128字节),超长时截断非关键参数 // String redirectUrl = errorRedirect.toString(); // if (redirectUrl.getBytes().length > 128) { // log.warn("错误重定向地址超长,截断处理: {}", redirectUrl); // // 只保留基础路径和error参数,舍弃其他业务参数 // redirectUrl = baseState + "/h5/oauth2-app/login?error=" + errorCode; // } // // log.info("企业微信拒绝授权重定向地址: {}", redirectUrl); // return redirectUrl; //} }