4.12 sa-token鉴权+重构前面的部分代码
This commit is contained in:
parent
41760e047d
commit
012e66bfc8
17
pom.xml
17
pom.xml
@ -79,6 +79,23 @@
|
||||
<groupId>org.springframework.session</groupId>
|
||||
<artifactId>spring-session-data-redis</artifactId>
|
||||
</dependency>
|
||||
<!-- Sa-Token 权限认证 -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-spring-boot-starter</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<!-- Sa-Token 整合 Redis (使用 jackson 序列化方式) -->
|
||||
<dependency>
|
||||
<groupId>cn.dev33</groupId>
|
||||
<artifactId>sa-token-redis-jackson</artifactId>
|
||||
<version>1.39.0</version>
|
||||
</dependency>
|
||||
<!-- 提供Redis连接池 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-configuration-processor</artifactId>
|
||||
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.config;
|
||||
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.servlet.*;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 请求包装过滤器
|
||||
*
|
||||
* @author pine
|
||||
*/
|
||||
@Order(1)
|
||||
@Component
|
||||
public class HttpRequestWrapperFilter implements Filter {
|
||||
|
||||
@Override
|
||||
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws ServletException, IOException {
|
||||
if (request instanceof HttpServletRequest) {
|
||||
HttpServletRequest servletRequest = (HttpServletRequest) request;
|
||||
String contentType = servletRequest.getHeader(Header.CONTENT_TYPE.getValue());
|
||||
if (ContentType.JSON.getValue().equals(contentType)) {
|
||||
// 可以再细粒度一些,只有需要进行空间权限校验的接口才需要包一层
|
||||
chain.doFilter(new RequestWrapper(servletRequest), response);
|
||||
} else {
|
||||
chain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
package edu.whut.smilepicturebackend.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import javax.servlet.ReadListener;
|
||||
import javax.servlet.ServletInputStream;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.servlet.http.HttpServletRequestWrapper;
|
||||
import java.io.*;
|
||||
|
||||
/**
|
||||
* 包装请求,使 InputStream 可以重复读取
|
||||
*
|
||||
* @author pine
|
||||
*/
|
||||
@Slf4j
|
||||
public class RequestWrapper extends HttpServletRequestWrapper {
|
||||
|
||||
private final String body;
|
||||
|
||||
public RequestWrapper(HttpServletRequest request) {
|
||||
super(request);
|
||||
StringBuilder stringBuilder = new StringBuilder();
|
||||
try (InputStream inputStream = request.getInputStream(); BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) {
|
||||
char[] charBuffer = new char[128];
|
||||
int bytesRead = -1;
|
||||
while ((bytesRead = bufferedReader.read(charBuffer)) > 0) {
|
||||
stringBuilder.append(charBuffer, 0, bytesRead);
|
||||
}
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
body = stringBuilder.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServletInputStream getInputStream() throws IOException {
|
||||
final ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(body.getBytes());
|
||||
return new ServletInputStream() {
|
||||
@Override
|
||||
public boolean isFinished() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isReady() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setReadListener(ReadListener readListener) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
return byteArrayInputStream.read();
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public BufferedReader getReader() throws IOException {
|
||||
return new BufferedReader(new InputStreamReader(this.getInputStream()));
|
||||
}
|
||||
|
||||
public String getBody() {
|
||||
return this.body;
|
||||
}
|
||||
|
||||
}
|
@ -15,6 +15,9 @@ import edu.whut.smilepicturebackend.constant.UserConstant;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.dto.picture.*;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
@ -48,12 +51,12 @@ public class PictureController {
|
||||
private final AliYunAiApi aliYunAiApi;
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 上传图片(可重新上传),前端选中图片就会调用该接口,无需前端点'创建'按钮!
|
||||
*TODO:目前有个bug,用户上传图片需要审核,会跳转到一个空白的图片详情页
|
||||
*/
|
||||
@PostMapping("/upload")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD)
|
||||
// @AuthCheck(mustRole = UserConstant.ADMIN_ROLE)
|
||||
public BaseResponse<PictureVO> uploadPicture(
|
||||
@RequestPart("file") MultipartFile multipartFile,
|
||||
@ -68,6 +71,7 @@ public class PictureController {
|
||||
* 通过 URL 上传图片(可重新上传)
|
||||
*/
|
||||
@PostMapping("/upload/url")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_UPLOAD)
|
||||
public BaseResponse<PictureVO> uploadPictureByUrl(
|
||||
@RequestBody PictureUploadRequest pictureUploadRequest,
|
||||
HttpServletRequest request) {
|
||||
@ -84,6 +88,7 @@ public class PictureController {
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_DELETE)
|
||||
public BaseResponse<Boolean> deletePicture(@RequestBody DeleteRequest deleteRequest,
|
||||
HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
@ -98,6 +103,7 @@ public class PictureController {
|
||||
* 编辑图片(给用户使用)或创建图片时,编辑标签、分类的时候
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<Boolean> editPicture(@RequestBody PictureEditRequest pictureEditRequest, HttpServletRequest request) {
|
||||
if (pictureEditRequest == null || pictureEditRequest.getId() <= 0) {
|
||||
throw new BusinessException(ErrorCode.PARAMS_ERROR);
|
||||
@ -159,6 +165,7 @@ public class PictureController {
|
||||
|
||||
/**
|
||||
* 根据 id 获取图片(封装类)
|
||||
* 这里不用sa-token的注解鉴权,因为它强制要求用户登录。故这里使用编程式注解
|
||||
*/
|
||||
@GetMapping("/get/vo")
|
||||
public BaseResponse<PictureVO> getPictureVOById(long id, HttpServletRequest request) {
|
||||
@ -170,8 +177,11 @@ public class PictureController {
|
||||
// 空间权限校验
|
||||
Long spaceId = picture.getSpaceId();
|
||||
if (spaceId != null) {
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
pictureService.checkPictureAuth(loginUser, picture);
|
||||
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW); //编程式鉴权
|
||||
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||
//User loginUser = userService.getLoginUser(request);
|
||||
//改为使用注解鉴权
|
||||
//pictureService.checkPictureAuth(loginUser, picture);
|
||||
}
|
||||
// 获取封装类
|
||||
return ResultUtils.success(pictureService.getPictureVO(picture, request));
|
||||
@ -210,12 +220,15 @@ public class PictureController {
|
||||
pictureQueryRequest.setNullSpaceId(true);
|
||||
} else {
|
||||
// 私有空间
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
Space space = spaceService.getById(spaceId);
|
||||
ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
if (!loginUser.getId().equals(space.getUserId())) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
||||
}
|
||||
boolean hasPermission = StpKit.SPACE.hasPermission(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
ThrowUtils.throwIf(!hasPermission, ErrorCode.NO_AUTH_ERROR);
|
||||
//已改为编程式鉴权
|
||||
// User loginUser = userService.getLoginUser(request);
|
||||
// Space space = spaceService.getById(spaceId);
|
||||
// ThrowUtils.throwIf(space == null, ErrorCode.NOT_FOUND_ERROR, "空间不存在");
|
||||
// if (!loginUser.getId().equals(space.getUserId())) {
|
||||
// throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "没有空间权限");
|
||||
// }
|
||||
}
|
||||
// 查询数据库
|
||||
Page<Picture> picturePage = pictureService.page(new Page<>(current, size),
|
||||
@ -280,6 +293,7 @@ public class PictureController {
|
||||
* 以图搜图
|
||||
*/
|
||||
@PostMapping("/search/picture")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
|
||||
public BaseResponse<List<ImageSearchResult>> getSimilarPicture(@RequestBody SearchPictureByPictureRequest request) throws IOException {
|
||||
ThrowUtils.throwIf(request == null, ErrorCode.NO_AUTH_ERROR);
|
||||
List<ImageSearchResult> similarImage = pictureService.getSimilarPicture(request);
|
||||
@ -290,6 +304,7 @@ public class PictureController {
|
||||
* 按照颜色搜索
|
||||
*/
|
||||
@PostMapping("/search/color")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_VIEW)
|
||||
public BaseResponse<List<PictureVO>> searchPictureByColor(@RequestBody SearchPictureByColorRequest searchPictureByColorRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(searchPictureByColorRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
String picColor = searchPictureByColorRequest.getPicColor();
|
||||
@ -300,9 +315,10 @@ public class PictureController {
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量编辑图片
|
||||
* 对私人/团队空间批量编辑图片
|
||||
*/
|
||||
@PostMapping("/edit/batch")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<Boolean> editPictureByBatch(@RequestBody PictureEditByBatchRequest pictureEditByBatchRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(pictureEditByBatchRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
User loginUser = userService.getLoginUser(request);
|
||||
@ -317,7 +333,7 @@ public class PictureController {
|
||||
* @return
|
||||
*/
|
||||
@PostMapping("/out_painting/create_task")
|
||||
// @SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.PICTURE_EDIT)
|
||||
public BaseResponse<CreateOutPaintingTaskResponse> createPictureOutPaintingTask(@RequestBody CreatePictureOutPaintingTaskRequest createPictureOutPaintingTaskRequest,
|
||||
HttpServletRequest request) {
|
||||
if (createPictureOutPaintingTaskRequest == null || createPictureOutPaintingTaskRequest.getPictureId() == null) {
|
||||
|
@ -7,6 +7,8 @@ import edu.whut.smilepicturebackend.common.ResultUtils;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.annotation.SaSpaceCheckPermission;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserEditRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.spaceuser.SpaceUserQueryRequest;
|
||||
@ -42,6 +44,7 @@ public class SpaceUserController {
|
||||
* 添加成员到空间
|
||||
*/
|
||||
@PostMapping("/add")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Long> addSpaceUser(@RequestBody SpaceUserAddRequest spaceUserAddRequest, HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUserAddRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
long id = spaceUserService.addSpaceUser(spaceUserAddRequest);
|
||||
@ -52,6 +55,7 @@ public class SpaceUserController {
|
||||
* 从空间移除成员
|
||||
*/
|
||||
@PostMapping("/delete")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Boolean> deleteSpaceUser(@RequestBody DeleteRequest deleteRequest,
|
||||
HttpServletRequest request) {
|
||||
if (deleteRequest == null || deleteRequest.getId() <= 0) {
|
||||
@ -71,6 +75,7 @@ public class SpaceUserController {
|
||||
* 查询某个成员在某个空间的信息
|
||||
*/
|
||||
@PostMapping("/get")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<SpaceUser> getSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest) {
|
||||
// 参数校验
|
||||
ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
@ -87,6 +92,7 @@ public class SpaceUserController {
|
||||
* 查询成员信息列表
|
||||
*/
|
||||
@PostMapping("/list")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<List<SpaceUserVO>> listSpaceUser(@RequestBody SpaceUserQueryRequest spaceUserQueryRequest,
|
||||
HttpServletRequest request) {
|
||||
ThrowUtils.throwIf(spaceUserQueryRequest == null, ErrorCode.PARAMS_ERROR);
|
||||
@ -100,6 +106,7 @@ public class SpaceUserController {
|
||||
* 编辑成员信息(设置权限)
|
||||
*/
|
||||
@PostMapping("/edit")
|
||||
@SaSpaceCheckPermission(value = SpaceUserPermissionConstant.SPACE_USER_MANAGE)
|
||||
public BaseResponse<Boolean> editSpaceUser(@RequestBody SpaceUserEditRequest spaceUserEditRequest,
|
||||
HttpServletRequest request) {
|
||||
if (spaceUserEditRequest == null || spaceUserEditRequest.getId() <= 0) {
|
||||
|
@ -0,0 +1,48 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* SpaceUserAuthContext
|
||||
* 表示用户在特定空间内的授权上下文,包括关联的图片、空间和用户信息。
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserAuthContext {
|
||||
|
||||
/**
|
||||
* 临时参数,不同请求对应的 id 可能不同
|
||||
*/
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 图片 ID
|
||||
*/
|
||||
private Long pictureId;
|
||||
|
||||
/**
|
||||
* 空间 ID
|
||||
*/
|
||||
private Long spaceId;
|
||||
|
||||
/**
|
||||
* 空间用户 ID
|
||||
*/
|
||||
private Long spaceUserId;
|
||||
|
||||
/**
|
||||
* 图片信息
|
||||
*/
|
||||
private Picture picture;
|
||||
|
||||
/**
|
||||
* 空间信息
|
||||
*/
|
||||
private Space space;
|
||||
|
||||
/**
|
||||
* 空间用户信息
|
||||
*/
|
||||
private SpaceUser spaceUser;
|
||||
}
|
@ -0,0 +1,111 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.hutool.core.io.resource.ResourceUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserAuthConfig;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserRole;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员权限管理
|
||||
*/
|
||||
@Component
|
||||
public class SpaceUserAuthManager {
|
||||
|
||||
@Resource
|
||||
private UserService userService;
|
||||
|
||||
@Resource
|
||||
private SpaceUserService spaceUserService;
|
||||
|
||||
public static final SpaceUserAuthConfig SPACE_USER_AUTH_CONFIG;
|
||||
|
||||
static {
|
||||
String json = ResourceUtil.readUtf8Str("biz/spaceUserAuthConfig.json");
|
||||
SPACE_USER_AUTH_CONFIG = JSONUtil.toBean(json, SpaceUserAuthConfig.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据角色获取权限列表
|
||||
*
|
||||
* @param spaceUserRole
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPermissionsByRole(String spaceUserRole) {
|
||||
if (StrUtil.isBlank(spaceUserRole)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
SpaceUserRole role = SPACE_USER_AUTH_CONFIG.getRoles()
|
||||
.stream()
|
||||
.filter(r -> r.getKey().equals(spaceUserRole))
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
if (role == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return role.getPermissions();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 获取权限列表
|
||||
*
|
||||
* @param space
|
||||
* @param loginUser
|
||||
* @return
|
||||
*/
|
||||
public List<String> getPermissionList(Space space, User loginUser) {
|
||||
if (loginUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 管理员权限
|
||||
List<String> ADMIN_PERMISSIONS = getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
|
||||
// 公共图库
|
||||
if (space == null) {
|
||||
if (userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
}
|
||||
SpaceTypeEnum spaceTypeEnum = SpaceTypeEnum.getEnumByValue(space.getSpaceType());
|
||||
if (spaceTypeEnum == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 根据空间获取对应的权限
|
||||
switch (spaceTypeEnum) {
|
||||
case PRIVATE:
|
||||
// 私有空间,仅本人或管理员有所有权限
|
||||
if (space.getUserId().equals(loginUser.getId()) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
case TEAM:
|
||||
// 团队空间,查询 SpaceUser 并获取角色和权限
|
||||
SpaceUser spaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, space.getId())
|
||||
.eq(SpaceUser::getUserId, loginUser.getId())
|
||||
.one();
|
||||
if (spaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
} else {
|
||||
return getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
}
|
||||
return new ArrayList<>();
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.dev33.satoken.stp.StpInterface;
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import cn.hutool.core.util.ObjUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.ReflectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.extra.servlet.ServletUtil;
|
||||
import cn.hutool.http.ContentType;
|
||||
import cn.hutool.http.Header;
|
||||
import cn.hutool.json.JSONUtil;
|
||||
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.manager.auth.model.SpaceUserPermissionConstant;
|
||||
import edu.whut.smilepicturebackend.model.entity.Picture;
|
||||
import edu.whut.smilepicturebackend.model.entity.Space;
|
||||
import edu.whut.smilepicturebackend.model.entity.SpaceUser;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceRoleEnum;
|
||||
import edu.whut.smilepicturebackend.model.enums.SpaceTypeEnum;
|
||||
import edu.whut.smilepicturebackend.service.PictureService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceService;
|
||||
import edu.whut.smilepicturebackend.service.SpaceUserService;
|
||||
import edu.whut.smilepicturebackend.service.UserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.*;
|
||||
|
||||
import static edu.whut.smilepicturebackend.constant.UserConstant.USER_LOGIN_STATE;
|
||||
|
||||
|
||||
/**
|
||||
* 自定义权限加载接口实现类
|
||||
*/
|
||||
@Component // 保证此类被 SpringBoot 扫描,完成 Sa-Token 的自定义权限验证扩展
|
||||
@RequiredArgsConstructor
|
||||
public class StpInterfaceImpl implements StpInterface {
|
||||
|
||||
// 默认是 /api
|
||||
@Value("${server.servlet.context-path}")
|
||||
private String contextPath;
|
||||
|
||||
private final UserService userService;
|
||||
|
||||
|
||||
private final SpaceService spaceService;
|
||||
|
||||
|
||||
private final SpaceUserService spaceUserService;
|
||||
|
||||
private final PictureService pictureService;
|
||||
|
||||
|
||||
private final SpaceUserAuthManager spaceUserAuthManager;
|
||||
|
||||
/**
|
||||
* 返回一个账号所拥有的权限码集合
|
||||
*/
|
||||
@Override
|
||||
public List<String> getPermissionList(Object loginId, String loginType) {
|
||||
// 判断 loginType,仅对类型为 "space" 进行权限校验
|
||||
if (!StpKit.SPACE_TYPE.equals(loginType)) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 管理员权限,表示权限校验通过
|
||||
List<String> ADMIN_PERMISSIONS = spaceUserAuthManager.getPermissionsByRole(SpaceRoleEnum.ADMIN.getValue());
|
||||
// 获取上下文对象
|
||||
SpaceUserAuthContext authContext = getAuthContextByRequest();
|
||||
// 如果所有字段都为空,表示查询公共图库,可以通过
|
||||
if (isAllFieldsNull(authContext)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
// 获取 userId 前面登录的时候把信息存进去过,现在取出来。
|
||||
User loginUser = (User) StpKit.SPACE.getSessionByLoginId(loginId).get(USER_LOGIN_STATE);
|
||||
if (loginUser == null) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR, "用户未登录");
|
||||
}
|
||||
Long userId = loginUser.getId();
|
||||
// 优先从上下文中获取 SpaceUser 对象
|
||||
SpaceUser spaceUser = authContext.getSpaceUser();
|
||||
if (spaceUser != null) {
|
||||
return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
// 如果有 spaceUserId,必然是团队空间,通过数据库查询 SpaceUser 对象
|
||||
Long spaceUserId = authContext.getSpaceUserId();
|
||||
if (spaceUserId != null) {
|
||||
spaceUser = spaceUserService.getById(spaceUserId);
|
||||
if (spaceUser == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间用户信息");
|
||||
}
|
||||
// 取出当前登录用户对应的 spaceUser
|
||||
SpaceUser loginSpaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, spaceUser.getSpaceId())
|
||||
.eq(SpaceUser::getUserId, userId)
|
||||
.one();
|
||||
if (loginSpaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
// 这里会导致管理员在私有空间没有权限,可以再查一次库处理
|
||||
return spaceUserAuthManager.getPermissionsByRole(loginSpaceUser.getSpaceRole());
|
||||
}
|
||||
// 如果没有 spaceUserId,尝试通过 spaceId 或 pictureId 获取 Space 对象并处理
|
||||
Long spaceId = authContext.getSpaceId();
|
||||
if (spaceId == null) {
|
||||
// 如果没有 spaceId,通过 pictureId 获取 Picture 对象和 Space 对象
|
||||
Long pictureId = authContext.getPictureId();
|
||||
// 图片 id 也没有,则默认通过权限校验
|
||||
if (pictureId == null) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
}
|
||||
Picture picture = pictureService.lambdaQuery()
|
||||
.eq(Picture::getId, pictureId)
|
||||
.select(Picture::getId, Picture::getSpaceId, Picture::getUserId)
|
||||
.one();
|
||||
if (picture == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到图片信息");
|
||||
}
|
||||
spaceId = picture.getSpaceId();
|
||||
// 公共图库,仅本人或管理员可操作
|
||||
if (spaceId == null) {
|
||||
if (picture.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
// 不是自己的图片,仅可查看
|
||||
return Collections.singletonList(SpaceUserPermissionConstant.PICTURE_VIEW);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 获取 Space 对象
|
||||
Space space = spaceService.getById(spaceId);
|
||||
if (space == null) {
|
||||
throw new BusinessException(ErrorCode.NOT_FOUND_ERROR, "未找到空间信息");
|
||||
}
|
||||
// 根据 Space 类型判断权限
|
||||
if (space.getSpaceType() == SpaceTypeEnum.PRIVATE.getValue()) {
|
||||
// 私有空间,仅本人或管理员有权限
|
||||
if (space.getUserId().equals(userId) || userService.isAdmin(loginUser)) {
|
||||
return ADMIN_PERMISSIONS;
|
||||
} else {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
} else {
|
||||
// 团队空间,查询 SpaceUser 并获取角色和权限
|
||||
spaceUser = spaceUserService.lambdaQuery()
|
||||
.eq(SpaceUser::getSpaceId, spaceId)
|
||||
.eq(SpaceUser::getUserId, userId)
|
||||
.one();
|
||||
if (spaceUser == null) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
return spaceUserAuthManager.getPermissionsByRole(spaceUser.getSpaceRole());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 本项目中不使用。返回一个账号所拥有的角色标识集合 (权限与角色可分开校验)
|
||||
*/
|
||||
@Override
|
||||
public List<String> getRoleList(Object loginId, String loginType) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中获取上下文对象
|
||||
*/
|
||||
private SpaceUserAuthContext getAuthContextByRequest() {
|
||||
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
|
||||
String contentType = request.getHeader(Header.CONTENT_TYPE.getValue());
|
||||
SpaceUserAuthContext authRequest;
|
||||
// 获取请求参数
|
||||
if (ContentType.JSON.getValue().equals(contentType)) {
|
||||
String body = ServletUtil.getBody(request);
|
||||
authRequest = JSONUtil.toBean(body, SpaceUserAuthContext.class);
|
||||
} else {
|
||||
Map<String, String> paramMap = ServletUtil.getParamMap(request);
|
||||
authRequest = BeanUtil.toBean(paramMap, SpaceUserAuthContext.class);
|
||||
}
|
||||
// 根据请求路径区分 id 字段的含义
|
||||
Long id = authRequest.getId();
|
||||
if (ObjUtil.isNotNull(id)) {
|
||||
// 获取到请求路径的业务前缀,/api/picture/aaa?a=1
|
||||
String requestURI = request.getRequestURI();
|
||||
// 先替换掉上下文,剩下的就是前缀
|
||||
String partURI = requestURI.replace(contextPath + "/", "");
|
||||
// 获取前缀的第一个斜杠前的字符串
|
||||
String moduleName = StrUtil.subBefore(partURI, "/", false);
|
||||
switch (moduleName) {
|
||||
case "picture":
|
||||
authRequest.setPictureId(id);
|
||||
break;
|
||||
case "spaceUser":
|
||||
authRequest.setSpaceUserId(id);
|
||||
break;
|
||||
case "space":
|
||||
authRequest.setSpaceId(id);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
}
|
||||
return authRequest;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断对象的所有字段是否为空
|
||||
*
|
||||
* @param object
|
||||
* @return
|
||||
*/
|
||||
private boolean isAllFieldsNull(Object object) {
|
||||
if (object == null) {
|
||||
return true; // 对象本身为空
|
||||
}
|
||||
// 获取所有字段并判断是否所有字段都为空
|
||||
return Arrays.stream(ReflectUtil.getFields(object.getClass()))
|
||||
// 获取字段值
|
||||
.map(field -> ReflectUtil.getFieldValue(object, field))
|
||||
// 检查是否所有字段都为空
|
||||
.allMatch(ObjectUtil::isEmpty);
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth;
|
||||
|
||||
import cn.dev33.satoken.stp.StpLogic;
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* StpLogic 门面类,管理项目中所有的 StpLogic 账号体系
|
||||
* 添加 @Component 注解的目的是确保静态属性 DEFAULT 和 SPACE 被初始化
|
||||
*/
|
||||
@Component
|
||||
public class StpKit {
|
||||
|
||||
public static final String SPACE_TYPE = "space";
|
||||
|
||||
/**
|
||||
* 默认原生会话对象,项目中目前没使用到
|
||||
*/
|
||||
public static final StpLogic DEFAULT = StpUtil.stpLogic;
|
||||
|
||||
/**
|
||||
* Space 会话对象,管理 Space 表所有账号的登录、权限认证
|
||||
*/
|
||||
public static final StpLogic SPACE = new StpLogic(SPACE_TYPE);
|
||||
}
|
@ -0,0 +1,57 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.annotation;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import cn.dev33.satoken.annotation.SaMode;
|
||||
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
/**
|
||||
* 空间权限认证:必须具有指定权限才能进入该方法
|
||||
* <p> 可标注在函数、类上(效果等同于标注在此类的所有方法上)
|
||||
*/
|
||||
@SaCheckPermission(type = StpKit.SPACE_TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target({ElementType.METHOD, ElementType.TYPE})
|
||||
public @interface SaSpaceCheckPermission {
|
||||
|
||||
/**
|
||||
* 需要校验的权限码
|
||||
*
|
||||
* @return 需要校验的权限码
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
String[] value() default {};
|
||||
|
||||
/**
|
||||
* 验证模式:AND | OR,默认AND
|
||||
*
|
||||
* @return 验证模式
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
SaMode mode() default SaMode.AND;
|
||||
|
||||
/**
|
||||
* 在权限校验不通过时的次要选择,两者只要其一校验成功即可通过校验
|
||||
*
|
||||
* <p>
|
||||
* 例1:@SaCheckPermission(value="user-add", orRole="admin"),
|
||||
* 代表本次请求只要具有 user-add权限 或 admin角色 其一即可通过校验。
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* 例2: orRole = {"admin", "manager", "staff"},具有三个角色其一即可。 <br>
|
||||
* 例3: orRole = {"admin, manager, staff"},必须三个角色同时具备。
|
||||
* </p>
|
||||
*
|
||||
* @return /
|
||||
*/
|
||||
@AliasFor(annotation = SaCheckPermission.class)
|
||||
String[] orRole() default {};
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.annotation;
|
||||
|
||||
import cn.dev33.satoken.interceptor.SaInterceptor;
|
||||
import cn.dev33.satoken.strategy.SaAnnotationStrategy;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotatedElementUtils;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
import javax.annotation.PostConstruct;
|
||||
|
||||
/**
|
||||
* Sa-Token 开启注解和配置
|
||||
*/
|
||||
@Configuration
|
||||
public class SaTokenConfigure implements WebMvcConfigurer {
|
||||
|
||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 注册 Sa-Token 拦截器,打开注解式鉴权功能
|
||||
registry.addInterceptor(new SaInterceptor()).addPathPatterns("/**");
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void rewriteSaStrategy() {
|
||||
// 重写Sa-Token的注解处理器,增加注解合并功能
|
||||
SaAnnotationStrategy.instance.getAnnotation = (element, annotationClass) -> {
|
||||
return AnnotatedElementUtils.getMergedAnnotation(element, annotationClass);
|
||||
};
|
||||
}
|
||||
}
|
@ -0,0 +1,25 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员权限配置
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserAuthConfig implements Serializable {
|
||||
|
||||
/**
|
||||
* 权限列表
|
||||
*/
|
||||
private List<SpaceUserPermission> permissions;
|
||||
|
||||
/**
|
||||
* 角色列表
|
||||
*/
|
||||
private List<SpaceUserRole> roles;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -0,0 +1,30 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 空间成员权限
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserPermission implements Serializable {
|
||||
|
||||
/**
|
||||
* 权限键
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 权限名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
/**
|
||||
* 空间成员权限常量
|
||||
*/
|
||||
public interface SpaceUserPermissionConstant {
|
||||
|
||||
/**
|
||||
* 空间用户管理权限
|
||||
*/
|
||||
String SPACE_USER_MANAGE = "spaceUser:manage";
|
||||
|
||||
/**
|
||||
* 图片查看权限
|
||||
*/
|
||||
String PICTURE_VIEW = "picture:view";
|
||||
|
||||
/**
|
||||
* 图片上传权限
|
||||
*/
|
||||
String PICTURE_UPLOAD = "picture:upload";
|
||||
|
||||
/**
|
||||
* 图片编辑权限
|
||||
*/
|
||||
String PICTURE_EDIT = "picture:edit";
|
||||
|
||||
/**
|
||||
* 图片删除权限
|
||||
*/
|
||||
String PICTURE_DELETE = "picture:delete";
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package edu.whut.smilepicturebackend.manager.auth.model;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 空间成员角色
|
||||
*/
|
||||
@Data
|
||||
public class SpaceUserRole implements Serializable {
|
||||
|
||||
/**
|
||||
* 角色键
|
||||
*/
|
||||
private String key;
|
||||
|
||||
/**
|
||||
* 角色名称
|
||||
*/
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 权限键列表
|
||||
*/
|
||||
private List<String> permissions;
|
||||
|
||||
/**
|
||||
* 角色描述
|
||||
*/
|
||||
private String description;
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
}
|
@ -282,8 +282,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
&& !userService.isAdmin(loginUser)) {
|
||||
throw new BusinessException(ErrorCode.NO_AUTH_ERROR);
|
||||
}
|
||||
// 校验权限
|
||||
checkPictureAuth(loginUser, oldPicture);
|
||||
// 校验权限,已改为注解鉴权
|
||||
// checkPictureAuth(loginUser, oldPicture);
|
||||
// 开启事务
|
||||
transactionTemplate.execute(status -> {
|
||||
// 操作数据库
|
||||
@ -317,8 +317,8 @@ public class PictureServiceImpl extends ServiceImpl<PictureMapper, Picture>
|
||||
long id = pictureEditRequest.getId();
|
||||
Picture oldPicture = this.getById(id);
|
||||
ThrowUtils.throwIf(oldPicture == null, ErrorCode.NOT_FOUND_ERROR);
|
||||
// 校验权限
|
||||
checkPictureAuth(loginUser, oldPicture);
|
||||
// 校验权限,已改为注解鉴权
|
||||
// checkPictureAuth(loginUser, oldPicture);
|
||||
// 补充审核 参数,每次编辑图片都要重新过审
|
||||
this.fillReviewParams(picture, loginUser);
|
||||
// 操作数据库
|
||||
|
@ -14,6 +14,7 @@ import edu.whut.smilepicturebackend.constant.UserConstant;
|
||||
import edu.whut.smilepicturebackend.exception.BusinessException;
|
||||
import edu.whut.smilepicturebackend.exception.ErrorCode;
|
||||
import edu.whut.smilepicturebackend.exception.ThrowUtils;
|
||||
import edu.whut.smilepicturebackend.manager.auth.StpKit;
|
||||
import edu.whut.smilepicturebackend.model.dto.user.UserAddRequest;
|
||||
import edu.whut.smilepicturebackend.model.dto.user.UserQueryRequest;
|
||||
import edu.whut.smilepicturebackend.model.entity.User;
|
||||
@ -127,6 +128,10 @@ public class UserServiceImpl extends ServiceImpl<UserMapper, User>
|
||||
}
|
||||
// 4. 保存用户的登录态
|
||||
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE, user);
|
||||
// 记录用户登录态到 Sa-token,便于空间鉴权时使用,注意保证该用户信息与 SpringSession 中的信息过期时间一致
|
||||
|
||||
StpKit.SPACE.login(user.getId());
|
||||
StpKit.SPACE.getSession().set(UserConstant.USER_LOGIN_STATE, user);
|
||||
return this.getLoginUserVO(user);
|
||||
}
|
||||
|
||||
|
62
src/main/resources/biz/spaceUserAuthConfig.json
Normal file
62
src/main/resources/biz/spaceUserAuthConfig.json
Normal file
@ -0,0 +1,62 @@
|
||||
{
|
||||
"permissions": [
|
||||
{
|
||||
"key": "spaceUser:manage",
|
||||
"name": "成员管理",
|
||||
"description": "管理空间成员,添加或移除成员"
|
||||
},
|
||||
{
|
||||
"key": "picture:view",
|
||||
"name": "查看图片",
|
||||
"description": "查看空间中的图片内容"
|
||||
},
|
||||
{
|
||||
"key": "picture:upload",
|
||||
"name": "上传图片",
|
||||
"description": "上传图片到空间中"
|
||||
},
|
||||
{
|
||||
"key": "picture:edit",
|
||||
"name": "修改图片",
|
||||
"description": "编辑已上传的图片信息"
|
||||
},
|
||||
{
|
||||
"key": "picture:delete",
|
||||
"name": "删除图片",
|
||||
"description": "删除空间中的图片"
|
||||
}
|
||||
],
|
||||
"roles": [
|
||||
{
|
||||
"key": "viewer",
|
||||
"name": "浏览者",
|
||||
"permissions": [
|
||||
"picture:view"
|
||||
],
|
||||
"description": "查看图片"
|
||||
},
|
||||
{
|
||||
"key": "editor",
|
||||
"name": "编辑者",
|
||||
"permissions": [
|
||||
"picture:view",
|
||||
"picture:upload",
|
||||
"picture:edit",
|
||||
"picture:delete"
|
||||
],
|
||||
"description": "查看图片、上传图片、修改图片、删除图片"
|
||||
},
|
||||
{
|
||||
"key": "admin",
|
||||
"name": "管理员",
|
||||
"permissions": [
|
||||
"spaceUser:manage",
|
||||
"picture:view",
|
||||
"picture:upload",
|
||||
"picture:edit",
|
||||
"picture:delete"
|
||||
],
|
||||
"description": "成员管理、查看图片、上传图片、修改图片、删除图片"
|
||||
}
|
||||
]
|
||||
}
|
6
src/main/resources/static/index.html
Normal file
6
src/main/resources/static/index.html
Normal file
@ -0,0 +1,6 @@
|
||||
<html>
|
||||
<body>
|
||||
<h1>hello word!!!</h1>
|
||||
<p>this is a html page</p>
|
||||
</body>
|
||||
</html>
|
Loading…
x
Reference in New Issue
Block a user