1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
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<JSONObject> result = new Result<JSONObject>();
        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<SysThirdAccount> query = new LambdaQueryWrapper<SysThirdAccount>();
            query.eq(SysThirdAccount::getThirdUserUuid, uuid);
            query.eq(SysThirdAccount::getThirdType, source);
            List<SysThirdAccount> 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<String> thirdUserCreate(@RequestBody ThirdLoginModel model) {
        log.info("第三方登录创建新账号:" );
        Result<String> 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<String> checkPassword(@RequestBody JSONObject json) {
        Result<String> 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<JSONObject> getThirdLoginUser(@PathVariable("token") String token,@PathVariable("thirdType") String thirdType) throws Exception {
        Result<JSONObject> result = new Result<JSONObject>();
        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<SysThirdAccount> 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<String> bindingThirdPhone(@RequestBody JSONObject jsonObject) {
        Result<String> result = new Result<String>();
        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 "解码失败";
        }
    }
 
}