feat: 开发中...

This commit is contained in:
2023-03-17 03:55:20 +08:00
parent 8233fd5141
commit 9949fba3ed
25 changed files with 325 additions and 67 deletions

View File

@@ -1,15 +1,16 @@
package cn.hamster3.application.blog.config;
import cn.hamster3.application.blog.config.security.BlogUser;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.domain.AuditorAware;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.Optional;
import java.util.UUID;
@Configuration
public class WebConfig {
@@ -19,14 +20,14 @@ public class WebConfig {
}
@Bean
public AuditorAware<String> getUserIDAuditorAware() {
public AuditorAware<UUID> getUserIDAuditorAware() {
return () -> {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
User user = (User) authentication.getPrincipal();
return Optional.of(user.getUsername());
BlogUser user = (BlogUser) authentication.getPrincipal();
return Optional.of(user.getUuid());
};
}

View File

@@ -0,0 +1,18 @@
package cn.hamster3.application.blog.config.security;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.User;
import java.util.Collection;
import java.util.UUID;
@Getter
public class BlogUser extends User {
private final UUID uuid;
public BlogUser(String username, String password, Collection<? extends GrantedAuthority> authorities, UUID uuid) {
super(username, password, authorities);
this.uuid = uuid;
}
}

View File

@@ -1,6 +1,7 @@
package cn.hamster3.application.blog.config.security;
import lombok.extern.slf4j.Slf4j;
import org.jetbrains.annotations.NotNull;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@@ -32,7 +33,7 @@ public class DevSecurityConfiguration {
log.info("add cors configuration...");
return new WebMvcConfigurer() {
@Override
public void addCorsMappings(CorsRegistry registry) {
public void addCorsMappings(@NotNull CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("*")

View File

@@ -1,10 +1,8 @@
package cn.hamster3.application.blog.config.security;
import cn.hamster3.application.blog.constant.UserPermissions;
import cn.hamster3.application.blog.dao.UserRepository;
import cn.hamster3.application.blog.entity.UserEntity;
import jakarta.annotation.Resource;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
@@ -17,14 +15,15 @@ public class UserDetailServiceImpl implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepo.findByEmailOrNicknameAllIgnoreCase(username, username, UserEntity.class)
.map(user -> new User(
user.getId().toString(),
return userRepo.findByEmailIgnoreCaseWithPermission(username)
.map(user -> new BlogUser(
user.getEmail(),
user.getPassword(),
user.getPermissions()
.stream()
.map(permission -> new SimpleGrantedAuthority(permission.name()))
.toList()
.map(UserPermissions::getAuthority)
.toList(),
user.getId()
)).orElseThrow(() -> new UsernameNotFoundException("user not found."));
}
}

View File

@@ -1,5 +0,0 @@
package cn.hamster3.application.blog.constant;
public enum UserPermission {
}

View File

@@ -0,0 +1,17 @@
package cn.hamster3.application.blog.constant;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
public enum UserPermissions {
MODIFY_USER_INFO,
MODIFY_USER_PERMISSION;
private final SimpleGrantedAuthority authority;
UserPermissions() {
this.authority = new SimpleGrantedAuthority(name());
}
public SimpleGrantedAuthority getAuthority() {
return authority;
}
}

View File

@@ -1,16 +0,0 @@
package cn.hamster3.application.blog.constant;
public enum UserRole {
/**
* 游客
*/
GUEST,
/**
* 作者
*/
AUTHOR,
/**
* 管理员
*/
ADMINISTRATOR
}

View File

@@ -0,0 +1,13 @@
package cn.hamster3.application.blog.controller;
import cn.hamster3.application.blog.vo.ResponseVO;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class ExceptionController {
@ExceptionHandler(Exception.class)
public ResponseVO<String> onException(Exception e) {
return ResponseVO.failed(e);
}
}

View File

@@ -2,10 +2,8 @@ package cn.hamster3.application.blog.controller;
import cn.hamster3.application.blog.service.IUserService;
import cn.hamster3.application.blog.vo.ResponseVO;
import cn.hamster3.application.blog.vo.user.UserInfoResponseVO;
import cn.hamster3.application.blog.vo.user.UserRegisterRequireVO;
import cn.hamster3.application.blog.vo.user.UserRegisterResponseVO;
import cn.hamster3.application.blog.vo.user.UserUpdateRequireVO;
import cn.hamster3.application.blog.vo.attach.AttachInfoResponseVO;
import cn.hamster3.application.blog.vo.user.*;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.http.MediaType;
@@ -13,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
/**
* 用户相关接口
@@ -23,14 +22,19 @@ public class UserController {
@Resource
private IUserService userService;
@PostMapping("/login")
public ResponseVO<Void> loginUser(@RequestBody @Valid UserLoginRequireVO requireVO) {
return userService.loginUser(requireVO);
}
@PostMapping("/")
public ResponseVO<UserRegisterResponseVO> createUser(@RequestBody @Valid UserRegisterRequireVO requireVO) {
return userService.registerUser(requireVO);
public ResponseVO<UserRegisterResponseVO> createUser(@RequestBody @Valid UserCreateRequireVO requireVO) {
return userService.createUser(requireVO);
}
@PutMapping("/")
public ResponseVO<Void> updateUser(@RequestBody @Valid UserUpdateRequireVO requireVO) {
return ResponseVO.success();
return userService.updateUser(requireVO);
}
@GetMapping("/")
@@ -39,12 +43,12 @@ public class UserController {
}
@GetMapping("/{userID}/")
public ResponseVO<UserInfoResponseVO> getUserInfo(@PathVariable String userID) {
return ResponseVO.success(null);
public ResponseVO<UserInfoResponseVO> getUserInfo(@PathVariable UUID userID) {
return userService.getUserInfo(userID);
}
@GetMapping("/{userID}/attaches")
public ResponseVO<Void> getUserAttaches(@PathVariable String userID) {
return ResponseVO.success(null);
public ResponseVO<List<AttachInfoResponseVO>> getUserAttaches(@PathVariable UUID userID) {
return userService.getUserAttaches(userID);
}
}

View File

@@ -0,0 +1,8 @@
package cn.hamster3.application.blog.dao;
import cn.hamster3.application.blog.entity.AttachEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface AttachRepository extends JpaRepository<AttachEntity, Long>, JpaSpecificationExecutor<AttachEntity> {
}

View File

@@ -10,10 +10,14 @@ import java.util.Optional;
import java.util.UUID;
public interface UserRepository extends JpaRepository<UserEntity, UUID>, JpaSpecificationExecutor<BlogEntity> {
@EntityGraph(attributePaths = {"permissions"})
Optional<UserEntity> findByEmailIgnoreCaseWithPermission(String email);
boolean existsByNicknameIgnoreCase(String nickname);
boolean existsByEmailIgnoreCase(String email);
@EntityGraph(attributePaths = {"permissions"})
<T> Optional<T> findByEmailOrNicknameAllIgnoreCase(String email, String nickname, Class<T> type);
@EntityGraph(attributePaths = {"attachEntities"})
UserEntity findByIdWithAttach(UUID uuid);
}

View File

@@ -1,7 +1,6 @@
package cn.hamster3.application.blog.entity;
import cn.hamster3.application.blog.constant.UserPermission;
import cn.hamster3.application.blog.constant.UserRole;
import cn.hamster3.application.blog.constant.UserPermissions;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.Setter;
@@ -36,19 +35,21 @@ public class UserEntity {
@Column(name = "password", nullable = false, length = 32)
private String password;
@Setter
@ToString.Exclude
@ElementCollection
@Column(name = "permission")
@CollectionTable(name = "user_entity_permissions", joinColumns = @JoinColumn(name = "user_id"))
private Set<UserPermission> permissions = new HashSet<>();
@Enumerated
@Column(name = "role", nullable = false)
private UserRole role;
private Set<UserPermissions> permissions = new HashSet<>();
@Setter
@ToString.Exclude
@OneToMany(mappedBy = "uploader", orphanRemoval = true)
@OrderBy("create_time DESC")
private List<AttachEntity> attachEntities = new ArrayList<>();
@Setter
@ToString.Exclude
@OneToMany(mappedBy = "uploader", orphanRemoval = true)
@OrderBy("create_time DESC")
private List<BlogEntity> blogEntities = new ArrayList<>();

View File

@@ -0,0 +1,13 @@
package cn.hamster3.application.blog.entity.mapper;
import cn.hamster3.application.blog.entity.AttachEntity;
import cn.hamster3.application.blog.vo.attach.AttachInfoResponseVO;
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
import org.mapstruct.ReportingPolicy;
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface AttachMapper {
AttachInfoResponseVO entityToInfoVO(AttachEntity entity);
}

View File

@@ -1,16 +1,19 @@
package cn.hamster3.application.blog.entity.mapper;
import cn.hamster3.application.blog.entity.UserEntity;
import cn.hamster3.application.blog.vo.user.UserCreateRequireVO;
import cn.hamster3.application.blog.vo.user.UserInfoResponseVO;
import cn.hamster3.application.blog.vo.user.UserRegisterRequireVO;
import cn.hamster3.application.blog.vo.user.UserRegisterResponseVO;
import cn.hamster3.application.blog.vo.user.UserUpdateRequireVO;
import org.mapstruct.Mapper;
import org.mapstruct.MappingConstants;
import org.mapstruct.ReportingPolicy;
@Mapper(unmappedTargetPolicy = ReportingPolicy.IGNORE, componentModel = MappingConstants.ComponentModel.SPRING)
public interface UserMapper {
UserEntity voToEntity(UserRegisterRequireVO requireVO);
UserEntity voToEntity(UserCreateRequireVO requireVO);
UserEntity voToEntity(UserUpdateRequireVO requireVO);
UserInfoResponseVO entityToInfoVO(UserEntity requireVO);

View File

@@ -2,7 +2,8 @@ package cn.hamster3.application.blog.service;
import cn.hamster3.application.blog.vo.ResponseVO;
import cn.hamster3.application.blog.vo.blog.BlogCreateRequireVO;
import org.jetbrains.annotations.NotNull;
public interface IBlogService {
ResponseVO<Long> createBlog(BlogCreateRequireVO requireVO);
@NotNull ResponseVO<Long> createBlog(@NotNull BlogCreateRequireVO requireVO);
}

View File

@@ -1,10 +1,22 @@
package cn.hamster3.application.blog.service;
import cn.hamster3.application.blog.vo.ResponseVO;
import cn.hamster3.application.blog.vo.user.UserRegisterRequireVO;
import cn.hamster3.application.blog.vo.user.UserRegisterResponseVO;
import cn.hamster3.application.blog.vo.attach.AttachInfoResponseVO;
import cn.hamster3.application.blog.vo.user.*;
import org.jetbrains.annotations.NotNull;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
import java.util.UUID;
public interface IUserService {
@NotNull ResponseVO<UserRegisterResponseVO> registerUser(@NotNull UserRegisterRequireVO requireVO);
@NotNull ResponseVO<Void> loginUser(@NotNull UserLoginRequireVO requireVO);
@NotNull ResponseVO<UserRegisterResponseVO> createUser(@NotNull UserCreateRequireVO requireVO);
@NotNull ResponseVO<Void> updateUser(@NotNull UserUpdateRequireVO requireVO);
@NotNull ResponseVO<UserInfoResponseVO> getUserInfo(UUID id);
@NotNull ResponseVO<List<AttachInfoResponseVO>> getUserAttaches(@PathVariable UUID userID);
}

View File

@@ -7,6 +7,7 @@ import cn.hamster3.application.blog.service.IBlogService;
import cn.hamster3.application.blog.vo.ResponseVO;
import cn.hamster3.application.blog.vo.blog.BlogCreateRequireVO;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Service;
@Service
@@ -17,7 +18,7 @@ public class BlogService implements IBlogService {
private BlogRepository blogRepo;
@Override
public ResponseVO<Long> createBlog(BlogCreateRequireVO requireVO) {
public @NotNull ResponseVO<Long> createBlog(@NotNull BlogCreateRequireVO requireVO) {
BlogEntity entity = blogMapper.voToEntity(requireVO);
BlogEntity save = blogRepo.save(entity);
return ResponseVO.success(save.getId());

View File

@@ -1,28 +1,78 @@
package cn.hamster3.application.blog.service.impl;
import cn.hamster3.application.blog.config.security.BlogUser;
import cn.hamster3.application.blog.constant.UserPermissions;
import cn.hamster3.application.blog.dao.UserRepository;
import cn.hamster3.application.blog.entity.UserEntity;
import cn.hamster3.application.blog.entity.mapper.AttachMapper;
import cn.hamster3.application.blog.entity.mapper.UserMapper;
import cn.hamster3.application.blog.service.IUserService;
import cn.hamster3.application.blog.vo.ResponseVO;
import cn.hamster3.application.blog.vo.user.UserRegisterRequireVO;
import cn.hamster3.application.blog.vo.user.UserRegisterResponseVO;
import cn.hamster3.application.blog.vo.attach.AttachInfoResponseVO;
import cn.hamster3.application.blog.vo.user.*;
import jakarta.annotation.Resource;
import org.jetbrains.annotations.NotNull;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.web.bind.annotation.PathVariable;
import java.util.List;
import java.util.UUID;
@Service
public class UserService implements IUserService {
@Resource
private PasswordEncoder passwordEncoder;
@Resource
private UserMapper userMapper;
@Resource
private AttachMapper attachMapper;
@Resource
private UserRepository userRepo;
@Resource
private AuthenticationManager authenticationManager;
@Override
public @NotNull ResponseVO<UserRegisterResponseVO> registerUser(@NotNull UserRegisterRequireVO requireVO) {
public @NotNull ResponseVO<Void> loginUser(@NotNull UserLoginRequireVO requireVO) {
UserEntity userEntity = userRepo.findByEmailIgnoreCaseWithPermission(requireVO.getEmail()).orElse(null);
if (userEntity == null) {
return ResponseVO.failed("未找到该邮箱!");
}
if (!passwordEncoder.matches(requireVO.getPassword(), userEntity.getPassword())) {
return ResponseVO.failed("密码错误!");
}
List<SimpleGrantedAuthority> userPermissions = userEntity.getPermissions().stream()
.map(UserPermissions::getAuthority)
.toList();
BlogUser blogUser = new BlogUser(
userEntity.getEmail(),
userEntity.getPassword(),
userPermissions,
userEntity.getId()
);
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
blogUser,
requireVO.getPassword(),
blogUser.getAuthorities()
);
authenticationManager.authenticate(token);
SecurityContextHolder.getContext().setAuthentication(token);
return ResponseVO.success();
}
@Override
public @NotNull ResponseVO<UserRegisterResponseVO> createUser(@NotNull UserCreateRequireVO requireVO) {
UserEntity entity = userMapper.voToEntity(requireVO);
entity.setEmail(entity.getEmail().toLowerCase());
@@ -39,4 +89,69 @@ public class UserService implements IUserService {
return ResponseVO.success("注册成功!", userMapper.entityToRegisterVO(save));
}
@Override
public @NotNull ResponseVO<Void> updateUser(@NotNull UserUpdateRequireVO requireVO) {
UserEntity userEntity = userRepo.findById(requireVO.getId()).orElse(null);
if (userEntity == null) {
return ResponseVO.failed("未找到该用户!");
}
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null) {
return ResponseVO.failed("你没有这个权限!");
}
BlogUser blogUser = (BlogUser) authentication.getPrincipal();
if (!blogUser.getUuid().equals(userEntity.getId())
&& blogUser.getAuthorities().contains(UserPermissions.MODIFY_USER_INFO.getAuthority())) {
return ResponseVO.failed("你没有这个权限!");
}
if (requireVO.getEmail() != null) {
if (userRepo.existsByEmailIgnoreCase(requireVO.getEmail())) {
return ResponseVO.failed("已存在相同邮箱的账户!");
}
userEntity.setEmail(requireVO.getEmail().toLowerCase());
}
if (requireVO.getNickname() != null) {
if (userRepo.existsByNicknameIgnoreCase(requireVO.getNickname())) {
return ResponseVO.failed("已存在相同的用户昵称!");
}
userEntity.setNickname(requireVO.getNickname());
}
if (requireVO.getPassword() != null) {
userEntity.setPassword(passwordEncoder.encode(requireVO.getPassword()));
}
if (requireVO.getPermissions() != null) {
if (!blogUser.getAuthorities().contains(UserPermissions.MODIFY_USER_PERMISSION.getAuthority())) {
return ResponseVO.failed("你没有这个权限!");
}
// 用户必须具有 MODIFY_USER_PERMISSION 权限,且操作对象不为用户自己时才能更改权限
if (blogUser.getUuid().equals(userEntity.getId())) {
return ResponseVO.failed("你不能更改自己的权限!");
}
userEntity.setPermissions(requireVO.getPermissions());
}
return ResponseVO.success();
}
@Override
public @NotNull ResponseVO<UserInfoResponseVO> getUserInfo(UUID id) {
return userRepo.findById(id)
.map(o -> ResponseVO.success(userMapper.entityToInfoVO(o)))
.orElse(ResponseVO.failed("未找到该用户!"));
}
@Override
public @NotNull ResponseVO<List<AttachInfoResponseVO>> getUserAttaches(@PathVariable UUID userID) {
UserEntity userEntity = userRepo.findByIdWithAttach(userID);
if (userEntity == null) {
return ResponseVO.failed("未找到该用户!");
}
List<AttachInfoResponseVO> list = userEntity.getAttachEntities()
.stream().map(o -> attachMapper.entityToInfoVO(o))
.toList();
return ResponseVO.success(list);
}
}

View File

@@ -5,6 +5,9 @@ import lombok.Data;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.io.PrintWriter;
import java.io.StringWriter;
@Data
@AllArgsConstructor
public class ResponseVO<T> {
@@ -31,4 +34,11 @@ public class ResponseVO<T> {
return new ResponseVO<>(403, msg, null);
}
public static ResponseVO<String> failed(Exception e) {
StringWriter writer = new StringWriter();
e.printStackTrace(new PrintWriter(writer));
// StringWriter 不需要 close()
return new ResponseVO<>(403, e.getMessage(), writer.toString());
}
}

View File

@@ -0,0 +1,14 @@
package cn.hamster3.application.blog.vo.attach;
import lombok.Data;
import java.util.Date;
import java.util.UUID;
@Data
public class AttachInfoResponseVO {
private Long id;
private UUID uploader;
private Date createTime;
private Date updateTime;
}

View File

@@ -7,7 +7,7 @@ import lombok.Data;
@Data
@AllArgsConstructor
public class UserRegisterRequireVO {
public class UserCreateRequireVO {
@Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", message = "邮箱配置不合法!")
private String email;
@Size(min = 3, max = 16, message = "用户昵称必须包含 3~16 个字符!")

View File

@@ -1,4 +1,14 @@
package cn.hamster3.application.blog.vo.user;
import cn.hamster3.application.blog.constant.UserPermissions;
import java.util.Set;
import java.util.UUID;
public class UserInfoResponseVO {
private UUID id;
private String email;
private String nickname;
private String password;
private Set<UserPermissions> permissions;
}

View File

@@ -0,0 +1,15 @@
package cn.hamster3.application.blog.vo.user;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.AllArgsConstructor;
import lombok.Data;
@Data
@AllArgsConstructor
public class UserLoginRequireVO {
@Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", message = "邮箱不合法!")
private String email;
@Size(min = 8, max = 16, message = "密码必须包含 8~16 个字符!")
private String password;
}

View File

@@ -1,6 +1,6 @@
package cn.hamster3.application.blog.vo.user;
import cn.hamster3.application.blog.constant.UserPermission;
import cn.hamster3.application.blog.constant.UserPermissions;
import lombok.AllArgsConstructor;
import lombok.Data;
@@ -14,7 +14,7 @@ public class UserRegisterResponseVO {
private UUID id;
private String email;
private String nickname;
private List<UserPermission> permissions;
private List<UserPermissions> permissions;
private Date createTime;
private Date updateTime;
}

View File

@@ -1,4 +1,23 @@
package cn.hamster3.application.blog.vo.user;
import cn.hamster3.application.blog.constant.UserPermissions;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.util.Set;
import java.util.UUID;
@Data
public class UserUpdateRequireVO {
@NotNull(message = "用户id不能为空")
private UUID id;
@Pattern(regexp = "^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", message = "邮箱配置不合法!")
private String email;
@Size(min = 3, max = 16, message = "用户昵称必须包含 3~16 个字符!")
private String nickname;
@Size(min = 8, max = 16, message = "密码必须包含 8~16 个字符!")
private String password;
private Set<UserPermissions> permissions;
}