用户中心
登录逻辑
接受参数:用户账户,密码
接受类型:POST
请求体:JSON格式的数据
请求参数很长时不建议用get
返回值:用户信息(脱敏)
具体逻辑:
- 校验用户的账户,密码,是否符合要求
- 账户不少于四位
- 密码不少于四位
- 账户不能重复
- 账户不包含特殊字符
- 其他的校验
- 校验密码是否输入正确 ,要和数据库中的密码进行对比
- 记录用户的登录态(Session),存到服务器上,(用后端SpringBoot 框架封装的服务器tomcat去记录)
- 返回用户信息(脱敏)
如何知道是哪个用户?
- 连接服务器之后,得到一个session态,返回给前端
- 登录成功,得到登录成功的session,返回给前端一个设置cookie的命令 session => cookie
- 前端接收到后端命令后,设置cookie,保存到浏览器中
- 前端再次去请求后端的时候,在请求头中带着cookie去请求
- 后端拿到前端传来的cookie,找到对应的session
- 后端从session中可以取出基于该session存储的变量等。
登录service
/**
* 用户登录
*
* @param userLoginDto 用户登录信息
* @param request
* @return 脱敏后的用户信息
*/
User userLogin(UserLoginDto userLoginDto, HttpServletRequest request);
重点逻辑,用户脱敏:
//用户脱敏
User safetyUser = new User();
safetyUser.setId(user.getId());
safetyUser.setUsername(user.getUsername());
safetyUser.setUserAccount(userAccount);
safetyUser.setAvatarUrl(user.getAvatarUrl());
safetyUser.setGender(user.getGender());
safetyUser.setPhone(user.getPhone());
safetyUser.setEmail(user.getEmail());
safetyUser.setUserStatus(user.getUserStatus());
记录登录态:
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE,user);
完整代码:
public User userLogin(UserLoginDto userLoginDto, HttpServletRequest request) {
String userAccount = userLoginDto.getUserAccount();
String userPassword = userLoginDto.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
//todo 修改为自定义异常
return null;
}
if (userAccount.length() < 4) {
return null;
}
if (userPassword.length() < 4) {
return null;
}
//账户不能包含字符
//账户不能包含特殊字符
String validPattern = "^[a-zA-Z0-9_]+$";
Matcher matcher = Pattern.compile(validPattern).matcher(userAccount);
if (!matcher.find()) {
return null;
}
String encryptPassword = DigestUtils.md5DigestAsHex((SALT + userPassword).getBytes());
//查询用户是否存在
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
userQueryWrapper.eq("userAccount", userAccount);
userQueryWrapper.eq("userPassword", encryptPassword);
User user = userMapper.selectOne(userQueryWrapper);
if (user == null) {
log.info("user login failed, userAccount cannot match userPassword");
return null;
}
//用户脱敏
User safetyUser = new User();
safetyUser.setId(user.getId());
safetyUser.setUsername(user.getUsername());
safetyUser.setUserAccount(userAccount);
safetyUser.setAvatarUrl(user.getAvatarUrl());
safetyUser.setGender(user.getGender());
safetyUser.setPhone(user.getPhone());
safetyUser.setEmail(user.getEmail());
safetyUser.setUserStatus(user.getUserStatus());
//记录用户登录态
request.getSession().setAttribute(UserConstant.USER_LOGIN_STATE,user);
return safetyUser;
}
这里注意,系统设置的删除并不是真正的删除,而是逻辑删除(0-未删除 1-删除)可以在mybatis-plus中设置
mybatis-plus:
configuration:
map-underscore-to-camel-case: false
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
控制器UserController
提高效率的插件
下面这个插件可以自动填充函数 的参数
使用如下:
此时就可以自动填充了
完整逻辑如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterDto userRegisterDto) {
if (userRegisterDto == null) {
throw new RuntimeException("参数不能为空");
}
long id = userService.userRegister(userRegisterDto);
return id;
}
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginDto userLoginDto, HttpServletRequest request) {
if (userLoginDto == null) {
throw new RuntimeException("参数不能为空");
}
String userAccount = userLoginDto.getUserAccount();
String userPassword = userLoginDto.getUserPassword();
if (StringUtils.isAnyBlank(userAccount, userPassword)) {
throw new RuntimeException("用户名或密码不能为空");
}
return userService.userLogin(userLoginDto, request);
}
}
登录注册测试
使用RestfulTool 进行接口测试:
测试成功:
查询用户:
@GetMapping("/search")
public List<User> searchUsers(String username, HttpServletRequest request) {
if (!isAdmin(request)) {
return new ArrayList<>();
}
QueryWrapper<User> userQueryWrapper = new QueryWrapper<>();
if (StringUtils.isNotBlank(username)) {
userQueryWrapper.like("username", username);
}
List<User> userList = userService.list(userQueryWrapper);
return userList.stream().map(user -> {
return userService.getSafetyUser(user);
}).collect(Collectors.toList());
}
这里封装了一个用户脱敏的函数:
@Override
public User getSafetyUser(User user) {
User safetyUser = new User();
safetyUser.setId(user.getId());
safetyUser.setUsername(user.getUsername());
safetyUser.setUserAccount(user.getUserAccount());
safetyUser.setAvatarUrl(user.getAvatarUrl());
safetyUser.setGender(user.getGender());
safetyUser.setPhone(user.getPhone());
safetyUser.setEmail(user.getEmail());
safetyUser.setUserStatus(user.getUserStatus());
safetyUser.setUserRole(user.getUserRole());
return safetyUser;
}
删除用户:
@PostMapping("/delete/{id}")
public boolean deleteUser(@PathVariable Long id, HttpServletRequest request) {
if (!isAdmin(request)) {
return false;
}
if (id < 0) {
return false;
}
return userService.removeById(id);
}
判断是不是管理员:
private boolean isAdmin(HttpServletRequest request) {
//仅管理员可以查询
User user = (User) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
if (user == null || user.getUserRole() != UserConstant.ADMIN_USER_ROLE) {
return false;
}
return true;
}
前端代码编写
修改页脚代码为自己的信息:
{
key: 'github',
title: <GithubOutlined />,
href: 'https://github.com/yunfeidog',
blankTarget: true,
},
{
key: 'Blog',
title: 'Blog',
href: 'https://yunfeidog.github.io/blogv2/',
blankTarget: true,
},
页脚如下:
定义一个LOGO的常量:
export const SYSTEM_LOGO = "https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp"
使用:
logo={<img alt="logo" src={SYSTEM_LOGO} />}
删掉一些代码,保留登录页面:
修改前端登录参数:
type LoginParams = {
userAccount?: string;
userPassword?: string;
autoLogin?: boolean;
type?: string;
};
前后端交互
前端 需要向后端发送请求 ajax
axios封装了ajax
request 是ant design 项目又封装了aixos
追踪request源码,用到了umi插件,requesConfig是一个配置
代理
正向代理:替客户端向服务器发送请求
反向代理:替服务器接受请求。
Nginx服务器,nodejs服务器
原本请求:http://localhost:8000/api/user/login
代理之后:http://localhost:8080/user/login
proxy.ts文件:
dev: {
// localhost:8000/api/** -> http://localhost:8080/api/**
'/api/': {
// 要代理的地址
target: 'http://localhost:8080',
// 配置了这个可以从 http 代理到 https
// 依赖 origin 的功能可能需要这个,比如 cookie
changeOrigin: true,
//去掉 api 前缀
pathRewrite: { '^/api': '' },
},
成功测试登录:
注册页面
直接复制login页面进行修改即可:
添加一个路由:
{
path: '/user',
layout: false,
routes: [
{ name: '登录', path: '/user/login', component: './user/Login' },
{ name: '注册', path: '/user/register', component: './user/Register' },
{ component: './404' },
],
},
此时访问http://localhost:8000/user/register发现会重定向到login
在app.tsx中,需要修改onPageChange和fetchUserInfo函数
onPageChange: () => {
const {location} = history;
const whiteList = ['/user/register', loginPath]
// 如果在白名单中,不做任何处理
if (whiteList.includes(location.pathname)) {
return;
}
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
此时发现 注册按钮的字是登录
无法进行修改,因为这是procomponents里面写死的组件这里我们查看源码进行修改
找到登录按钮,这时就可以修改属性来 修改注册 按钮的值
修改如下:
成功:
注册逻辑:
const handleSubmit = async (values: API.RegisterParams) => {
const {userPassword, checkPassword} = values;
if (userPassword != checkPassword) {
message.error('两次输入的密码不一致!');
return;
}
try {
// 注册
const id = await register(values);
if (id > 0) {
const defaultLoginSuccessMessage = '注册成功!';
message.success(defaultLoginSuccessMessage);
/** 此方法会跳转到 redirect 参数所在的位置 */
if (!history) return;
const {query} = history.location;
history.push({
pathname: '/user/login',
query
})
return;
} else {
throw new Error(`register error id =${id}`)
}
// 如果失败去设置用户错误信息
} catch (error) {
const defaultLoginFailureMessage = '注册失败,请重试!';
message.error(defaultLoginFailureMessage);
}
};
注册 成功页面:
获取当前用户
获取当前用户后端接口:
@GetMapping("/current")
public User gerCurrentUser(HttpServletRequest request) {
User user = (User) request.getSession().getAttribute(UserConstant.USER_LOGIN_STATE);
if (user == null) {
return null;
}
Long userId = user.getId();
User user1 = userService.getById(userId);
return userService.getSafetyUser(user1);
}
修改前端每次刷新自动获取当前用户的逻辑:
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}> {
// 页面刚进入时,获取用户信息
const fetchUserInfo = async () => {
try {
return await queryCurrentUser();
} catch (error) {
// history.push(loginPath);
}
return undefined;
};
// 如果是无需登录的页面,不执行
if (WHITE_LIST.includes(history.location.pathname)) {
return {
fetchUserInfo,
settings: defaultSettings,
};
}
const currentUser = await fetchUserInfo();
return {
fetchUserInfo,
currentUser,
settings: defaultSettings,
};
}
查询用户表格
复制一份组件,修改为UserManage,并且在Admin目录下面:
Ant Design Pro(Umi 框架)
app.tsx 项目全局入口文件,定义了整个项目中使用的公共数据(比如用户信息)
access.ts 控制用户的访问权限
首次访问页面(刷新页面),进入 app.tsx,执行 getInitialState 方法,该方法的返回值就是全局可用的状态值。
access.ts代码:用户判断当前用户是不是管理员
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
return {
canAdmin: currentUser && currentUser.userRole === 1,
};
}
添加一个路由,用户管理:
{
path: '/admin',
name: '管理页',
icon: 'crown',
access: 'canAdmin',
component: './Admin', // 该组件下的路由都会被添加到路由配置中
routes: [
{ path: '/admin/user-manage', name: '用户管理', icon: 'smile', component: './Admin/UserManage' },
{ component: './404' },
],
},
修改Admin.tsx页面,使用children来控制子页面: 里面的children就是子页面
const Admin: React.FC = (props) => {
const {children} = props;
return (
<PageHeaderWrapper>
{children}
</PageHeaderWrapper>
);
};
export default Admin;
ProComponents 高级表单
- 通过 columns 定义表格有哪些列
- columns 属性
- dataIndex 对应返回数据对象的属性
- title 表格列名
- copyable 是否允许复制
- ellipsis 是否允许缩略
- valueType:用于声明这一列的类型(dateTime、select)
直接复制源代码进行使用
前端定义搜索用户接口:
/** 搜索用户 GET /api/search */
export async function searchUsers(options?: { [key: string]: any }) {
return request<API.CurrentUser[]>('/api/user/search', {
method: 'GET',
...(options || {}),
});
}
查询表格完整代码如下:
import type {ActionType, ProColumns} from '@ant-design/pro-components';
import {ProTable, TableDropdown} from '@ant-design/pro-components';
import {useRef} from 'react';
import {searchUsers} from "@/services/ant-design-pro/api";
import {SYSTEM_LOGO} from "@/constant";
const columns: ProColumns<API.CurrentUser>[] = [
{
dataIndex: 'id',
title: 'ID',
width: 48,
},
{
title: '用户名',
dataIndex: 'username',
copyable: true,
},
{
title: '用户账户',
dataIndex: 'userAccount',
copyable: true,
},
{
title: '用户头像',
dataIndex: 'avatarUrl',
render: (_, record) => (
<div>
<img src={record.avatarUrl ?? SYSTEM_LOGO} alt={"cxk"} width={100}/>
{/*<Image src={record.avatarUrl} width={100}/>*/}
</div>
),
copyable: true,
},
{
title: '性别',
dataIndex: 'gender',
},
{
title: '电话',
dataIndex: 'phone',
copyable: true,
},
{
title: '邮件',
dataIndex: 'email',
copyable: true,
},
{
title: '状态',
dataIndex: 'userStatus',
},
{
title: '角色',
dataIndex: 'userRole',
valueType: 'select',
valueEnum: {
0: {text: '普通用户', status: 'Default'},
1: {text: '管理员', status: 'Success'},
}
},
{
title: '创建时间',
dataIndex: 'createTime',
valueType: 'date',
},
{
title: '操作',
valueType: 'option',
key: 'option',
render: (text, record: any, _, action) => [
<a
key="editable"
onClick={() => {
action?.startEditable?.(record.id);
}}
>
编辑
</a>,
<a href={record.url} target="_blank" rel="noopener noreferrer" key="view">
查看
</a>,
<TableDropdown
key="actionGroup"
onSelect={() => action?.reload()}
menus={[
{key: 'copy', name: '复制'},
{key: 'delete', name: '删除'},
]}
/>,
],
},
];
export default () => {
const actionRef = useRef<ActionType>();
// @ts-ignore
return (
<ProTable<API.CurrentUser>
columns={columns}
actionRef={actionRef}
cardBordered
request={async (params = {}, sort, filter) => {
console.log(sort, filter);
const userList = await searchUsers();
return {
data: userList
}
}}
editable={{
type: 'multiple',
}}
columnsState={{
persistenceKey: 'pro-table-singe-demos',
persistenceType: 'localStorage',
onChange(value) {
console.log('value: ', value);
},
}}
rowKey="id"
search={{
labelWidth: 'auto',
}}
// options={{
// setting: {
// listsHeight: 400,
// },
// }}
form={{
// 由于配置了 transform, 提交的参与与定义的不同这里需要转化一下
syncToUrl: (values, type) => {
if (type === 'get') {
return {
...values,
created_at: [values.startTime, values.endTime],
};
}
return values;
},
}}
pagination={{
pageSize: 5,
onChange: (page) => console.log(page),
}}
dateFormatter="string"
headerTitle="高级表格"
/>
);
};
页面 效果如下:
用户注销
后端代码:
@PostMapping("/logout")
public Integer logout(HttpServletRequest request) {
if (request == null) {
return null;
}
return userService.logout(request);
}
@Override
public Integer logout(HttpServletRequest request) {
request.getSession().removeAttribute(UserConstant.USER_LOGIN_STATE);
return 1;
}
前端接口:
/** 退出登录接口 POST /api/user/logout */
export async function outLogin(options?: { [key: string]: any }) {
return request<Record<string, any>>('/api/user/logout', {
method: 'POST',
...(options || {}),
});
}