✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🎯 你正在阅读「Java项目-企悦抽」系列文章 🎯
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🔥 弹简特 个人主页
❄️ 个人专栏直通车:
✨ 靠热爱去书写自己,靠勇敢去书写生活!
✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨✨
🌟 博主简介:

文章目录:
前言
友友们,上一期我们完成了登录模块的基础功能——验证码获取与缓存逻辑开发,本期我们将推进登录核心业务开发,重点实现登录鉴权、验证码登录与密码登录整套逻辑。那么话不多说,咱们直接开始吧!
一、登录鉴权方式
最简单的登录流程:
1、web登录页填写登录信息,前端发送请求到后端
2、后端接收登录信息,并校验,校验成功,返回成功结果
1、Cookie-Session认证方式


缺点:
1、仅用于web
2、不能跨域
3、集群环境下不适用
2、Token认证方式

缺点:用户数量大,对于内存来说,压力巨大,因为Redis是内存中的
3、基于Token的JWT令牌认证
此时登录之后,我不会单独生成 SessionId 或者是 token,而是我单独拎出来一个服务叫做 jwt,唯一和上述的区别就是 对于生成出来的 jwt 令牌,我们是不用存储在后端的,他生成这个 jwt 令牌之后,就直接返回给前端。
而对于前端来说,他是存储这个 jwt 的,此时如果 web 进行后续的操作的时候,会将这个 jwt 带上,然后对于后端来说我们对 jwt 令牌进行校验。
那么此时有人会问:你 jwt 都没有存储在后端,我们怎么去校验呢?
此时这个是对于 jwt 的原理有关的,jwt 是一个加解密的过程,也就是说我们会把我们关心的内容生成一个 jwt 令牌,这个其实就是一个加密的过程,那么这个令牌不被后端存储,而是交由前端存储,这个加解密的过程,不用我们手动编写逻辑完成,而是靠独立拆分出来的 JWT 工具包来实现。
完整的 JWT 由三部分组成:头部、负载、签名。
我们将用户相关信息放入负载进行加密处理,加密完成后整合生成完整的 JWT 令牌字符串,后端直接将该字符串返回给前端;后续前端发起各类业务请求时会携带该令牌字符串,后端接收后对其执行解密校验操作,若解密流程能够正常执行无报错,就代表令牌校验通过。

好处:不像Cookie-session一样仅仅限于web,因为jwt不是存在Cookie里面的,所以不被web页面限制,又由于他不是存在Cookie里面的,所以他能够接受跨域资源的访问,同时你记住,由于我们jwt也不受分布式的限制,因为他只是涉及加解密的机制。
我们本项目使用的是jwt,那么登录的方式就是
1、web登录页填写登录信息,前端发送请求到后端
2、后端接收登录信息,并校验,校验成功,返回成功结果
3、生成jwt令牌并返回
4、一图解释上述完整的原理

注意:方式3我们的密钥字符串是写到配置文件中的或者写到工具类中的,一致不会动,不是存到缓存中,而是写在代码里,他不占内存,且只生成一次,后续直接一直使用
JWT相关内容请参考👉详讲JWT
二、封装JWTUtil工具类
那么上述咱们讲了jwt的原理之后,我们就将这个东西的使用封装成一个工具类,然后咱使用的时候就直接使用就行了。
咱这些工具类都是统一的,直接复制粘贴别人写好的就行了,知道自己怎么使用就OK了。

