完成子账户接口设计

This commit is contained in:
柏码の讲师 2023-12-13 17:15:03 +08:00
parent 48acb638e7
commit fa630a1d89
11 changed files with 130 additions and 5 deletions

View File

@ -56,7 +56,8 @@ public class SecurityConfiguration {
.requestMatchers("/api/auth/**", "/error").permitAll()
.requestMatchers("/monitor/**").permitAll()
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
.anyRequest().hasAnyRole(Const.ROLE_DEFAULT)
.requestMatchers("/api/user/sub/**").hasRole(Const.ROLE_ADMIN)
.anyRequest().hasAnyRole(Const.ROLE_ADMIN, Const.ROLE_NORMAL)
)
.formLogin(conf -> conf
.loginProcessingUrl("/api/auth/login")

View File

@ -2,12 +2,16 @@ package com.example.controller;
import com.example.entity.RestBean;
import com.example.entity.vo.request.ChangePasswordVO;
import com.example.entity.vo.request.CreateSubAccountVO;
import com.example.entity.vo.response.SubAccountVO;
import com.example.service.AccountService;
import com.example.utils.Const;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/user")
public class UserController {
@ -22,4 +26,23 @@ public class UserController {
RestBean.success() : RestBean.failure(401, "原密码输入错误!");
}
@PostMapping("/sub/create")
public RestBean<Void> createSubAccount(@RequestBody @Valid CreateSubAccountVO vo) {
service.createSubAccount(vo);
return RestBean.success();
}
@GetMapping("/sub/delete")
public RestBean<Void> deleteSubAccount(int uid,
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
if(uid == userId)
return RestBean.failure(401, "非法参数");
service.deleteSubAccount(uid);
return RestBean.success();
}
@GetMapping("/sub/list")
public RestBean<List<SubAccountVO>> subAccountList() {
return RestBean.success(service.listSubAccount());
}
}

View File

@ -23,4 +23,5 @@ public class Account implements BaseData {
String email;
String role;
Date registerTime;
String clients;
}

View File

@ -0,0 +1,20 @@
package com.example.entity.vo.request;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.Size;
import lombok.Data;
import org.hibernate.validator.constraints.Length;
import java.util.List;
@Data
public class CreateSubAccountVO {
@Length(min = 1, max = 10)
String username;
@Email
String email;
@Length(min = 6, max = 20)
String password;
@Size(min = 1)
List<Integer> clients;
}

View File

@ -0,0 +1,12 @@
package com.example.entity.vo.response;
import com.alibaba.fastjson2.JSONArray;
import lombok.Data;
@Data
public class SubAccountVO {
int id;
String username;
String email;
JSONArray clientList;
}

View File

@ -19,6 +19,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
import java.util.ArrayList;
/**
* 用于对请求头中Jwt令牌进行校验的工具为当前请求添加用户验证信息
@ -60,6 +61,7 @@ public class JwtAuthenticationFilter extends OncePerRequestFilter {
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
request.setAttribute(Const.ATTR_USER_ID, utils.toId(jwt));
request.setAttribute(Const.ATTR_USER_ROLE, new ArrayList<>(user.getAuthorities()).get(0).getAuthority());
}
}
filterChain.doFilter(request, response);

View File

@ -3,13 +3,20 @@ package com.example.service;
import com.baomidou.mybatisplus.extension.service.IService;
import com.example.entity.dto.Account;
import com.example.entity.vo.request.ConfirmResetVO;
import com.example.entity.vo.request.CreateSubAccountVO;
import com.example.entity.vo.request.EmailResetVO;
import com.example.entity.vo.response.SubAccountVO;
import org.springframework.security.core.userdetails.UserDetailsService;
import java.util.List;
public interface AccountService extends IService<Account>, UserDetailsService {
Account findAccountByNameOrEmail(String text);
String registerEmailVerifyCode(String type, String email, String address);
String resetEmailAccountPassword(EmailResetVO info);
String resetConfirm(ConfirmResetVO info);
boolean changePassword(int id, String oldPass, String newPass);
void createSubAccount(CreateSubAccountVO vo);
void deleteSubAccount(int uid);
List<SubAccountVO> listSubAccount();
}

View File

@ -1,10 +1,13 @@
package com.example.service.impl;
import com.alibaba.fastjson2.JSONArray;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.example.entity.dto.Account;
import com.example.entity.vo.request.ConfirmResetVO;
import com.example.entity.vo.request.CreateSubAccountVO;
import com.example.entity.vo.request.EmailResetVO;
import com.example.entity.vo.response.SubAccountVO;
import com.example.mapper.AccountMapper;
import com.example.service.AccountService;
import com.example.utils.Const;
@ -19,6 +22,8 @@ import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@ -127,6 +132,35 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return true;
}
@Override
public void createSubAccount(CreateSubAccountVO vo) {
Account account = this.findAccountByNameOrEmail(vo.getEmail());
if(account != null)
throw new IllegalArgumentException("该电子邮件已被注册");
account = this.findAccountByNameOrEmail(vo.getUsername());
if(account != null)
throw new IllegalArgumentException("该用户名已被注册");
account = new Account(null, vo.getUsername(), passwordEncoder.encode(vo.getPassword()),
vo.getEmail(), Const.ROLE_NORMAL, new Date(), JSONArray.copyOf(vo.getClients()).toJSONString());
this.save(account);
}
@Override
public void deleteSubAccount(int uid) {
this.removeById(uid);
}
@Override
public List<SubAccountVO> listSubAccount() {
return this.list(Wrappers.<Account>query().eq("role", Const.ROLE_NORMAL))
.stream().map(account -> {
SubAccountVO vo = account.asViewObject(SubAccountVO.class);
vo.setClientList(JSONArray.parse(account.getClients()));
return vo;
}).toList();
}
/**
* 移除Redis中存储的邮件验证码
* @param email 电邮

View File

@ -7,6 +7,8 @@ public final class Const {
//JWT令牌
public final static String JWT_BLACK_LIST = "jwt:blacklist:";
public final static String JWT_FREQUENCY = "jwt:frequency:";
//用户
public final static String USER_BLACK_LIST = "user:blacklist:";
//请求频率限制
public final static String FLOW_LIMIT_COUNTER = "flow:counter:";
public final static String FLOW_LIMIT_BLOCK = "flow:block:";
@ -18,10 +20,11 @@ public final class Const {
public final static int ORDER_CORS = -102;
//请求自定义属性
public final static String ATTR_USER_ID = "userId";
public final static String ATTR_USER_ROLE = "userRole";
public final static String ATTR_CLIENT = "client";
//消息队列
public final static String MQ_MAIL = "mail";
//用户角色
public final static String ROLE_DEFAULT = "admin";
public final static String ROLE_ADMIN = "admin";
public final static String ROLE_NORMAL = "user";
}

View File

@ -109,6 +109,7 @@ public class JwtUtils {
try {
DecodedJWT verify = jwtVerifier.verify(token);
if(this.isInvalidToken(verify.getId())) return null;
if(this.isInvalidUser(verify.getClaim("id").asInt())) return null;
Map<String, Claim> claims = verify.getClaims();
return new Date().after(claims.get("exp").asDate()) ? null : verify;
} catch (JWTVerificationException e) {
@ -177,6 +178,14 @@ public class JwtUtils {
return true;
}
public void deleteUser(int uid) {
template.opsForValue().set(Const.USER_BLACK_LIST + uid, "", expire, TimeUnit.HOURS);
}
private boolean isInvalidUser(int uid){
return Boolean.TRUE.equals(template.hasKey(Const.USER_BLACK_LIST + uid));
}
/**
* 验证Token是否被列入Redis黑名单
* @param uuid 令牌ID

View File

@ -1,6 +1,6 @@
<script setup>
import {reactive, ref} from "vue";
import {Lock, Switch} from "@element-plus/icons-vue";
import {Lock, Plus, Switch} from "@element-plus/icons-vue";
import {logout, post} from "@/net";
import {ElMessage} from "element-plus";
import router from "@/router";
@ -56,6 +56,8 @@ function resetPassword() {
<div style="display: flex;gap: 10px">
<div style="flex: 50%">
<div class="info-card">
<div class="title"><i class="fa-solid fa-lock"></i> 修改密码</div>
<el-divider style="margin: 10px 0"/>
<el-form @validate="onValidate" :model="form" :rules="rules"
ref="formRef" style="margin: 20px" label-width="100">
<el-form-item label="当前密码" prop="password">
@ -81,7 +83,11 @@ function resetPassword() {
</div>
</div>
<div class="info-card" style="flex: 50%">
<div class="title"><i class="fa-solid fa-users"></i> 子用户管理</div>
<el-divider style="margin: 10px 0"/>
<el-empty :image-size="100" description="还没有任何子用户哦">
<el-button :icon="Plus" type="primary" plain>添加子用户</el-button>
</el-empty>
</div>
</div>
</template>
@ -91,5 +97,12 @@ function resetPassword() {
border-radius: 7px;
padding: 15px 20px;
background-color: var(--el-bg-color);
height: fit-content;
.title {
font-size: 18px;
font-weight: bold;
color: dodgerblue;
}
}
</style>