SpringBoot集成JWT实现Token登录验证

SpringBoot集成JWT实现Token登录验证

参考文章:

https://blog.csdn.net/weixin_46666822/article/details/125110714

https://www.cnblogs.com/hanease/p/16381898.html

https://blog.csdn.net/AkiraNicky/article/details/99307713

https://blog.csdn.net/qq_42263280/article/details/128009297

JWT介绍

JWT(JSON Web Token)是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准。用户登录成功之后,将用户的信息进行加密,然后生成一个token返回给客户端,服务器不保存任何用户信息,服务器通过使用保存的密钥验证token的正确性。把用户信息存到token中,这样客户端、服务端都可以从token中获取用户的基本信息,既然客户端可以获取,肯定是不能存放敏感信息的,因为浏览器可以直接从token获取用户信息。JWT是目前最流行的跨域认证解决方案,适合前后端分离项目通过Restful API进行数据交互时进行身份认证。

Session认证、Token认证、JWT认证

session认证

众所周知,http 协议本身是无状态的协议,那就意味着当有用户向系统使用账户名称和密码进行用户认证之后,下一次请求还要再一次用户认证才行。因为我们不能通过 http 协议知道是哪个用户发出的请求,所以如果要知道是哪个用户发出的请求,那就需要在服务器保存一份用户信息(保存至 session ),然后在认证成功后返回 cookie 值传递给浏览器,那么用户在下一次请求时就可以带上 cookie 值,服务器就可以识别是哪个用户发送的请求,是否认证,是否登录过期等等。这就是传统的 session 认证方式。

session 认证的缺点其实很明显,由于 session 是保存在服务器里,所以如果分布式部署应用的话,会出现session不能共享的问题,很难扩展。于是乎为了解决 session 共享的问题,又引入了 redis,接着往下看。

token认证

这种方式跟 session 的方式流程差不多,不同的地方在于保存的是一个 token 值到 redis,token 一般是一串随机的字符(比如UUID),value 一般是用户ID,并且设置一个过期时间。每次请求服务的时候带上 token 在请求头,后端接收到token 则根据 token 查一下 redis 是否存在,如果存在则表示用户已认证,如果 token 不存在则跳到登录界面让用户重新登录,登录成功后返回一个 token 值给客户端。

优点是多台服务器都是使用 redis 来存取 token,不存在不共享的问题,所以容易扩展。缺点是每次请求都需要查一下redis,会造成 redis 的压力,还有增加了请求的耗时,每个已登录的用户都要保存一个 token 在 redis,也会消耗 redis 的存储空间。

有没有更好的方式呢?接着往下看。

JWT认证

JWT认证与token方式有一些不同的地方,就是不需要依赖 redis,用户信息存储在客户端。所以关键在于生成 JWT 和解析 JWT 这两个地方。

源:https://blog.csdn.net/weixin_45410366/article/details/125031959

JWT构成

JWT分为三部分:Header(头部)、Payload(载荷)、Signature(签名)。这三个部分用.进行分隔,格式如下:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VyX2lkIjoiMSIsImV4cCI6MTY4MzEzMzA0OCwiaWF0IjoxNjgzMTMyOTg4LCJ1c2VybmFtZSI6ImFkbWluIn0.K3QVgpTOLP9kaDdZ-AMCV-_Jf9HOFxoeBqCvtCfANOc
头部
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

使用base64加密,用于存放token类型和加密算法,也可以使用JWT.create().withHeader()方法自定义放置在头部的信息。使用js的atob()方法可以将base64加密的字符串解密。

在这里插入图片描述

typ: 声明类型,通常为JWT
alg: 声明加密的算法,通常直接使用HMACSHA256,就是HS256了
载荷
eyJ1c2VyX2lkIjoiMSIsImV4cCI6MTY4MzEzMzA0OCwiaWF0IjoxNjgzMTMyOTg4LCJ1c2VybmFtZSI6ImFkbWluIn0

