package com.iot.gateway.oauth; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.iot.gateway.common.IpUtils; import com.iot.gateway.entity.OpenClient; import com.iot.gateway.mapper.OpenClientMapper; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import java.time.LocalDateTime; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @Slf4j @Tag(name = "OAuth 2.0 授权中心") @RestController @RequestMapping("/oauth2") public class OAuth2TokenController { @Autowired private OpenClientMapper clientMapper; @Autowired private JwtTokenProvider jwtTokenProvider; @Autowired private PasswordEncoder passwordEncoder; private final ObjectMapper objectMapper = new ObjectMapper(); @Operation(summary = "客户端凭据模式换取访问令牌 (支持 form-urlencoded 与 application/json)") @PostMapping(value = "/token") public ResponseEntity> issueToken( @RequestParam(value = "grant_type", required = false) String formGrantType, @RequestParam(value = "client_id", required = false) String formClientId, @RequestParam(value = "client_secret", required = false) String formClientSecret, @RequestParam(value = "scope", required = false) String formScope, jakarta.servlet.http.HttpServletRequest request) { String grantType = formGrantType; String clientId = formClientId; String clientSecret = formClientSecret; String scope = formScope; // 兼容自适应解析 application/json 请求载荷 String contentType = request.getContentType(); if ((!StringUtils.hasText(clientId) || !StringUtils.hasText(clientSecret)) && contentType != null && contentType.toLowerCase().contains("application/json")) { try { JsonNode root = objectMapper.readTree(request.getInputStream()); if (root != null) { if (grantType == null && root.has("grant_type")) grantType = root.get("grant_type").asText(); if (clientId == null && root.has("client_id")) clientId = root.get("client_id").asText(); if (clientSecret == null && root.has("client_secret")) clientSecret = root.get("client_secret").asText(); if (scope == null && root.has("scope")) scope = root.get("scope").asText(); } } catch (Exception e) { log.warn("解析 JSON 格式 OAuth 请求体异常: {}", e.getMessage()); } } Map error = new HashMap<>(); if (!"client_credentials".equalsIgnoreCase(grantType)) { error.put("error", "unsupported_grant_type"); error.put("error_description", "仅支持 client_credentials 客户端凭据授权模式"); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } if (!StringUtils.hasText(clientId) || !StringUtils.hasText(clientSecret)) { error.put("error", "invalid_client"); error.put("error_description", "client_id 或 client_secret 不能为空"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); } OpenClient client = clientMapper.selectOne(new LambdaQueryWrapper() .eq(OpenClient::getClientId, clientId.trim())); if (client == null) { error.put("error", "invalid_client"); error.put("error_description", "客户端标识或密钥错误"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); } // 密钥比对:优先支持 BCrypt 哈希比对;兼顾历史明文并自动平滑升级为 BCrypt 哈希 boolean secretMatch = false; String storedSecret = client.getClientSecret(); if (storedSecret != null) { if (storedSecret.startsWith("$2a$") || storedSecret.startsWith("$2b$")) { secretMatch = passwordEncoder.matches(clientSecret, storedSecret); } else if (storedSecret.equals(clientSecret)) { secretMatch = true; client.setClientSecret(passwordEncoder.encode(clientSecret)); clientMapper.updateById(client); log.info("检测到明文密钥成功验证,已自动无感升级为 BCrypt 哈希: clientId={}", clientId); } } if (!secretMatch) { error.put("error", "invalid_client"); error.put("error_description", "客户端标识或密钥错误"); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(error); } if (client.getStatus() == null || client.getStatus() != 1) { error.put("error", "unauthorized_client"); error.put("error_description", "客户端已被禁用,无法申请令牌"); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); } if (client.getExpireTime() != null && client.getExpireTime().isBefore(LocalDateTime.now())) { error.put("error", "expired_token"); error.put("error_description", "客户端凭证已过期"); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); } // 校验客户端来源 IP 白名单(基于可信反向代理验证) String clientIp = IpUtils.getClientIp(request); if (!IpUtils.isAllowed(clientIp, client.getIpWhitelist())) { log.warn("OAuth 2.0 客户端 IP 白名单拦截: clientId={}, clientIp={}, whitelist={}", clientId, clientIp, client.getIpWhitelist()); error.put("error", "unauthorized_client"); error.put("error_description", "客户端来源 IP [" + clientIp + "] 不在允许的白名单范围内"); return ResponseEntity.status(HttpStatus.FORBIDDEN).body(error); } // 验证 Scope 并支持合法收窄 (Scope Narrowing) String effectiveScopes = client.getScopes(); if (StringUtils.hasText(scope)) { List clientAllowed = StringUtils.hasText(client.getScopes()) ? Arrays.asList(client.getScopes().split("[,;\\s]+")) : Collections.emptyList(); List requested = Arrays.asList(scope.split("[,;\\s]+")); for (String reqScope : requested) { String reqNorm = reqScope.trim().replace(":", "."); boolean allowed = clientAllowed.stream().anyMatch(ca -> { String caNorm = ca.trim().replace(":", "."); return caNorm.equals(reqNorm) || caNorm.equals("all") || caNorm.equals("*"); }); if (!allowed) { error.put("error", "invalid_scope"); error.put("error_description", "请求的作用域 [" + reqScope + "] 超出该客户端的允许授权范围"); return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(error); } } effectiveScopes = String.join(",", requested); } String accessToken = jwtTokenProvider.generateToken(client, effectiveScopes); log.info("OAuth 2.0 成功颁发令牌: clientId={}, clientIp={}, tenantId={}, scopes={}", clientId, clientIp, client.getTenantId(), effectiveScopes); Map response = new HashMap<>(); response.put("access_token", accessToken); response.put("token_type", "Bearer"); response.put("expires_in", jwtTokenProvider.getTokenExpireSeconds()); response.put("scope", effectiveScopes); response.put("tenant_id", client.getTenantId()); response.put("client_name", client.getClientName()); return ResponseEntity.ok(response); } }