学习目标:
Spring Boot 整合JWT实现基于自定义注解的 登录请求接口拦截
例:
- 一篇掌握 JWT 入门知识
1.1 在学习SpringBoot 整合JWT之前,我们先来说说JWT进行用户身份验证的流程
-
1:客户端使用用户名和密码请求登录 2:服务端收到请求,验证用户名和密码 3:验证成功后,服务端会签发一个token,再把这个token返回给客户端 4:客户端收到token后可以把它存储起来,比如放到cookie中 5:客户端每次向服务端请求资源时需要携带服务端签发的token,可以在cookie或者header中携带 6:服务端收到请求,然后去验证客户端请求里面带着的token,如果验证成功,就向客户端返回请求数据
1.2 而JWT就是上述流程当中token的一种具体实现方式,其全称是JSON Web Token,官网地址:https://jwt.io/
并且这个JWT token带有签名信息,接收后可以校验是否被篡改,所以可以用于在各方之间安全地将信息作为Json对象传输。JWT的认证流程如下:
-
1:首先,前端通过Web表单将自己的用户名和密码发送到后端的接口,这个过程一般是一个POST请求。建议的方式是通过SSL加密的传输(HTTPS),从而避免敏感信息被嗅探 2:后端核对用户名和密码成功后,将包含用户信息的数据作为JWT的Payload,将其与JWT Header分别进行Base64编码拼接后签名,形成一个JWT Token,形成的JWT Token就是一个如同lll.zzz.xxx的字符串 3:后端将JWT Token字符串作为登录成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的JWT Token即可 4:前端在每次请求时将JWT Token放入HTTP请求头中的Authorization属性中(解决XSS和XSRF问题) 后端检查前端传过来的JWT Token,验证其有效性,比如检查签名是否正确、是否过期、token的接收方是否是自己等等 5:验证通过后,后端解析出JWT Token中包含的用户信息,进行其他逻辑操作(一般是根据用户信息得到权限等),返回结果
1.3 JWT结构
JWT由3部分组成:标头(Header)、有效载荷(Payload)和签名(Signature)。在传输的时候,会将JWT的3部分分别进行Base64编码后用.进行连接形成最终传输的字符串
JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret) JWTString=Base64(Header).Base64(Payload).HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
1.3.1 Header
JWT头是一个描述JWT元数据的JSON对象,alg属性表示签名使用的算法,默认为HMAC SHA256(写为HS256);typ属性表示令牌的类型,JWT令牌统一写为JWT。最后,使用Base64 URL算法将上述JSON对象转换为字符串保存
{
"alg": "HS256",
"typ": "JWT"
}
1.3.2 Payload
有效载荷部分,是JWT的主体内容部分,也是一个JSON对象,包含需要传递的数据。 JWT指定七个默认字段供选择
iss:发行人
exp:到期时间
sub:主题
aud:用户
nbf:在此之前不可用
iat:发布时间
jti:JWT ID用于标识该JWT
除以上默认字段外,我们还可以自定义私有字段,一般会把包含用户信息的数据放到payload中,如下例:
{
"sub": "1234567890",
"name": "Helen",
"admin": true
}
请注意:默认情况下JWT是未加密的,因为只是采用base64算法,拿到JWT字符串后可以转换回原本的JSON数据,任何人都可以解读其内容,因此不要构建隐私信息字段,比如用户的密码一定不能保存到JWT中,以防止信息泄露。JWT只是适合在网络中传输一些非敏感的信息
JWT 解码工具:在线JWT Token解析解码工具_x@lijun的博客-CSDN博客
1.3.3 Signature
签名哈希部分是对上面两部分数据签名,需要使用base64编码后的header和payload数据,通过指定的算法生成哈希,以确保数据不会被篡改。首先,需要指定一个密钥(secret)。该密码仅仅为保存在服务器中,并且不能向用户公开。然后,使用header中指定的签名算法(默认情况下为HMAC SHA256)根据以下公式生成签名
HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret) HMACSHA256(base64UrlEncode(header)+"."+base64UrlEncode(payload),secret)
在计算出签名哈希后,JWT头,有效载荷和签名哈希的三个部分组合成一个字符串,每个部分用.分隔,就构成整个JWT对象
-
注意JWT每部分的作用,在服务端接收到客户端发送过来的JWT token之后: header和payload可以直接利用base64解码出原文,从header中获取哈希签名的算法,从payload中获取有效数据 signature由于使用了不可逆的加密算法,无法解码出原文,它的作用是校验token有没有被篡改。服务端获取header中的加密算法之后,利用该算法加上secretKey对header、payload进行加密,比对加密后的数据和客户端发送过来的是否一致。注意secretKey只能保存在服务端,而且对于不同的加密算法其含义有所不同,一般对于MD5类型的摘要加密算法,secretKey实际上代表的是盐值
SpringBoot 整合JWT实现基于自定义注解的-实现一个登录请求验证拦截
1:创建数据库结构
CREATE TABLE `users` (
`id` int NOT NULL AUTO_INCREMENT,
`username` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
`password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci ROW_FORMAT=DYNAMIC;
2: 创建SpringBoot(2.3.5.RELEASE版本)工程,引入pom依赖
pom.xml
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MP -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.11.0</version>
</dependency>
<!-- hutool -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.8.12</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
3: application.properties 文件
server.port=8900
spring.application.name=jwt
mybatis-plus.configuration.log-impl=org.apache.ibatis.logging.log4j2.Log4j2Impl
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.password=root
spring.datasource.url=jdbc:mysql://localhost:3306/peixun?serverTimezone=GMT%2B8
spring.datasource.username=root
4:JWTUtil 工具类
public class JWTUtils {
private static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHOsdadasdasfdssfeweee";
/**
* 生成token
* @param map 传入payload
* @return 返回token
*/
public static String getToken(Map<String,String> map){
Calendar instance = Calendar.getInstance();
// instance.add(Calendar.MILLISECOND,20);
instance.add(Calendar.DAY_OF_WEEK,7);
// 创建 JWT builder
JWTCreator.Builder builder = JWT.create();
// payload
map.forEach(builder::withClaim);
// 设置过期时间
builder.withExpiresAt(instance.getTime());
// 设置签名加密
String token = builder.sign(Algorithm.HMAC256(APP_SECRET));
return token;
}
/**
* 验证token
* @param token
* @return
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(APP_SECRET)).build().verify(token);
}
/**
* 获取token中payload
* @param token
* @return
*/
public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(APP_SECRET)).build().verify(token);
}
}
5:封装统计返回结果类
Result
public class Result implements Serializable {
/**
*
*/
private static final long serialVersionUID = 1L;
//状态码
private Integer code;
//响应消息
private String msg;
//响应数据
private Object data;
private Integer count;
public Integer getCount() {
return count;
}
public void setCount(Integer count) {
this.count = count;
}
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
public Object getData() {
return data;
}
public void setData(Object data) {
this.data = data;
}
public Result(ResultCode resultCode,Object data) {
this.code=resultCode.getCode();
this.msg=resultCode.getMsg();
this.data=data;
}
public Result(Integer code,String msg,Object data) {
this.code=code;
this.msg=msg;
this.data=data;
}
}
ResultCode
public enum ResultCode {
//成功
SUCCESS(200,"成功"),
PARAM_IS_INVALID(1001,"参数无效"),
PARAM_IS_BLANK(1002,"参数为空"),
PARAM_IS_ERROR(1004,"参数错误"),
PARAM_IS_COMPLETE(1003,"参数缺失"),
USER_LONGIN_ERROR(2001,"账号或密码错误"),
USER_LONGIN_EXIST(2002,"用户不存在"),
USER_LONGIN_EXISTD(2003,"用户已存在"),
KHXX_EXISTD(2010,"客户信息已存在!"),
USER_LONGIN_EXAMINE(2004,"用户待审核"),
UPDATE_EXAMINE(2006,"修改申请已提交,等待管理员审核"),
USER_LONGIN_STOP(2005,"用户已停用"),
ERROR(500,"操作失败"),
CUSTOMER_EXISTD(400,"客户信息已存在"),
PHONE_ERROR(402,"验证码或者手机号错误"),
PHONE_EXIST(405,"手机号为空"),
PHONE_RETRY(405,"请2分钟后重试"),
PHONE_FPRMATEXIST(406,"手机号错误"),
SINE_ERROR(401,"用户信息已过期,请重新登录"),
LONG_OVERTIME(406,"登录超时,请重新登录"),
KHXX_HTJH_EXISTD(1005,"该类型的计划已存在"),
USER_INSUFFICIENT_AUTHORITY(403,"权限不足"),
FILE_NOTBLANK(2007,"上传文件不能为空"),
FILE_MAXSIZE(2008,"上传文件不能超过2M"),
FILE_ERRORSUFFIX(2009,"上传文件错误,只能上传文档或表格以及PDF格式");
private Integer code;
private String msg;
public Integer getCode() {
return code;
}
public void setCode(Integer code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
ResultCode(Integer code,String msg){
this.code=code;
this.msg=msg;
}
}
上述准备工作以及完成
6:pojo 实体类
@Data
public class Users {
private String id;
private String username;
private String password;
}
7: annotation 自定义注解
/**
* @author xia
* @version 1.0
* @Data 用来跳过验证的 PassToken
* @date 2023/2/13 16:16
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
/**
* @author xia
* @version 1.0
* @DATA 用于登录后才能操作的token
* @date 2023/2/13 16:16
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface UserLoginToken {
boolean required() default true;
}
8:拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtInterceptor())
.excludePathPatterns("/user/**")
.addPathPatterns("/**");
}
@Bean
public JWTInterceptor jwtInterceptor() {
return new JWTInterceptor();
}
}
@Component
@SuppressWarnings("all")
public class JWTInterceptor implements HandlerInterceptor {
@Autowired
User user;
// 请求前到达之前拦截
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object object)
throws Exception {
// 从请求头获取 token
String token = request.getHeader("token");
//首先映射是不是方法,如果不是则返回
if(!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod=(HandlerMethod) object;
Method method=handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
// token 验证失败后的返回信息
if (method.isAnnotationPresent(UserLoginToken.class)) {
UserLoginToken userLoginToken = method.getAnnotation(UserLoginToken.class);
if (userLoginToken.required()) {
if (StrUtil.isBlank(token)) {
throw new RuntimeException( "无token,请重新登录");
}
// 获取 token 中的 user id
int userId;
try {
DecodedJWT decode = JWT.decode(token);
String id = decode.getClaim("id").asString();
userId = Integer.parseInt(id);
} catch (JWTDecodeException j) {
throw new RuntimeException("401");
}
System.out.println(userId);
Users users = user.findUserById(userId);
if (users == null) {
throw new RuntimeException("用户不存在,请重新登录");
}
try {
// JWT 工具包验证 token
JWTUtils.verify(token);
return true;
} catch (TokenExpiredException e) {
throw new RuntimeException("Token已经过期!!!");
} catch (SignatureVerificationException e){
throw new RuntimeException("签名错误!!!");
} catch (AlgorithmMismatchException e){
throw new RuntimeException("加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("无效token~~");
}
}
}
return true;
}
}
9:controller 层
@RestController
@Slf4j
@SuppressWarnings("all")
public class JwtController {
@Resource
private UsersMapper mapper;
// 认证
@PassToken
@PostMapping("/user/login")
public Result login(@RequestBody Users user) {
System.out.println("前端传来的用户数据:"+user);
try {
// 查询数据库
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
HashMap<String, Object> queryMap = new HashMap<>();
queryMap.put("username", user.getUsername());
queryMap.put("password", user.getPassword());
queryWrapper.allEq(queryMap);
Users userDb = mapper.selectOne(queryWrapper);
// 认证失败
if(userDb == null) {
throw new RuntimeException("没有此用户。请重新登录");
}
// 认证成功
Map<String, String> tokenClaimsMap = new HashMap<>(); //用来存放payload
tokenClaimsMap.put("id",userDb.getId());
tokenClaimsMap.put("username", userDb.getUsername());
String token = JWTUtils.getToken(tokenClaimsMap);
// 返回的数据
return new Result(ResultCode.SUCCESS,token);
} catch (Exception e) {
// 认证失败,返回的数据
e.printStackTrace();
return new Result(ResultCode.ERROR,e);
}
}
@UserLoginToken
@GetMapping("/private/info")
public String info(HttpServletRequest request){
String token = request.getHeader("token");
DecodedJWT tokenJWT = JWTUtils.getToken(token);
System.out.println(tokenJWT.getClaim("username").asString());
System.out.println(tokenJWT.getClaim("userId").asInt());
return "这是一段私人信息。只有登录才能显示";
}
@UserLoginToken
@GetMapping("/private/Test")
public Result info(){
String code = "这是一段私人信息。只有登录才能显示";
return new Result(ResultCode.SUCCESS,code);
}
@PassToken()
@PostMapping("/private/Test2")
public Result info2(){
String code = "这是一段普通信息。不登录也能显示";
return new Result(ResultCode.SUCCESS,code);
}
}
10: service 以及实现类
@Service
public interface User {
Users findUserById(int userId);
}
@Service
public class UserService implements User{
@Resource
private UsersMapper mapper;
@Override
public Users findUserById(int userId) {
Users users = mapper.selectById(userId);
System.out.println("------------>"+ users);
return users;
}
}
11: mapper层
@Mapper
public interface UsersMapper extends BaseMapper<Users> {
}
启动项目,演示效果如下:
1:账号登录成功生成Token (登录请求,使用@PassToken() 注解 跳过token验证)
@PassToken
@PostMapping("/user/login")
public Result login(@RequestBody Users user) {
System.out.println("前端传来的用户数据:"+user);
try {
// 查询数据库
QueryWrapper<Users> queryWrapper = new QueryWrapper<>();
HashMap<String, Object> queryMap = new HashMap<>();
queryMap.put("username", user.getUsername());
queryMap.put("password", user.getPassword());
queryWrapper.allEq(queryMap);
Users userDb = mapper.selectOne(queryWrapper);
// 认证失败
if(userDb == null) {
throw new RuntimeException("没有此用户。请重新登录");
}
// 认证成功
Map<String, String> tokenClaimsMap = new HashMap<>(); //用来存放payload
tokenClaimsMap.put("id",userDb.getId());
tokenClaimsMap.put("username", userDb.getUsername());
String token = JWTUtils.getToken(tokenClaimsMap);
// 返回的数据
return new Result(ResultCode.SUCCESS,token);
} catch (Exception e) {
// 认证失败,返回的数据
e.printStackTrace();
return new Result(ResultCode.ERROR,e);
}
2:使用这个注解 :@UserLoginToken(登录需要验证)
@UserLoginToken
@GetMapping("/private/info")
public String info(HttpServletRequest request){
String token = request.getHeader("token");
DecodedJWT tokenJWT = JWTUtils.getToken(token);
System.out.println(tokenJWT.getClaim("username").asString());
System.out.println(tokenJWT.getClaim("userId").asInt());
return "这是一段私人信息。只有登录才能显示";
}
@UserLoginToken
@GetMapping("/private/Test")
public Result info(){
String code = "这是一段私人信息。只有登录才能显示";
return new Result(ResultCode.SUCCESS,code);
}
不携带Token 打印结果:
Token 错误:
3: 使用@PassToken() 放行请求 ,即不需要Token 也可登录
@PassToken()
@PostMapping("/private/Test2")
public Result info2(){
String code = "这是一段普通信息。不登录也能显示";
return new Result(ResultCode.SUCCESS,code);
}
源码地址如下:
SpringBoot 整合 jwt: 学习token的1111