需求分析
在黑馬頭條項目中,登錄有兩種方式:一種是用戶輸入賬號密碼后登錄,這種方式登陸后的權限很大,可以查看,也可以進行其他操作;另一種方式就是用戶點擊不登錄,以游客的身份進入系統,但是權限不足,只可以查看帖子等信息,無法評論點準收藏等操作。
表結構分析
關于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 來返回操作結果。