使用base64加密,存放附带的信息:比如用户的nickname,id,username等,这样以后验证了令牌之后就可以直接从这里获取信息而不用再查数据库。

在这里插入图片描述

载荷中的内容又可以分为3种标准:

  • 标准中注册的声明

  • 公共的声明

  • 私有的声明

【标准声明】

iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

【公共声明】

公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密

【私有声明】

私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息
签名

JWT的第三部分是一个签证信息,这个签证信息由三部分组成:

header (base64后的)
payload (base64后的)
secret
第三部分需要base64加密后的header和base64加密后的payload使用 . 连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了JWT的第三部分。

**加盐:**https://www.jianshu.com/p/dafc031f72a8

注意:

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分,secret是保存在服务器端的,JWT的签发生成也是在服务器端的,secret就是用来进行JWT的签发和JWT的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发JWT了。

服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名,如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

SpringBoot集成JWT

maven引入依赖
<!--JWT-->
<dependency>
	<groupId>com.auth0</groupId>
	<artifactId>java-jwt</artifactId>
	<version>3.7.0</version>
</dependency>
DTO实体类
package com.example.myweb.controller.dto;

import lombok.Data;
import javax.validation.constraints.NotEmpty;

@Data
public class UserDTO {
    @NotEmpty(message = "用户名不能为空")
    private String username;
    @NotEmpty(message = "密码不能为空")
    private String password;
    private String nickname;
    private String token;
}
TokenUtil工具类

用于获取Token

package com.example.myweb.util;

import cn.hutool.core.date.DateUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Date;

@Component
public class TokenUtil {

//    static Integer expire=60;//token过期时间,单位s

    /**
     * 从配置文件中读取token过期时间,并赋值给静态变量expire
     */
    static Integer expire;
    @Value("${jwt.expire}")
    public void setExpire(Integer expire){
        TokenUtil.expire=expire;
    }

    public static String createToken(Integer userId, String sign) {
        Date nowDate= new Date();
        Date expireDate = DateUtil.offsetSecond(nowDate, expire);
//        HashMap<String, Object> objectObjectHashMap = new HashMap<>();
//        objectObjectHashMap.put("testHeader","testHeader");
        return JWT.create()
//            .withHeader(objectObjectHashMap) //设置token头部header(第一部分)的信息
            .withClaim("id",userId) //将id=userId保存到token的载荷payload(第二部分)中
//            .withClaim("username",username) //将username=username保存到token的载荷payload(第二部分)中
//            .withSubject("testSubject") //将sub=testSubject保存到token的载荷payload(第二部分)中
//        .withAudience("testAudience") //将aud="testAudience"保存到token的载荷(第二部分)中
            .withIssuedAt(nowDate) //设置token创建时间,并将iat=nowDate保存到token的载荷payload(第二部分)中
            .withExpiresAt(expireDate) // 设置token过期时间,并将exp=expireDate保存到token的载荷payload(第二部分)中
//            设置签名(第三部分signature):这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
            .sign(Algorithm.HMAC256(sign));// 以sign作为token的密钥,并将加密算法HMAC256保存到token头部中header(第一部分)。//            设置签名(第三部分signature):这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。
    }
}
配置文件application.yaml
spring:
  datasource:
    url: jdbc:mysql://localhost:3306/myweb_db?ServerTimezone=GMT%2b8
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 12345678
  mvc:
    pathmatch:
      matching-strategy: ANT_PATH_MATCHER  #解决springboot整合swagger后启动项目时报org.springframework.context.ApplicationContextException: Failed to start bean 'documentationPluginsBootstrapper';的错误

server:
  port: 8888

