添加子用户相关接口

This commit is contained in:
柏码の讲师 2023-12-03 22:46:17 +08:00
parent ed25cba5c6
commit 74ce37b72c
14 changed files with 165 additions and 30 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

@ -0,0 +1,47 @@
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 {
@Resource
AccountService service;
@PostMapping("/change-password")
public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo,
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ?
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

@ -1,24 +0,0 @@
package com.example.controller.exception;
import com.example.entity.RestBean;
import com.example.entity.vo.request.ChangePasswordVO;
import com.example.service.AccountService;
import com.example.utils.Const;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/api/user")
public class UserController {
@Resource
AccountService service;
@PostMapping("/change-password")
public RestBean<Void> changePassword(@RequestBody @Valid ChangePasswordVO vo,
@RequestAttribute(Const.ATTR_USER_ID) int userId) {
return service.changePassword(userId, vo.getPassword(), vo.getNew_password()) ?
RestBean.success() : RestBean.failure(401, "原密码输入错误");
}
}

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<String> 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 name;
String email;
JSONArray clients;
}

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令牌进行校验的工具为当前请求添加用户验证信息
@ -61,6 +62,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,14 +1,18 @@
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;
import com.example.utils.FlowUtils;
import com.example.utils.JwtUtils;
import jakarta.annotation.Resource;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Value;
@ -19,6 +23,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;
@ -45,6 +51,9 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
@Resource
FlowUtils flow;
@Resource
JwtUtils jwt;
/**
* 从数据库中通过用户名或邮箱查找用户详细信息
* @param username 用户名
@ -127,6 +136,31 @@ public class AccountServiceImpl extends ServiceImpl<AccountMapper, Account> impl
return true;
}
@Override
public void createSubAccount(CreateSubAccountVO vo) {
Account 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);
jwt.deleteUser(uid);
}
@Override
public List<SubAccountVO> listSubAccount() {
return this.list(Wrappers.<Account>query().eq("role", "user"))
.stream().map(account -> {
SubAccountVO vo = account.asViewObject(SubAccountVO.class);
vo.setClients(JSONArray.parse(account.getClients()));
return vo;
}).toList();
}
/**
* 移除Redis中存储的邮件验证码
* @param email 电邮

View File

@ -172,7 +172,6 @@ public class ClientServiceImpl extends ServiceImpl<ClientMapper, Client> impleme
StringBuilder sb = new StringBuilder(24);
for (int i = 0; i < 24; i++)
sb.append(CHARACTERS.charAt(random.nextInt(CHARACTERS.length())));
System.out.println(sb);
return sb.toString();
}
}

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,9 +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) {
@ -140,6 +141,14 @@ public class JwtUtils {
return claims.get("id").asInt();
}
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));
}
/**
* 频率检测防止用户高频申请Jwt令牌并且采用阶段封禁机制
* 如果已经提示无法登录的情况下用户还在刷那么就封禁更长时间

View File

@ -44,17 +44,27 @@ import {Back, Moon, Sunny} from "@element-plus/icons-vue";
import TabItem from "@/component/TabItem.vue";
import {ref} from "vue";
import {useDark} from "@vueuse/core";
import {useRoute} from "vue-router";
function userLogout() {
logout(() => router.push("/"))
}
const route = useRoute()
const defaultIndex = () => {
for (let tab of tabs) {
if(route.name === tab.route)
return tab.id
}
return 1
}
const dark = ref(useDark())
const tab = ref(1)
const tabs = [
{id: 1, name: '管理', route: 'manage'},
{id: 2, name: '安全', route: 'security'}
]
const tab = ref(defaultIndex())
function changePage(item) {
tab.value = item.id
router.push({name: item.route})

View File

@ -2,7 +2,7 @@
import {reactive, ref} from "vue";
import {logout, post} from "@/net";
import {ElMessage} from "element-plus";
import {Lock, Switch} from "@element-plus/icons-vue";
import {Lock, Plus, Switch} from "@element-plus/icons-vue";
import router from "@/router";
const formRef = ref()
@ -54,6 +54,8 @@ function resetPassword() {
<template>
<div style="display: flex;gap: 10px">
<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">
@ -75,7 +77,11 @@ function resetPassword() {
</el-form>
</div>
<div class="info-card">
<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>
@ -84,7 +90,14 @@ function resetPassword() {
.info-card {
border-radius: 7px;
width: 100%;
height: fit-content;
padding: 15px 20px;
background-color: var(--el-bg-color);
.title {
font-size: 18px;
font-weight: bold;
color: dodgerblue;
}
}
</style>