注意:这里如果看起来很吃力的铁铁们,可以参考上述给的jwt博客,不然后续代码看起来很吃力,此处咱没有过多去解释JWT原理和逻辑,因为我在JWT详解博客中总结过一篇很详细,很通俗易懂的内容,需要的铁铁们可以参考哦。
首先,生成秘钥字符串(随机生成,然后放起来(放到配置文件中),后续一直使用),注意:不是存起来,而是写到我们的配置文件中,如期一来,集群也使用,而且如此一来,我们也不占用内存
原理还是看咱上述给的博客
生成秘钥字符串的代码
/**⽣成密钥字符串*/
@Test
public void genKey(){
// 创建了⼀个密钥对象,使⽤HS256签名算法。
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
// 将密钥编码为Base64字符串。
String secretString = Encoders.BASE64.encode(key.getEncoded());
System.out.println(secretString);
}
此时你生成完这个字符串之后,记住 存起来,我们使用这个密钥字符串结合api生成一个密钥对象,这个密钥对象用于生成签名,以及这个密钥对象用于解密token,记住老铁,咱之前在博客系统中也有详细的说过,可以参考👉JWT使用案例。
代码
/**
* @ClassName JWTUtil
* @Description TODO JWT工具类
* @Author zhongge
* @Version 1.0
*/
public class JWTUtil {
/**
* 日志对象
*/
private static final Logger logger = LoggerFactory.getLogger(JWTUtil.class);
/**
* 密钥:Base64编码的密钥
*/
private static final String SECRET = "XDdowsTOMYxvxDjV2dAKdjez6UZJzy44IpsEbDSG0Q0=";
/**
* 生成安全密钥:将一个Base64编码的密钥解码并创建一个HMAC SHA密钥。
*/
private static final SecretKey SECRET_KEY = Keys.hmacShaKeyFor(
Decoders.BASE64.decode(SECRET));
/**
* 过期时间(单位: 毫秒)
*/
private static final long EXPIRATION = 60*60*1000;
/**
* 生成token
*
* @param claim {"id": 12, "name":"lisi"}
* @return token
*/
public static String genJwt(Map<String, Object> claim){
//签名算法
String jwt = Jwts.builder()
.setClaims(claim) // 自定义内容(载荷)
.setIssuedAt(new Date()) // 设置签发时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRATION)) // 设置过期时间
.signWith(SECRET_KEY) // 签名算法
.compact();
return jwt;
}
/**
* 验证token(解密)
*/
public static Claims parseJWT(String jwt){
if (!StringUtils.hasLength(jwt)){
return null;
}
// 创建解析器, 设置签名密钥
JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder().setSigningKey(SECRET_KEY);
Claims claims = null;
try {
//解析token
claims = jwtParserBuilder.build().parseClaimsJws(jwt).getBody();
}catch (Exception e){
// 签名验证失败
logger.error("解析令牌错误,jwt:{}", jwt, e);
}
return claims;
}
/**
* 从token中获取用户ID
*/
public static Integer getUserIdFromToken(String jwtToken) {
Claims claims = JWTUtil.parseJWT(jwtToken);
if (claims != null) {
Map<String, Object> userInfo = new HashMap<>(claims);
return (Integer) userInfo.get("userId");
}
return null;
}
}
三、登录逻辑
1、流程如下所示:
- 用户在登录页选择电话+密码或电话+验证码方式,将登录信息提交至 User Server。
- 服务端先做基础校验(登录信息完整性 / 手机号),再从 MySQL 获取用户信息,并校验用户身份(含是否强制身份登录)。
- 密码登录时由服务端校验密码是否一致;验证码登录时由 verificationCode Service 向 redis 查询并校验验证码是否准确。
- 校验通过后,服务端使用 JWT 生成 Token 并返回给客户端;客户端保存 Token,后续请求携带该令牌,由服务端校验合法性后再执行业务逻辑。
2、流程图如下所示:

3、时序图

四、实现控制层密码、验证码登录接口
1、约定前后端交互接口
密码登录
[请求方式] POST
[请求地址] /password/login
[请求参数]{ "loginName":"13199999999", "password":"123456", "mandatoryIdentity":"ADMIN" }[响应数据]
{ "code": 200, "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxNjI2MjI5OCwiZXhwIjoxNzE2MjY0MDk4fQ.QfiZmZcfzd5ls_t8lg7bsTF7kA0daK-psjUt1QRj9d4", "identity": "ADMIN" }, "msg": "" }二、验证码登录
[请求方式] POST
[请求地址] /message/login
[请求参数]{ "loginMobile":"13199999999", "verificationCode":"0475", "mandatoryIdentity":"ADMIN" }[响应数据]
{ "code": 200, "data": { "token": "eyJhbGciOiJIUzI1NiJ9.eyJpZGVudGl0eSI6Ik5PUk1BTCIsInVzZXJJZCI6MjEsImlhdCI6MTcxNjI2MjUyMywiZXhwIjoxNzE2MjY0MzIzfQ.XEuwO8AvNcqstbOrkI9kWaMhbN-HN2DfnUYGhJthA3I", "identity": "ADMIN" }, "msg": "" }



2、实体类的封装

返回实体类

请求参数实体类

但是你看,我们请求实体类中有公共的字段,所以此时就可以提取出来