mybatis:
  mapper-locations: classpath:mapper/*.xml
  type-aliases-package: com.example.myweb.entity 
  configuration:
    map-underscore-to-camel-case: true
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

jwt:
#Token过期时间
  expire: 60
# 加密秘钥
#  secret: abcdefg123456 我们不使用固定的秘钥,而是使用用户的密码作为秘钥
编写统一异常处理类

《统一异常处理与统一响应返回.md》

自定义跳出拦截器的注解

方便我们后续使用自定义注解去标记不需要拦截的方法。

package com.example.myweb.config;

import java.lang.annotation.*;

/**
 * 自定义跳出拦截器的注解,添加此注解时springmvc拦截器不会进行拦截
 */
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PassToken {
    boolean required() default true;
}

@Retention注解说明

RetentionPolicy.SOURCE:这种类型的Annotations只在源代码级别保留,编译时就会被忽略,在class字节码文件中不包含。

RetentionPolicy.CLASS:这种类型的Annotations编译时被保留,默认的保留策略,在class文件中存在,但JVM将会忽略,运行时无法获得。

RetentionPolicy.RUNTIME:这种类型的Annotations将被JVM保留,所以他们能在运行时被JVM或其他使用反射机制的代码所读取和使用。

**@Document:**说明该注解将被包含在javadoc中

**@Inherited:**说明子类可以继承父类中的该注解

自定义拦截器
package com.example.myweb.config;

import cn.hutool.core.util.StrUtil;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.example.myweb.entity.User;
import com.example.myweb.exception.AppException;
import com.example.myweb.exception.AppExceptionCodeMsg;
import com.example.myweb.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Date;

/**
 * 自定义拦截器
 */
@Component
public class JwtInterceptor implements HandlerInterceptor {

    @Autowired
    private UserService userService;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {

        // 如果不是映射方法直接放行
        if(!(handler instanceof HandlerMethod)){
            return true;
        } else {
            HandlerMethod h = (HandlerMethod) handler;
            //检查是否添加了@PassToken注解
            PassToken passToken = h.getMethodAnnotation(PassToken.class);
            //添加了@AuthAccess注解的方法则直接放行
            if (passToken != null) {
                return true;
            }
        }

        //从请求头中获取token,提前和前端约定好把jwt生成的token放到请求头中的token字段中
        String token = request.getHeader("token");

        // 执行认证,StrUtil.isBlank()是hutool依赖中的方法,需要先在pom.xml中导入hutool依赖
        if (StrUtil.isBlank(token)) {
            throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_NO_TOKEN);
        }
        //检查token是否过期
        Date expiresAt = JWT.decode(token).getExpiresAt();
        if (expiresAt.before(new Date())){
            throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_TOKEN_EXPIRE);
        }

        // 获取token中的user_id
        Integer userId;
        try {
            userId = JWT.decode(token).getClaim("id").asInt();
            if(userId==null){
                throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_TOKEN_FAILED);
            }
        } catch (JWTDecodeException j) {
            throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_TOKEN_FAILED); //出现异常说明token解析出错,即token被修改了
        }
        // 根据token中的userid查询数据库
        User user = userService.getUserById(userId);
        if (user == null) {
            throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_NO_USER);
        }
        // 使用用户密码验证token
        try {
            JWT.require(Algorithm.HMAC256(user.getPassword())).build().verify(token); //使用用户密码作为秘钥,使用HMAC256算法验证token的第三部分
        } catch (JWTVerificationException e) {
            throw new AppException(AppExceptionCodeMsg.USER_AUTHENTICATION_TOKEN_FAILED);
        }
        return true;
    }
}

这里需要说明一下实现拦截器的方法,我们只需要实现HandlerInterceptor接口即可,它主要定义了三个方法:

boolean preHandle ():

预处理回调方法,实现处理器的预处理,第三个参数为响应的处理器,自定义Controller,返回值为true表示继续流程(如调用下一个拦截器或处理器)或者接着执行postHandle()和afterCompletion();false表示流程中断,不会继续调用其他的拦截器或处理器,中断执行。

void postHandle():

后处理回调方法,实现处理器的后处理(DispatcherServlet进行视图返回渲染之前进行调用),此时我们可以通过modelAndView(模型和视图对象)对模型数据进行处理或对视图进行处理,modelAndView也可能为null。

