跳到主要内容

用户中心

登录逻辑

接受参数:用户账户,密码

接受类型:POST

请求体:JSON格式的数据

请求参数很长时不建议用get

返回值:用户信息(脱敏)

具体逻辑:

  1. 校验用户的账户,密码,是否符合要求
    1. 账户不少于四位
    2. 密码不少于四位
    3. 账户不能重复
    4. 账户不包含特殊字符
    5. 其他的校验
  2. 校验密码是否输入正确 ,要和数据库中的密码进行对比
  3. 记录用户的登录态(Session),存到服务器上,(用后端SpringBoot 框架封装的服务器tomcat去记录)
  4. 返回用户信息(脱敏)

如何知道是哪个用户?

  1. 连接服务器之后,得到一个session态,返回给前端
  2. 登录成功,得到登录成功的session,返回给前端一个设置cookie的命令 session => cookie
  3. 前端接收到后端命令后,设置cookie,保存到浏览器中
  4. 前端再次去请求后端的时候,在请求头中带着cookie去请求
  5. 后端拿到前端传来的cookie,找到对应的session
  6. 后端从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

提高效率的插件

下面这个插件可以自动填充函数 的参数

image-20231016195611322

使用如下:

image-20231016195719640

此时就可以自动填充了

image-20231016195845930

完整逻辑如下:

@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 进行接口测试:

image-20231016200612792

测试成功:

image-20231016200709652

查询用户:

@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,
},

页脚如下:

image-20231017154227098

定义一个LOGO的常量:

export const SYSTEM_LOGO = "https://s2.loli.net/2023/10/16/QRiUYmDLB2vZuE6.webp"

使用:

          logo={<img alt="logo" src={SYSTEM_LOGO} />}

删掉一些代码,保留登录页面:

image-20231017155255273

修改前端登录参数:

  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': '' },
},

成功测试登录:

image-20231017174107430

注册页面

直接复制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里面写死的组件这里我们查看源码进行修改

image-20231017181304607

找到登录按钮,这时就可以修改属性来 修改注册 按钮的值

image-20231017181527032

修改如下:

image-20231017181647734

成功:

image-20231017181710615

注册逻辑:

  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);
}
};

注册 成功页面:

image-20231017183406608

获取当前用户

获取当前用户后端接口:

    @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目录下面:

image-20231017192334416

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 高级表单

  1. 通过 columns 定义表格有哪些列
  2. columns 属性
    • dataIndex 对应返回数据对象的属性
    • title 表格列名
    • copyable 是否允许复制
    • ellipsis 是否允许缩略
    • valueType:用于声明这一列的类型(dateTime、select)

image-20231017194039521

直接复制源代码进行使用

前端定义搜索用户接口:

/** 搜索用户 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="高级表格"
/>
);
};

页面 效果如下:

image-20231017203631534

用户注销

后端代码:

    @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 || {}),
});
}