需求分析
在黑马头条项目中,登录有两种方式:一种是用户输入账号密码后登录,这种方式登陆后的权限很大,可以查看,也可以进行其他操作;另一种方式就是用户点击不登录,以游客的身份进入系统,但是权限不足,只可以查看帖子等信息,无法评论点准收藏等操作。
表结构分析
关于app端用户相关的内容较多,可以单独设置一个库leadnews_user:
登录需要用到的是ap_user表,表结构如下:
-- auto-generated definition
create table ap_user
(id int unsigned auto_increment comment '主键'primary key,salt varchar(32) null comment '密码、通信等加密盐',name varchar(20) null comment '用户名',password varchar(32) null comment '密码,md5加密',phone varchar(11) null comment '手机号',image varchar(255) null comment '头像',sex tinyint unsigned null comment '0 男1 女2 未知',is_certification tinyint unsigned null comment '0 未1 是',is_identity_authentication tinyint(1) null comment '是否身份认证',status tinyint unsigned null comment '0正常1锁定',flag tinyint unsigned null comment '0 普通用户1 自媒体人2 大V',created_time datetime null comment '注册时间'
)comment 'APP用户信息表';
表结构如下图所示:
tinyint类型:占1个字节,不指定unsigned(非负数),值范围(-128,127),指定了unsigned,值范围(0,255)
tinyint通常表示小范围的数值,或者表示true或false,通常值为0表示false,值为1表示true
项目中的持久层使用的mybatis-plus,一般都使用mybais-plus逆向生成对应的实体类
app_user表对应的实体类如下:
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;import java.io.Serializable;
import java.util.Date;/*** <p>* APP用户信息表* </p>** @author itheima*/
@Data
@TableName("ap_user")
public class ApUser implements Serializable {private static final long serialVersionUID = 1L;/*** 主键*/@TableId(value = "id", type = IdType.AUTO)private Integer id;/*** 密码、通信等加密盐*/@TableField("salt")private String salt;/*** 用户名*/@TableField("name")private String name;/*** 密码,md5加密*/@TableField("password")private String password;/*** 手机号*/@TableField("phone")private String phone;/*** 头像*/@TableField("image")private String image;/*** 0 男1 女2 未知*/@TableField("sex")private Boolean sex;/*** 0 未1 是*/@TableField("is_certification")private Boolean certification;/*** 是否身份认证*/@TableField("is_identity_authentication")private Boolean identityAuthentication;/*** 0正常1锁定*/@TableField("status")private Boolean status;/*** 0 普通用户1 自媒体人2 大V*/@TableField("flag")private Short flag;/*** 注册时间*/@TableField("created_time")private Date createdTime;}
这个实体类名为 ApUser ,是一个Java类,用于映射数据库中的 ap_user 表,它包含了多个字段,如主键 id 、加密盐 salt 、用户名 name 、加密后的密码 password 、手机号 phone 、用户头像 image 、性别 sex 、认证状态 certification 、身份认证状态 identityAuthentication 、账户状态 status 、用户类型 flag 以及注册时间 createdTime ,其中 id 字段使用了MyBatis-Plus的 @TableId 注解来指定其为主键,并且设置为主键生成策略为自动增长,其他字段则使用 @TableField 注解来指定对应的数据库列名,类中还使用了lombok的 @Data 注解来自动生成getter和setter方法,以及 serialVersionUID 来支持序列化,整个类实现了 Serializable 接口,以支持对象的序列化操作。
思路分析
Md5相同的密码每次加密都一样,不太安全,所以在项目开发中我们一般会使用加盐来加强密码,思路如下:首先,前端根据用户名+密码来注册:
第一步:我们根据用户生成salt,这是一个随机的字符串用表中的salt来保存;
第二步:我们通过密码+salt来组成新的密码,经过md5加密后的密码保存在表password字段中;
第三步:当前端发来请求,我们会先根据用户名来数据库中查询用户;如果用户不为空,则取出salt的值和前端传递过来的密码拼接成新的密码;然后再跟数据库中的密码比对,如果二者密码一致,则用户验证成功;相反,如果比对不一致,则表示密码有误。
第四步:如果用户是首次登录的话,也就是是注册的话,后端是需要生成一个jwt令牌给前端的,前端将这个令牌保存好;之后前端的每一次请求,都需要携带这个令牌;在后端根据令牌是否存在并且令牌是否有效来判断请求是否可以正常执行。
用户游客登录,生成jwt返回(基于默认值0生成),当然这种方式登录 的权限没有账号密码登录的权限大,游客登录一般只有查看操作。
运营端微服务搭建
首先需要创建工程,在heima-leadnews-service下创建工程heima-leadnews-user,创建好之后需要建立对应的包,如图所示:
引导类
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;@SpringBootApplication
@EnableDiscoveryClient
@MapperScan("com.heima.user.mapper")
public class UserApplication {public static void main(String[] args) {SpringApplication.run(UserApplication.class,args);}
}
这个引导类名为 UserApplication ,它是一个Spring Boot应用的启动类,使用了 @SpringBootApplication 注解来标识这是一个Spring Boot应用,并且包含了 @EnableAutoConfiguration 、 @ComponentScan 和 @Configuration 等注解的功能,用于自动配置Spring应用上下文和组件扫描。此外,它还使用了 @EnableDiscoveryClient 注解来启用Spring Cloud的客户端发现功能,这使得应用能够注册到服务发现中心,从而实现微服务架构中的服务发现。 @MapperScan 注解用于指定MyBatis的Mapper接口所在的包路径,以便Spring Boot自动扫描并注册这些Mapper接口。在 main 方法中,通过调用 SpringApplication.run 方法并传入当前类和命令行参数,来启动Spring Boot应用。这个类是整个应用的入口点,通过运行这个类,就可以启动整个Spring Boot应用并使其参与到微服务架构中。
bootstrap.yml
server:port: 51801
spring:application:name: leadnews-usercloud:nacos:discovery:server-addr: 192.168.200.130:8848config:server-addr: 192.168.200.130:8848file-extension: yml
在nacos中创建配置文件
代码如下:
spring:datasource:driver-class-name: com.mysql.jdbc.Driverurl: jdbc:mysql://localhost:3306/leadnews_user?useUnicode=true&characterEncoding=UTF-8&serverTimezone=UTCusername: rootpassword: root
# 设置Mapper接口所对应的XML文件位置,如果你在Mapper接口中有自定义方法,需要进行该配置
mybatis-plus:mapper-locations: classpath*:mapper/*.xml# 设置别名包扫描路径,通过该属性可以给包中的类注册别名type-aliases-package: com.heima.model.user.pojos
logback.xml
<?xml version="1.0" encoding="UTF-8"?><configuration><!--定义日志文件的存储地址,使用绝对路径--><property name="LOG_HOME" value="e:/logs"/><!-- Console 输出设置 --><appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"><encoder><!--格式化输出:%d表示日期,%thread表示线程名,%-5level:级别从左显示5个字符宽度%msg:日志消息,%n是换行符--><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern><charset>utf8</charset></encoder></appender><!-- 按照每天生成日志文件 --><appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"><rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"><!--日志文件输出的文件名--><fileNamePattern>${LOG_HOME}/leadnews.%d{yyyy-MM-dd}.log</fileNamePattern></rollingPolicy><encoder><pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern></encoder></appender><!-- 异步输出 --><appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender"><!-- 不丢失日志.默认的,如果队列的80%已满,则会丢弃TRACT、DEBUG、INFO级别的日志 --><discardingThreshold>0</discardingThreshold><!-- 更改默认的队列的深度,该值会影响性能.默认值为256 --><queueSize>512</queueSize><!-- 添加附加的appender,最多只能添加一个 --><appender-ref ref="FILE"/></appender><logger name="org.apache.ibatis.cache.decorators.LoggingCache" level="DEBUG" additivity="false"><appender-ref ref="CONSOLE"/></logger><logger name="org.springframework.boot" level="debug"/><root level="info"><!--<appender-ref ref="ASYNC"/>--><appender-ref ref="FILE"/><appender-ref ref="CONSOLE"/></root>
</configuration>
登录功能实现
①:接口定义
@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {@PostMapping("/login_auth")public ResponseResult login(@RequestBody LoginDto dto) {return null;}
}
②:持久层mapper
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.heima.model.user.pojos.ApUser;
import org.apache.ibatis.annotations.Mapper;@Mapper
public interface ApUserMapper extends BaseMapper<ApUser> {
}
③:业务层service
import com.baomidou.mybatisplus.extension.service.IService;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.dtos.LoginDto;
import com.heima.model.user.pojos.ApUser;public interface ApUserService extends IService<ApUser>{/*** app端登录* @param dto* @return*/public ResponseResult login(LoginDto dto);}
实现类:
import com.baomidou.mybatisplus.core.toolkit.StringUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.common.enums.AppHttpCodeEnum;
import com.heima.model.user.dtos.LoginDto;
import com.heima.model.user.pojos.ApUser;
import com.heima.user.mapper.ApUserMapper;
import com.heima.user.service.ApUserService;
import com.heima.utils.common.AppJwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;import java.util.HashMap;
import java.util.Map;@Service//标记为服务类
@Transactional//保证了事务的原子性
@Slf4j//方便了记录日志
//这段代码的主要功能是实现用户登录和游客登录的逻辑
//用户登录时,根据手机号查询用户,验证密码后返回用户的JWT令牌和用户信息
//游客登录时,返回id为0的游客JWT令牌
public class ApUserServiceImpl extends ServiceImpl<ApUserMapper, ApUser> implements ApUserService {@Overridepublic ResponseResult login(LoginDto dto) {//1.正常登录(手机号+密码登录),如果前端传递的手机号和密码都不为空的话,执行以下操作if (!StringUtils.isBlank(dto.getPhone()) && !StringUtils.isBlank(dto.getPassword())) {//1.1查询用户,使用MyBatis Plus的getOne方法查询数据库中phone字段与dto.getPhone()相同的用户信息ApUser apUser = getOne(Wrappers.<ApUser>lambdaQuery().eq(ApUser::getPhone, dto.getPhone()));//先对从数据库中查询的对象进行判断,为空表示数据库没有登陆者信息,直接结束方法if (apUser == null) {//用统一封装结果集封装结果,返回提示给用户return ResponseResult.errorResult(AppHttpCodeEnum.DATA_NOT_EXIST,"用户不存在");}//1.2 比对密码//获取数据库用户的盐String salt = apUser.getSalt();//获取前端传递过来的代码String pswd = dto.getPassword();//将上述两个变量进行字符串拼接,然后再md5加密,赋值给pswdpswd = DigestUtils.md5DigestAsHex((pswd + salt).getBytes());//判断我们刚刚生成的密码和数据库中的密码是否一致if (!pswd.equals(apUser.getPassword())) {//不一致的话,通过统一封装结果集封装登录密码错误的信息会前端return ResponseResult.errorResult(AppHttpCodeEnum.LOGIN_PASSWORD_ERROR);}//密码比对成功,就需要给用户生成jet令牌//1.3 返回数据 jwtMap<String, Object> map = new HashMap<>();//用自定义的工具类AppJwtUtil给登录用户的id生成tokenmap.put("token", AppJwtUtil.getToken(apUser.getId().longValue()));//下面这两步是是为了保护用户隐私设置的,即将盐和密码置空,前端就查看不了密码//然后再将当前用户存入map集合中apUser.setSalt("");apUser.setPassword("");map.put("user", apUser);//最后,用统一封装结果集将map集合返回给前端return ResponseResult.okResult(map);} else {//2.游客登录,即没有账号密码 同样返回token id = 0//我们约定如果是游客登录的话,直接使用0作为用户的id,生成tokenMap<String, Object> map = new HashMap<>();map.put("token", AppJwtUtil.getToken(0l));//直接将生成的token返回给前端展示return ResponseResult.okResult(map);}}
}
④:控制层controller
import com.heima.model.common.dtos.ResponseResult;
import com.heima.model.user.dtos.LoginDto;
import com.heima.user.service.ApUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;@RestController
@RequestMapping("/api/v1/login")
public class ApUserLoginController {@Autowiredprivate ApUserService apUserService;@PostMapping("/login_auth")public ResponseResult login(@RequestBody LoginDto dto) {return apUserService.login(dto);}
}
思路分析:
这段代码实现了一个用户登录服务,其中包括正常用户登录和游客登录两种情况。对于正常用户登录,首先检查前端传递的手机号和密码是否都不为空,然后使用MyBatis Plus的查询方法根据手机号查询数据库中的用户信息,如果用户不存在则返回错误提示;如果用户存在,则进一步比对密码,通过将前端密码与数据库中的盐值拼接后进行MD5加密,与数据库中的密码进行比较,如果密码错误则返回错误提示;密码正确则生成JWT令牌,并将用户的敏感信息(盐和密码)置空后,连同令牌一起返回给前端。对于游客登录,直接生成一个id为0的JWT令牌并返回。整个过程中,使用了 @Transactional 注解来保证事务的原子性,使用 @Slf4j 注解来方便日志记录,同时通过自定义的工具类 AppJwtUtil 来生成JWT令牌,通过统一封装的结果集 ResponseResult 来返回操作结果。