文章目录
登录功能(第三天)
接口设计
接受参数:用户账户、密码
请求类型:POST
请求体:JSON 格式的数据
请求参数很长时不建议用 get
返回值:用户信息( 脱敏 )信息脱敏是指对某些敏感信息通过脱敏规则进行数据的变形,实现敏感隐私数据的可靠保护。 数据脱敏可以分为两种:静态数据脱敏和动态数据脱敏。
登录逻辑
-
校验用户账户和密码是否合法
- 非空
- 账户长度 不小于 4 位
- 密码就 不小于 8 位吧
- 账户不包含特殊字符
-
校验密码是否输入正确,要和数据库中的密文密码去对比
-
用户信息脱敏,隐藏敏感信息,防止数据库中的字段泄露(利用user对象封装该返回的信息,代码体现)可以封装一个dto,返回想要返回的信息比如uservo包装类
-
我们要记录用户的登录态(session),将其存到服务器上(用后端 SpringBoot 框架封装的服务器 tomcat 去记录)
cookie 后端读取cookie跟session做匹配从而得知用户是否登录
-
返回脱敏后的用户信息(返回dto类)
dto类:(封装一个除了实体类以外的包装类,返回要求的数据,因为有时不需要返回所有的属性故而自己封装一个)
如何知道是哪个用户登录了?
(这⽅⾯的知识是在学习javaweb会接触到)
-
连接服务器端后,得到⼀个 session 状态(匿名会话),返回给前端
-
登录成功后,得到了登录成功的 session,并且给该 session 设置⼀些值(⽐如⽤户信息),返回给前端⼀个设置 cookie 的 “命令”
session => cookie
-
前端接收到后端的命令后,设置 cookie,保存到浏览器内
-
前端再次请求后端的时候(相同的域名),在请求头中带上cookie去请求
-
后端拿到前端传来的 cookie,找到对应的 session
-
后端从 session 中可以取出基于该 session 存储的变量(⽤户的登录信息、登录名,token等等)
实现
控制器注解:
@RestController 适用于编写 restful 风格的 api,返回值默认为 json 类型
校验写在哪里?
- controller 层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好)
- service 层是对业务逻辑的校验(有可能被 controller 之外的类调用)
service层:
/**
* @param userAccount 用户账号
* @param userPassword 用户密码
* @param request
* @return 脱敏后的用户信息
*/
User doLogin(String userAccount, String userPassword, HttpServletRequest request);
@Slf4j log记录日志注解
serviceimpl层:(return null 后边会返回自定义异常类)
//用户登录态键
public static final String USER_LOGIN_STATE="userLoginState";
/**
* @param userAccount 用户账号
* @param userPassword 用户密码
* @param request
* @return 脱敏后的用户信息
*/
@Override
public User doLogin(String userAccount, String userPassword, HttpServletRequest request) {
//1.校验
if(StringUtils.isAnyBlank(userAccount,userPassword)){
return null;
}
if(userAccount.length()<4){
return null;
}
if(userPassword.length()<8){
return null;
}
//账户不能包含特殊字符
String regEx = ".*[\\s`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?\\\\]+.*";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(userAccount);
boolean matches = m.matches();
if(matches){
return null;
}
//2.加密
String encryPassword=DigestUtils.md5DigestAsHex((SALT+userPassword).getBytes());
//查询用户是否存在(假如用户状态已经被删除了是否还会查出来,
//MyBatis-Plus框架它有⼀个逻辑删除,默认会帮助我们查询出来没有被删的⽤户
QueryWrapper<User>queryWrapper=new QueryWrapper<>();
queryWrapper.eq("userAccount",userAccount);
queryWrapper.eq("userPassword",encryPassword);//这里用的加密后的密码
//用selectOne方法查询一条数据即可
User user = userMapper.selectOne(queryWrapper);
//用户不存在
if (user==null){
//记录日志
log.info("user login failed,userAccount cannot match userPasswird");
return null;
}
//3. 用户脱敏(密码不用返回) 不脱敏前端可以看到所有后端返回的信息
//可以自己封装一个vo类返回需要返回的信息
User safeUser=new User();
safeUser.setId(user.getId());
safeUser.setUsername(user.getUsername());
safeUser.setAvatarUrl(user.getAvatarUrl());
safeUser.setUserAccount(user.getUserAccount());
safeUser.setGender(user.getGender());
safeUser.setPhone(user.getPhone());
safeUser.setEmail(user.getEmail());
safeUser.setUserStatus(user.getUserStatus());
safeUser.setCreateTime(user.getCreateTime());
//4.记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE,safeUser);
//5.返回脱敏后的信息
return safeUser;
}
安装该插件(可以自动填充java参数)alt+enter 快捷键自动填充
先封装⼀个对象,专⻔⽤来接收请求参数
implements Serializable 序列化
一个对象序列化的接口 。 一个类只有实现了Serializable接口 它的对象才是可序列化的
因此如果要序列化某些类的对象 这些类必须实现Serializable接口。并且 Serializable是一个空接口 没有什么具体内容 他的目的只是简单的标识一个类的对象可以被序列化。什么情况下需要序列化?
当你想把的内存中的对象写入到硬盘的时候
当你想用套接字在网络上传送对象的时候
当你想通过RMI传输对象的时候
/**
* 用户注册请求体
*/
@Data
public class UserRegisterRequest implements Serializable {
private static final long serialVersionUID = 1905122041950251207L;
private String userAccount;
private String userPassword;
private String checkPassword;
}
/**
* 用户登录请求体
*/
@Data
public class UserLoginRequest {
private String userAccount;
private String userPassword;
}
登录注册接口的实现
Controller层:
@RestController 适⽤于编写 restful ⻛格的 api,返回值默认为 json 类型
@RequestBody 用来告诉springmvc把前端传来的json参数与对象做关联
/**
* 用户接口
*/
@RestController
@RequestMapping("/user")
public class UserController {
@Resource
private UserService userService;
/**
* 用户注册
* @param userRegisterRequest(用户注册请求体,上边引入了)
* @return
*/
@PostMapping("/register")
public Long userRegister(@RequestBody UserRegisterRequest userRegisterRequest){
if (userRegisterRequest==null){
return null;
}
String userAccount = userRegisterRequest.getUserAccount();
String userPassword = userRegisterRequest.getUserPassword();
String checkPassword = userRegisterRequest.getCheckPassword();
if (StringUtils.isAnyBlank(userAccount,userPassword,checkPassword)){
return null;
}
return userService.userRegister(userAccount, userPassword, checkPassword);
}
/**
* 用户登录
* @param userLoginRequest(用户登录请求体)
* @param request
* @return
*/
@PostMapping("/login")
public User userLogin(@RequestBody UserLoginRequest userLoginRequest, HttpServletRequest request){
if (userLoginRequest==null){
return null;
}
String userAccount = userLoginRequest.getUserAccount();
String userPassword = userLoginRequest.getUserPassword();
if (StringUtils.isAnyBlank(userAccount,userPassword)){
return null;
}
return userService.doLogin(userAccount, userPassword,request);
}
}
实现层已经进行非空判断了,Controller层可以不校验,但Controller层检验作用是controller层倾向于对请求参数本身的校验,不涉及业务逻辑本身(越少越好)
service层是对业务逻辑的校验(有可能被Controller之外的类调用)
mybatisplus逻辑删除
步骤1 application.yml(logic-delete-field如果指定了可以忽略步骤二,或者不指定在相应字段加注解,指定的话比较麻烦建议不指定,然后通过注解去指定每个实体类字段)
mybatis-plus:
global-config:
db-config:
logic-delete-field: flag # 全局逻辑删除的实体字段名(since 3.3.0,配置后可以忽略不配置步骤2)
logic-delete-value: 1 # 逻辑已删除值(默认为 1)
logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
步骤2 实体类字段上加上@TableLogic
注解
@TableLogic
private Integer deleted;
用户管理接口(必须鉴权)
加个role字段判断用户身份
然后再实体类中添加身份字段
/**
* 用户身份 0普通用户 1管理员
*/
private Integer role;
改到service层
访问静态方法要用类名.方法
//仅管理员可查询
request.getSession().getAttribute(UserService.USER_LOGIN_STATE);
Controller层
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@GetMapping("/search")
public List<User> searchUsers(String username,HttpServletRequest request){
//仅管理员可查询
Object userObj = request.getSession().getAttribute(UserService.USER_LOGIN_STATE);
//强制类型转换
User user=(User)userObj;
//要判空操作
if(user==null ||user.getRole()!=1){
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
//StringUtils.isNotBlank()不为空不为空格 StringUtils.isAnyBlank区别
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username",username);
}
return userService.list(queryWrapper);
}
/**
* 根据用户id删除用户(没有鉴权)
* @param id
* @return
*/
//TOOD @Delete的实现
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id){
if(id<=0){
return false;
}
return userService.removeById(id);
}
改成这样后(定义一个常量,这样更符合编码规范)
/**
* 用户常量
*/
public interface UserConstant {
//用户登录态键
String USER_LOGIN_STATE="userLoginState";
/**
* --------权限----------
*/
//默认权限
int OEFAULT_ROLE=0;
//管理员权限
int ADMIN_ROLE=1;
}
Controller层
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@GetMapping("/search")
public List<User> searchUsers(String username,HttpServletRequest request){
//仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
//强制类型转换
User user=(User)userObj;
//要判空操作
if(user==null ||user.getRole()!=ADMIN_ROLE){
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
//StringUtils.isNotBlank()不为空不为空格 StringUtils.isAnyBlank区别
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username",username);
}
return userService.list(queryWrapper);
}
/**
* 根据用户id删除用户
* @param id
* @return
*/
//TOOD @Delete的实现
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id,HttpServletRequest request){
//仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
//强制类型转换
User user=(User)userObj;
//要判空操作
if(user==null ||user.getRole()!=ADMIN_ROLE){
return false;
}
//强制类型转换
if(id<=0){
return false;
}
return userService.removeById(id);
}
然后发现这两个方法有相同的地方所以提出来写成一个公共方法
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@GetMapping("/search")
public List<User> searchUsers(String username,HttpServletRequest request){
if(!isAdmin(request)){
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
//StringUtils.isNotBlank()不为空不为空格 StringUtils.isAnyBlank区别
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username",username);
}
return userService.list(queryWrapper);
}
/**
* 根据用户id删除用户
* @param id
* @return
*/
//TOOD @Delete的实现
@PostMapping("/delete")
public boolean deleteUser(@RequestBody long id,HttpServletRequest request){
if(!isAdmin(request)){
return false;
}
//强制类型转换
if(id<=0){
return false;
}
return userService.removeById(id);
}
/**
* 是否为管理员
* @param request
* @return
*/
private boolean isAdmin(HttpServletRequest request){
//仅管理员可查询
Object userObj = request.getSession().getAttribute(USER_LOGIN_STATE);
//强制类型转换
User user=(User)userObj;
//要判空操作
if(user==null ||user.getRole()!=ADMIN_ROLE){
return false;
}
return true;
}
role是mysql的保留关键字所以要在xml映射里面写成
role
形式,或者修改表字段,这里推荐修改表字段为user_role
spring:
application:
name: user-center
datasource:
driver-class-name: com.mysql.jdbc.Driver
username: root
password: root
url: jdbc:mysql://localhost:3306/yupi
#session失效时间
session:
timeout: 86400
给session添加失效时间,此处失效时间为一天
userServiceImpl 的改变 (用户脱敏(改为调用公共方法)的地方)
/**
* @param userAccount 用户账号
* @param userPassword 用户密码
* @param request
* @return 脱敏后的用户信息
*/
@Override
public User doLogin(String userAccount, String userPassword, HttpServletRequest request) {
//1.校验
if(StringUtils.isAnyBlank(userAccount,userPassword)){
return null;
}
if(userAccount.length()<4){
return null;
}
if(userPassword.length()<8){
return null;
}
//账户不能包含特殊字符
String regEx = ".*[\\s`~!@#$%^&*()+=|{}':;',\\[\\].<>/?~!@#¥%……&*()——+|{}【】‘;:”“’。,、?\\\\]+.*";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(userAccount);
boolean matches = m.matches();
if(matches){
return null;
}
//2.加密
String encryPassword=DigestUtils.md5DigestAsHex((SALT+userPassword).getBytes());
//查询用户是否存在(假如用户状态已经被删除了是否还会查出来,
// MyBatis-Plus框架它有⼀个逻辑删除,默认会帮助我们查询出来没有被删的⽤户
QueryWrapper<User>queryWrapper=new QueryWrapper<>();
queryWrapper.eq("userAccount",userAccount);
queryWrapper.eq("userPassword",encryPassword);//这里用的加密后的密码
//用selectOne方法查询一条数据即可
User user = userMapper.selectOne(queryWrapper);
//用户不存在
if (user==null){
//记录日志
log.info("user login failed,userAccount cannot match userPasswird");
return null;
}
//3 用户脱敏(改为调用公共方法)
User safeUser=getSafetyUser(user);
//4.记录用户的登录态
request.getSession().setAttribute(USER_LOGIN_STATE,safeUser);
//5.返回脱敏后的信息
return safeUser;
}
/**
* 用户脱敏
* @param orignUser
* @return
*/
@Override
public User getSafetyUser(User orignUser){
//3. 用户脱敏(密码不用返回) 不脱敏前端可以看到所有后端返回的信息
//可以自己封装一个vo类返回需要返回的信息
User safeUser=new User();
safeUser.setId(orignUser.getId());
safeUser.setUsername(orignUser.getUsername());
safeUser.setAvatarUrl(orignUser.getAvatarUrl());
safeUser.setUserAccount(orignUser.getUserAccount());
safeUser.setGender(orignUser.getGender());
safeUser.setPhone(orignUser.getPhone());
safeUser.setEmail(orignUser.getEmail());
safeUser.setUserRole(orignUser.getUserRole());
safeUser.setUserStatus(orignUser.getUserStatus());
safeUser.setCreateTime(orignUser.getCreateTime());
return safeUser;
}
userservice
/**
* 用户脱敏
* @param orignUser
* @return
*/
User getSafetyUser(User orignUser);
userController层修改方法:
/**
* 根据用户名查询用户信息
* @param username
* @return
*/
@GetMapping("/search")
public List<User> searchUsers(String username,HttpServletRequest request){
if(!isAdmin(request)){
return new ArrayList<>();
}
QueryWrapper<User> queryWrapper=new QueryWrapper<>();
//StringUtils.isNotBlank()不为空不为空格 StringUtils.isAnyBlank区别
if(StringUtils.isNotBlank(username)){
queryWrapper.like("username",username);
}
List<User> userList=userService.list(queryWrapper);
return userList.stream().map(user -> {
user.setUserPassword(null);
return userService.getSafetyUser(user);
}).collect(Collectors.toList());
}
我认为不如建一个dto包去返回想要的数据
前端修改
react的学习:https://blog.csdn.net/qq_31281245/article/details/127243871
export const SYSTEM_LOGO="https://img1.baidu.com/it/u=279206479,2064644760&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1678813200&t=ad7a9bb914d24cd46686e6913ae45522";
/**
* 文档地址
*/
export const PLAMET_LINK="www.baidu.com";
export const 常量名 :对应组件下新建文件constants常量文件来定义所有的dispatch的常量
删掉其他登录方式:我想做成点击后效果为暂未开发
忘记密码改为邮箱验证码找回也没做呢
1.修改底部版权信息
在这里插入图片描述
按要求改成自己的就行
2.修改logo
定义⼀个全局的常量,在src包下新建constant包
把 “/logo.svg” 改为 {SYSTEM_LOGO},这⾥要导⼊⼀下,根据提示或使⽤快捷键
导⼊的内容如下:
在Footer->index.tsx也可以直接使⽤ PLANET_LINK
还可以修改标题和副标题
3. 删除多余代码
按需删除即可
4.对接(请求)后台的接⼝:
找到handleSubmit,点击⼀下,vscode要按ctrl+单击
选中LoginParams ctrl+鼠标点击进入
将username,password的值改为后台接口定义的值
点击username按f2重构为userAccunt,点击password按f2重构为userPassword(webstrom快捷键shift+f6重构)
typings.d.ts就是定义设置返回参数的类型文件
回到handleSubmit
点击login进入api定义接口的文件
根据后台修改request的请求地址,去Ant Design Pro的官⽅⽂档https://pro.ant.design/zh-CN/docs/overview,搜索请求
提示我们要去app.ts去配置,顺便把官⽹提示的配置复制
mport { RequestConfig } from 'umi';
export const request: RequestConfig = {
timeout: 1000,
errorConfig: {},
middlewares: [],
requestInterceptors: [],
responseInterceptors: [],
errorHandler,
};
先测试⼀下,写下配置
请求地址修改为/user/login
去登录测试⼀下,请求地址正确(右侧的框是前端开发者的调试⼯具,按f12或者⿏标右键点检查)
现在有个问题。有啥问题,我们遇到了这个跨域错误,为什么会跨域?因为我们当前前端的浏览器⾥的地址,它是8000端⼝, 但是我们的后端,它是8080端⼝,端⼝不⼀样,就是跨域的。跨域的话,有很多种⽅式解决,要么搭⼀个代理,要么在你的后端去⽀持跨域,但后端⽀持跨域很不安全。Ant Design Pro它提供了⼀个配置代理的⽅式,我们直接⽤它的代理即可。
或者后台配置跨域:
可以用过滤器做,这里没改呢还
api.ts加上api前缀在请求路径上
后台则在application.yml 指定接口全局路径前缀
server:
port: 8080
servlet:
context-path: /api
在index.tsx把username替换成userAccount
password替换成userPassword(区别⼤⼩写匹配)
给密码多加⼀个⻓度校验
这⾥改成user,如果user存在的话,就显示登录成功,并且设置⽤户的登录状态为user
测试⼀下
⻓度的校验成功了