void afterCompletion():

整个请求处理完毕回调方法,该方法也是需要当前对应的Interceptor的preHandle()的返回值为true时才会执行,也就是在DispatcherServlet渲染了对应的视图之后执行。用于进行资源清理。

整个请求处理完毕回调方法。如性能监控中我们可以在此记录结束时间并输出消耗时间,还可以进行一些资源清理,类似于try-catch-finally中的finally,但仅调用处理器执行链中。

这里我们主要需要调用预处理回调方法即可,如果有其他业务需求,也可自行更改。

主要流程:

从 http 请求头中取出 token,

判断是否映射到方法

检查是否有passtoken注释,有则跳过认证

检查有没有需要用户登录的注解,有则需要取出并验证

认证通过则可以访问,不通过会报相关错误信息

然后通过配置类将我们自定义的拦截类注入到spring容器中,并进行拦截配置。

注册拦截器到SpringMvc
package com.example.myweb.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.annotation.Resource;

/**
 * 注册自定义拦截器到SpringMVC
 */
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
    @Resource
    JwtInterceptor jwtInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtInterceptor)//注册
                .addPathPatterns("/**")  // 拦截所有请求,通过判断token是否合法来决定是否需要登录
                .excludePathPatterns("/user/login","/user/register",
                        "/swagger-resources/**", "/webjars/**", "/v2/**", "/swagger-ui.html/**", "/api", "/api-docs", "/api-docs/**")
                .excludePathPatterns( "/**/*.html", "/**/*.js", "/**/*.css", "/**/*.woff", "/**/*.ttf");  // 放行静态文件
    }
}
Controller
package com.example.myweb.controller;

import cn.hutool.crypto.SecureUtil;
import com.example.myweb.common.Result;
import com.example.myweb.controller.dto.UserDTO;
import com.example.myweb.entity.User;
import com.example.myweb.service.UserService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import javax.validation.Valid;
import java.util.List;

@RestController
@RequestMapping(value = "/user")
@Validated
@Api(value = "用户接口", tags = {"用户接口"})
public class UserController{
    @Resource
    private UserService userService;
  
    @ApiOperation("用户登录")
    @PostMapping("/login")
    public Result<UserDTO> login(@RequestBody @Valid() UserDTO userDTO){
        UserDTO login = userService.login(userDTO);
        return Result.success("登录成功",login);
    }
}
Service
package com.example.myweb.service.impl;

import cn.hutool.crypto.SecureUtil;
import com.example.myweb.controller.dto.UserDTO;
import com.example.myweb.entity.User;
import com.example.myweb.exception.AppException;
import com.example.myweb.exception.AppExceptionCodeMsg;
import com.example.myweb.mapper.UserMapper;
import com.example.myweb.service.UserService;
import com.example.myweb.util.TokenUtil;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;
import java.util.List;

@Service
public class UserServiceImpl implements UserService {

    @Resource
    private UserMapper userMapper;

    @Override
    public UserDTO login(UserDTO userDTO) {
        User user = new User();
        user.setUsername(userDTO.getUsername());
        user.setPassword(SecureUtil.md5(userDTO.getPassword()));
        List<User> user1 = userMapper.getUser(user);
        if(user1==null || user1.size()==0){
            throw new AppException(AppExceptionCodeMsg.USERNAME_NOT_EXISTS);
        }
        List<User> login = userMapper.login(user);
        if(login!=null&&login.size()==1){
            String token = TokenUtil.createToken(login.get(0).getId(), login.get(0).getPassword());
            userDTO.setNickname(login.get(0).getNickname());
            userDTO.setToken(token);
            return userDTO;
        }else {
            throw new AppException(AppExceptionCodeMsg.USER_LOGIN_ACCOUNT);
        }
    }

    @Override
    public User getUserById(Integer id) {
        return userMapper.getUserById(id);
    }
}
package

在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值