跟着黑马程序员视频学习springboot3+vue3的学习笔记
登录的主逻辑开发
查询请求中的用户名和密码是否在数据库里(是否存在)
查询操作在注册时已经有了,所以我们只需要在UserController类里加上登录的API和处理逻辑就可以了
@PostMapping("/login")
public Result<String> login(@Pattern(regexp="^\\S{5,16}$") String username,@Pattern(regexp="^\\S{5,16}$") String password){
//根据用户名查询用户名
User loginUser = userService.findByUserName(username);
//判断用户是否存在
if(loginUser==null){
return Result.error("用户名不存在");
}
//判断密码是否正确 loginuser对象中密码是密文
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//登录成功
return Result.success("jwt token 令牌..");
}
return Result.error("密码错误");
}
结果
登录认证
在一个项目中,有很多controller,如果用户没有登录,是不可以查询到其他的controller接口的,这时其他接口在进行查询之前需要对用户的登录状态进行检查,这个就叫登录认证。
那我们如何实现登录认证呢?这就需要借助令牌的技术
浏览器访问登录接口,登录成功后,需要在后台生成一个令牌,并且把令牌响应给浏览器;浏览器再去访问其他接口的时候就需要把这个令牌给携带上,其他接口看到浏览器是携带令牌的并且令牌是合法有效的,就正常提供服务,否则就不提供。
令牌详解
令牌起到身份识别的作用,它就是一段字符串
令牌要求:
-
承载业务数据, 减少后续请求查询数据库的次数
-
需要具备防伪功能
使用JWT令牌规范
-
全称:JSON Web Token(JSON Web Tokens - jwt.io)
-
定义了一种简洁的,自包含的格式,用于通信双方以json数据格式安全的传输信息
以下就是Token令牌
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.JTdCJTIybmFtZSUyMiUzQSUyMlRvbSUyMiUyQyUyMmlhdCUyMiUzQTE1MTYyMzkwMjIlN0Q=.SflKxwRJSMeKKF2QT4fwpMeJf...
里面的两个“.”把Token令牌分成了三部分
-
Token组成:
-
第一部分:Header(头) , 记录令牌类型,签名算法等 {“alg”:"HS256", "type": "JWT"} alg:加密算法,防篡改
-
第二部分:Payload(有效载荷),携带一些自定义信息,默认信息等。这部分一定不要存放私密的数据,比如登录密码。 {“id”:"1" , "username": "Tom" }
-
第三部分:Signature(数字签名),防止Token被篡改,确保安全性。借助header,payload部分并且加入指定密钥,通过指定签名算法计算而来。{header,payload,secret}
-
借助Base64编码方式把任意数据转换成64个可打印字符,所以JS字符串是通过Base64编码转换成token令牌中展示的那一段字符
JWT解析Token令牌时,会通过解密第三部分数字签名从而得到头部和载荷;再拿着解密得到的内容和用户传递的内容进行比对,不一样的话就说明数据被篡改过,不让其访问。
如何生成令牌
使用生成令牌的工具
在pom.xml文件中引人java-jwt坐标和单元测试的坐标
在src/test/java/org.exampletest下新建一个测试类JwtTest
这段代码不需要记忆,工具类里面有。但是我还不会呢,我就是跟着视频敲
package org.exampletest;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import org.junit.jupiter.api.Test;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import static com.mysql.cj.protocol.ExportControlled.sign;
public class JwtTest {
@Test
public void testGen(){
Map<String, Object> claims = new HashMap<>();
claims.put("id","1");
claims.put("username","张三");
//生成jwt的代码
String token=JWT.create()
.withClaim("user",claims)//添加载荷
.withExpiresAt(new Date(System.currentTimeMillis()+1000*60*60*24))//添加过期时间
.sign(Algorithm.HMAC256("secret"));//指定算法,配置密钥,这个密钥一定不能泄露出去,否则不能起到防篡改的作用
System.out.println(token);
}
}
Token的验证
在JwtTest类中添加一个验证方法
如果篡改了头部和载荷部分的数据,验证失败
如果密钥改了,验证失败
token过期了,验证失败
正式进入登录认证代码的书写
-
添加JWT工具类
直接使用黑马程序员给我们准备好的工具类
-
黑马程序员教程资源下载指南 - 哔哩哔哩 (bilibili.com)搜索springboot3+vue3即可
-
黑马程序员领取资料的百度网盘https://pan.baidu.com/s/1w9nn_IRGcqYm1npllasjoQ&pwd=9988
-
不想下载的,我把代码粘过来了,如下
-
package org.exampletest.utils; import com.auth0.jwt.JWT; import com.auth0.jwt.algorithms.Algorithm; import java.util.Date; import java.util.Map; public class JwtUtil { private static final String KEY = "itheima"; //接收业务数据,生成token并返回 public static String genToken(Map<String, Object> claims) { return JWT.create() .withClaim("claims", claims) .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 12)) .sign(Algorithm.HMAC256(KEY)); } //接收token,验证token,并返回业务数据 public static Map<String, Object> parseToken(String token) { return JWT.require(Algorithm.HMAC256(KEY)) .build() .verify(token) .getClaim("claims") .asMap(); } }
-
修改UserController里的登录逻辑代码实现生成令牌
@PostMapping("/login")
public Result<String> login(@Pattern(regexp="^\\S{5,16}$") String username,@Pattern(regexp="^\\S{5,16}$") String password){
//根据用户名查询用户名
User loginUser = userService.findByUserName(username);
//判断用户是否存在
if(loginUser==null){
return Result.error("用户名不存在");
}
//判断密码是否正确 loginuser对象中密码是密文
if(Md5Util.getMD5String(password).equals(loginUser.getPassword())){
//登录成功
Map<String,Object>claims = new HashMap<>();
claims.put("id",loginUser.getId());
claims.put("username",loginUser.getUsername());
String token=JwtUtil.genToken(claims);
return Result.success(token);
}
return Result.error("密码错误");
}
令牌生成成功
-
在其他接口实验令牌验证
浏览器携带token再去访问其他接口,那么这个token是以什么方式携带过来的呢?以请求体还是请求头呢?
翻阅接口文档才知道
由接口文档得出,token是请求头携带过来的,所以list方法的参数是token,参数前还要加上
@RequestHeader(name="Authorization")
在ArtitleController中修改代码为:
package org.exampletest.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.exampletest.pojo.Result;
import org.exampletest.utils.JwtUtil;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/article")
public class ArticleController {
@RequestMapping("/list")
public Result<String> list(@RequestHeader(name="Authorization") String token, HttpServletResponse response){
//验证token
try {
Map<String,Object>claims= JwtUtil.parseToken(token);
return Result.success("所有的文章数据......");
}
}catch (Exception e){
//http响应状态码为401
response.setStatus(401);
return Result.error("未登录");
}
}
重新运行,发送请求
失败:
把登录成功的请求头加上:访问成功了
验证成功。
大家会想到项目中会有很多很多controller,有非常多的接口需要令牌的校验,每个接口都写一个令牌校验的代码显然是不现实的。多个接口都有同样的技术需要完成,可以用拦截器实现。给程序注册一个拦截器,让所有的请求都经过拦截器,在拦截器里统一完成验证,验证通过才让浏览器访问接口,验证不通过就不给访问。
完了。。。。看到拦截器这里又不懂了。。。。没关系,先继续往下学,不会的先跳过,大概把这个登录认证内容,流程了解清楚后,再回过头来看拦截器的具体内容
拦截器(Interceptor)是Spring框架中用于拦截请求和响应的组件。在Spring MVC中,拦截器可以在请求处理之前、处理之后或响应发送之前执行自定义逻辑。拦截器通常用于实现诸如权限校验、日志记录、事务管理等跨多个请求的通用功能。
在启动类所在的包下新建一个interceptors包(拦截器包),在拦截器包下建立一个登录拦截器LoginInterceptor类
LoginInterceptor
是一个自定义的拦截器,它实现了HandlerInterceptor
接口。该拦截器的核心功能是在请求处理之前验证用户的令牌(Token)。如果令牌验证成功,拦截器返回true
,允许请求继续执行;如果验证失败,拦截器返回false
,阻止请求继续处理,并设置响应状态码为401(未授权)。
package org.exampletest.interceptors;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.exampletest.pojo.Result;
import org.exampletest.utils.JwtUtil;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import java.util.Map;
@Component
public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler){
//令牌验证
String token=request.getHeader("Authorization");
//验证token
try{
Map<String, Object> claims= JwtUtil.parseToken(token);
return true;
}catch (Exception e){
response.setStatus(401);
//不放行
return false;
}
}
}
在启动类所在的包下新建一个config包,config包下新建一个WebConfig类,用于注册拦截器
拦截器通过
WebMvcConfigurer
接口的addInterceptors
方法进行配置。在WebConfig
类中,LoginInterceptor
被注入并注册到拦截器注册处。通过excludePathPatterns
方法,可以指定不需要进行令牌验证的路径,例如登录和注册接口。这样配置后,只有非登录和注册的请求会经过LoginInterceptor
的验证。拦截器的配置确保了只有经过验证的请求能够访问受保护的资源,从而提高了应用的安全性
package org.exampletest.config;
import org.exampletest.interceptors.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private LoginInterceptor loginInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry){
//登录注册接口不拦截
registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login","/user/register");
}
}
把ArticleController类中的令牌验证部分删掉
package org.exampletest.controller;
import jakarta.servlet.http.HttpServletResponse;
import org.exampletest.pojo.Result;
import org.exampletest.utils.JwtUtil;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
@RequestMapping("/article")
public class ArticleController {
@RequestMapping("/list")
public Result<String> list(/*@RequestHeader(name="Authorization") String token, HttpServletResponse response*/){
// //验证token
// try {
// Map<String,Object>claims= JwtUtil.parseToken(token);
// return Result.success("所有的文章数据......");
// }catch (Exception e){
// //http响应状态码为401
// response.setStatus(401);
// return Result.error("未登录");
// }
return Result.success("所有的文章数据......");
}
}
到这里登录部分的接口就实现了