收口预约只读接口权限与酒店隔离
This commit is contained in:
@@ -1,10 +1,13 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.access.common.enums.PlatformPermissionCode;
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextService;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessagePageResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageConversationResult;
|
||||
import cn.nianxx.thhotel.platform.message.common.result.SourceMessageOriginalResponse;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageOriginalAccessRequest;
|
||||
import cn.nianxx.thhotel.platform.message.common.request.SourceMessageQueryRequest;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageConversationService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageOriginalService;
|
||||
import cn.nianxx.thhotel.platform.message.service.SourceMessageQueryService;
|
||||
@@ -28,6 +31,8 @@ public class SourceMessageController {
|
||||
private final SourceMessageQueryService queryService;
|
||||
private final SourceMessageOriginalService originalService;
|
||||
private final SourceMessageConversationService conversationService;
|
||||
private final FrontendAuthorizationService authorizationService;
|
||||
private final HotelContextService hotelContextService;
|
||||
|
||||
/**
|
||||
* 注入 SourceMessage 查询与原文读取服务,Controller 不直接访问 Mapper 或 Repository。
|
||||
@@ -35,10 +40,14 @@ public class SourceMessageController {
|
||||
public SourceMessageController(
|
||||
SourceMessageQueryService queryService,
|
||||
SourceMessageOriginalService originalService,
|
||||
SourceMessageConversationService conversationService) {
|
||||
SourceMessageConversationService conversationService,
|
||||
FrontendAuthorizationService authorizationService,
|
||||
HotelContextService hotelContextService) {
|
||||
this.queryService = queryService;
|
||||
this.originalService = originalService;
|
||||
this.conversationService = conversationService;
|
||||
this.authorizationService = authorizationService;
|
||||
this.hotelContextService = hotelContextService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -52,6 +61,7 @@ public class SourceMessageController {
|
||||
@RequestParam(required = false) String captureStatus,
|
||||
@RequestParam(required = false) Integer pageNum,
|
||||
@RequestParam(required = false) Integer pageSize) {
|
||||
authorizationService.requirePermission(PlatformPermissionCode.SOURCE_MESSAGE_READ.name());
|
||||
return queryService.query(new SourceMessageQueryRequest(
|
||||
hotelId,
|
||||
externalMessageId,
|
||||
@@ -67,8 +77,11 @@ public class SourceMessageController {
|
||||
*/
|
||||
@GetMapping("/{id}")
|
||||
public SourceMessageSummaryResponse detail(@PathVariable Long id) {
|
||||
return queryService.getSummary(id)
|
||||
authorizationService.requirePermission(PlatformPermissionCode.SOURCE_MESSAGE_READ.name());
|
||||
SourceMessageSummaryResponse summary = queryService.getSummary(id)
|
||||
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND, "source message not found"));
|
||||
requireSourceMessageHotelAccess(summary);
|
||||
return summary;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -105,4 +118,11 @@ public class SourceMessageController {
|
||||
private boolean hasText(String value) {
|
||||
return value != null && !value.trim().isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* 按 SourceMessage 实际归属酒店校验当前用户访问权,避免跨酒店按 ID 读取摘要。
|
||||
*/
|
||||
private void requireSourceMessageHotelAccess(SourceMessageSummaryResponse summary) {
|
||||
hotelContextService.requireAccessibleHotel(summary.hotelId());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package cn.nianxx.thhotel.platform.message.control;
|
||||
|
||||
import cn.nianxx.thhotel.platform.hotel.service.HotelContextException;
|
||||
import cn.nianxx.thhotel.platform.security.service.impl.FrontendAuthorizationException;
|
||||
import java.util.Map;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
@@ -22,4 +23,16 @@ public class SourceMessageControllerAdvice {
|
||||
"error_code", exception.getErrorCode(),
|
||||
"message", exception.getMessage()));
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 SourceMessage 摘要接口登录或权限不足异常,避免泄漏内部鉴权细节。
|
||||
*/
|
||||
@ExceptionHandler(FrontendAuthorizationException.class)
|
||||
public ResponseEntity<Map<String, Object>> handleFrontendAuthorizationException(
|
||||
FrontendAuthorizationException exception) {
|
||||
return ResponseEntity.status(exception.getStatus())
|
||||
.body(Map.of(
|
||||
"error_code", exception.getErrorCode(),
|
||||
"message", exception.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package cn.nianxx.thhotel.platform.security.service;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
|
||||
/**
|
||||
* 前端业务接口强制鉴权服务。用于普通业务页面接口的登录和权限边界,不复用管理后台错误码。
|
||||
*/
|
||||
public interface FrontendAuthorizationService {
|
||||
|
||||
/**
|
||||
* 要求当前请求已登录,并返回当前用户上下文。
|
||||
*/
|
||||
AuthenticatedUserContext requireLogin();
|
||||
|
||||
/**
|
||||
* 要求当前请求用户拥有指定业务权限码。
|
||||
*/
|
||||
AuthenticatedUserContext requirePermission(String permissionCode);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package cn.nianxx.thhotel.platform.security.service.impl;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* 前端业务接口鉴权受控异常。ControllerAdvice 负责转换为稳定 HTTP 响应。
|
||||
*/
|
||||
public class FrontendAuthorizationException extends RuntimeException {
|
||||
|
||||
private final HttpStatus status;
|
||||
private final String errorCode;
|
||||
|
||||
public FrontendAuthorizationException(HttpStatus status, String errorCode, String message) {
|
||||
super(message);
|
||||
this.status = status;
|
||||
this.errorCode = errorCode;
|
||||
}
|
||||
|
||||
public HttpStatus getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public String getErrorCode() {
|
||||
return errorCode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package cn.nianxx.thhotel.platform.security.service.impl;
|
||||
|
||||
import cn.nianxx.thhotel.platform.security.common.dto.AuthenticatedUserContext;
|
||||
import cn.nianxx.thhotel.platform.security.service.CurrentUserContextService;
|
||||
import cn.nianxx.thhotel.platform.security.service.FrontendAuthorizationService;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* 前端业务接口强制鉴权服务实现。复用可选 token 解析结果,并返回业务接口专用错误码。
|
||||
*/
|
||||
@Service
|
||||
public class FrontendAuthorizationServiceImpl implements FrontendAuthorizationService {
|
||||
|
||||
private final CurrentUserContextService currentUserContextService;
|
||||
private final HttpServletRequest request;
|
||||
|
||||
/**
|
||||
* 注入当前用户上下文服务和原始请求,用于区分缺少 token 与 token 失效。
|
||||
*/
|
||||
public FrontendAuthorizationServiceImpl(
|
||||
CurrentUserContextService currentUserContextService,
|
||||
HttpServletRequest request) {
|
||||
this.currentUserContextService = currentUserContextService;
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求当前请求已登录;没有有效 token 时返回前端业务接口专用 401。
|
||||
*/
|
||||
@Override
|
||||
public AuthenticatedUserContext requireLogin() {
|
||||
return currentUserContextService.currentUser()
|
||||
.orElseThrow(this::missingOrInvalidLogin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 要求当前请求用户拥有指定业务权限码;已登录但无权限时返回 403。
|
||||
*/
|
||||
@Override
|
||||
public AuthenticatedUserContext requirePermission(String permissionCode) {
|
||||
AuthenticatedUserContext context = requireLogin();
|
||||
if (permissionCode == null || permissionCode.isBlank()
|
||||
|| !context.permissionCodes().contains(permissionCode)) {
|
||||
throw new FrontendAuthorizationException(
|
||||
HttpStatus.FORBIDDEN,
|
||||
"FRONTEND_PERMISSION_DENIED",
|
||||
"当前用户没有访问该业务能力的权限。");
|
||||
}
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 Authorization 头判断是未登录还是登录态失效,便于前端做统一提示。
|
||||
*/
|
||||
private FrontendAuthorizationException missingOrInvalidLogin() {
|
||||
String authorizationHeader = request.getHeader("Authorization");
|
||||
if (hasBearerToken(authorizationHeader)) {
|
||||
return new FrontendAuthorizationException(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"AUTH_SESSION_INVALID",
|
||||
"登录已失效,请重新登录。");
|
||||
}
|
||||
return new FrontendAuthorizationException(
|
||||
HttpStatus.UNAUTHORIZED,
|
||||
"AUTH_TOKEN_REQUIRED",
|
||||
"请先登录后再访问该业务能力。");
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否提供了非空 Bearer token。
|
||||
*/
|
||||
private boolean hasBearerToken(String authorizationHeader) {
|
||||
if (authorizationHeader == null || authorizationHeader.isBlank()) {
|
||||
return false;
|
||||
}
|
||||
String prefix = "Bearer ";
|
||||
return authorizationHeader.regionMatches(true, 0, prefix, 0, prefix.length())
|
||||
&& !authorizationHeader.substring(prefix.length()).trim().isBlank();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user