代码
/**
* @ClassName UserLoginResult
* @Description TODO 用户登录结果实体类
* @Author zhongge
* @Version 1.0
*/
@Data
public class UserLoginResult {
/**
* JWT令牌
*/
private String token;
/**
* 登录人员身份
*/
private String identity;
}
/**
* @ClassName UserLoginParam
* @Description TODO 登录请求实体父类
* @Author zhongge
* @Version 1.0
*/
@Data
public class UserLoginParam implements Serializable {
/**
* 强制某身份登录,不填写就不限制身份
*
* 这个身份从枚举类中来获取
* @see com.zhongge.lotterysystem.service.enums.UserIdentityEnum #name()
*/
private String mandatoryIdentity;
}
/**
* @ClassName UserPasswordLoginParam
* @Description TODO 手机号密码登录的请求实体类
* @Author zhongge
* @Version 1.0
*/
@Data
@EqualsAndHashCode(callSuper = true)
public class UserPasswordLoginParam extends UserLoginParam{
/**
* 手机号或者邮箱
*/
private String loginName;
/**
* 密码
*/
private String password;
}
/**
* @ClassName UserMessageLoginParam
* @Description TODO 手机号验证码登录的请求实体类
* @Author zhongge
* @Version 1.0
*/
@Data
@EqualsAndHashCode(callSuper = true)//你没有使用继承的话,那么你使用这个就会报错
public class UserMessageLoginParam extends UserLoginParam{
/**
* 手机号或者邮箱
*/
private String loginMobile;
/**
* 验证码
*/
private String verificationCode;
}
3、开始写控制层
说明序列化和反序列工具类的使用场景在打印日志上

服务层对外打交道的是DTO

而你控制层调用服务层 你得使用DTO对象来接收,那么你控制层和前端打交道,你前端要求返回什么对象,你服务层就返回是对象,好比,如图:

为什么大家这么约定?因为我们项目不是一个人做的,而是一个团队做的,所以为了他们之间的交互,我们得约定一些东西。
注意,咱们的DTO是返回实体类,你要向返回哪一些东西看齐
所以创建DTO实体类

/**
* @ClassName UserLoginDTO
* @Description TODO 服务层返回登录的实体类对象
* @Author zhongge
* @Version 1.0
*/
@Data
public class UserLoginDTO {
/**
* JWT令牌
*/
private String token;
/**
* 登录人员身份
*/
private String identity;
}
开始类型转换

如果数据为空

同时注意,咱的异常状态码变遇到需求 就边变化
那么类型装换的逻辑如下:

补充一些少些的东西
1、接收JSON对象使用@RequestBody注解

2、参数校验

当然,验证码登录也是同理

OK,那么接下里还有短信验证的控制层代码,和我们密码的方式很相似,所以我们就直接复制就好了



五、实现服务层密码、验证码登录逻辑
1、登录实现-密码登录业务逻辑




接下里我们先看密码登录的逻辑实现


查库

注意:手机号需要加密

查不到就抛异常

那么接下里我们就校验一下是否需要强制要求这个某个用户登录,什么意思呢?


校验密码



最后,如果程序走到此处就代表检验通过了,此时就塞入返回值:JWT令牌(token)

最后构建返回值直接返回即可

此处补充一点,我们的UerLoginDTO里面的身份我们使用枚举吧,这个枚举之前定义过的,是可管控的管理员和用户,之所以使用枚举 是因为咱想让这个身份信息在我们的管控范围内


这一步,现在看不懂就好好沉淀沉淀,后面回头就会了呢~~
此时,还得再控制层嗲用的那部分有一个类型转换的方法,需要做如下的修改:

OK,此时,咱就把那两个sql给实现一下就行了

OK,到此密码登录就完成了
2、登录实现-验证码登录业务逻辑
手机号短信验证码登录,第一步咱先验证一下手机号是否合法


开始校验验证码:
首先将处理验证码的工具类给注入进来,到时候用它去查询出当时发送的验证码

通过手机号从缓存中获取验证码

校验验证码

OK,到这里咱们的验证码登录也实现完成了~~
最后,各位铁汁,本期我们已经完成登录模块最核心的功能:登录鉴权、密码登录与验证码登录整套逻辑。下一期会先对两套登录接口进行功能测试,接着编写前端对接代码,最后实现全局登录拦截器,内容全是实操干货。如果本篇内容对你有帮助,不妨点赞收藏,也欢迎在评论区留言交流,你的每一份支持都是我持续创作的最大动力~👋
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2301_81982617/article/details/162972655




