package com.ruoyi.app.user; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper; import com.ruoyi.app.utils.oauth.OAuthVerifyService; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.constant.CacheConstants; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.model.LoginUserDto; import com.ruoyi.common.core.redis.RedisCache; import com.ruoyi.common.utils.ServletUtils; import com.ruoyi.common.utils.ip.AddressUtils; import com.ruoyi.common.utils.ip.IpUtils; import com.ruoyi.system.domain.InfoUser; import com.ruoyi.system.domain.InfoUserOauth; import com.ruoyi.system.mapper.InfoUserOauthMapper; import com.ruoyi.system.service.IInfoUserService; import com.ruoyi.system.utils.JwtUtil; import eu.bitwalker.useragentutils.UserAgent; import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.UUID; import java.util.concurrent.TimeUnit; /** * LINE 登录服务端回调(017-oauth-login 增量,2026-08-05)。 * *
为什么需要它:当用户走「唤起 LINE App / 系统浏览器」授权时,LINE 把重定向发给 * 注册的 redirect_uri(后端 https URL),uniapp(另一个 App)无法在中间截获 code。这时必须由 * 后端接住回调、换 token、完成登录,再把结果 302 跳回 App(自定义 scheme)。原 {@code /oauthLogin} * 保留不动,覆盖「前端自己拿到 code 再 POST 给后端」的另一种场景(H5 / webview / SDK)。 * *
流程:LINE → {@code GET /auth/line/callback?code=...&state=...} * → {@link OAuthVerifyService#verify verify("line", code)}(code 换 token 取 userId) * → 查 info_user_oauth:已绑定→签 JWT token;未绑定→生成 tempKey(needPhone) * → 302 跳到 {@code oauth.line.app-redirect}(如 com.twanmsdyh.app://oauthLogin)带结果: *
redirect_uri 三方一致性:LINE 要求「前端 authorize 的 redirect_uri」「后端换 token 的
* redirect_uri」「LINE Console 回调白名单」三者完全一致,否则回 400 redirect_uri_mismatch。
* 故 application.yml 的 {@code oauth.line.redirect-uri} 必须与 Console 一致(= 本端点地址)。
*
* @author foodie
* @date 2026-08-05
*/
@RestController
@RequestMapping("/auth/line")
public class LineCallbackController extends BaseController {
private static final Logger log = LoggerFactory.getLogger(LineCallbackController.class);
/** 与 InfoUserController.OAUTH_TEMP_PREFIX 一致:未绑定临时凭证 Redis 前缀(oauthBindPhone 消费) */
private static final String OAUTH_TEMP_PREFIX = "oauth:bind:";
@Autowired
private OAuthVerifyService oauthVerifyService;
@Autowired
private IInfoUserService infoUserService;
@Autowired
private InfoUserOauthMapper infoUserOauthMapper;
@Autowired
private RedisCache redisCache;
/** 后端登录后 302 跳回 App 的 scheme(App 注册该 scheme 接收 token/tempKey/error) */
@Value("${oauth.line.app-redirect}")
private String appRedirect;
/**
* LINE 服务端回调:接 code → 换 token → 登录 → 302 回 App。
* 用 @Anonymous 放行(LINE 以浏览器/App 身份回调,无 token)。
*/
@Anonymous
@GetMapping("/callback")
public void callback(@RequestParam(value = "code", required = false) String code,
@RequestParam(value = "state", required = false) String state,
HttpServletResponse response) throws IOException {
try {
if (code == null || code.isEmpty()) {
log.warn("[OAuth][LINE] callback 缺 code 参数, state={}", state);
redirectToApp(response, "error", "no_code");
return;
}
log.info("[OAuth][LINE] callback 收到 code(len={}), state={}", code.length(), state);
// code → userId(OAuthVerifyService.verifyLine:code 换 access_token 再取 profile)
String providerUid = oauthVerifyService.verify("line", code);
InfoUserOauth bind = infoUserOauthMapper.selectOne(
new LambdaQueryWrapper