当前位置:   article > 正文

SpringBoot集成JWT实现Token登录验证_springboot jwt token

springboot jwt token

目录

1.1 JWT是什么?

1.2 JWT主要使用场景

1.3 JWT请求流程

1.4 JWT结构

二,SpringBoot集成JWT具体实现过程

2.1添加相关依赖

2.2自定义跳出拦截器的注解

2.3自定义全局统一返回值方法,异常类及相关枚举

2.4编写JWT工具类,用于生成Token令牌

2.5编写拦截器并注入容器

三,测试

3.1放行一般类接口

3.2放行登录接口


1.1 JWT是什么?

JWT官网

在JWT官网中可以明确看到关于它的定义

JSON Web令牌(JWT)是一种开放的标准(RFC 7519),它定义了一种紧凑而独立的方式在各方之间安全地传输信息为JSON对象。该信息可以被验证和信任,因为它是数字签名的。JWT可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公开/私有密钥类型签名。 虽然JWT可以被加密以提供各方之间的保密,但我们将重点关注签名的令牌。被签名的令牌可以验证包含在其中的声明的完整性,而加密的令牌对其他各方隐藏这些声明。当使用公钥/私钥对签名时,签名还证明只有持有私钥的一方才是签名方。

1.2 JWT主要使用场景

  1. 授权(Authorization):这是使用JWT最常见的场景。一旦用户登录,每个后续请求都将包括JWT,允许用户访问该令牌允许的路由、服务和资源。

  2. 单点登录(Single Sign On ):单点登录是当今广泛使用的JWT特性,因为它的小规模和易于跨不同领域使用的能力。

  3. 信息交换(lnformation Exchange):信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。

  4. 传输信息(transmitting information):在各方之间传输信息。由于JWT可以签名--例如,使用公共/私钥对--您可以确保发件人是他们所说的发送者。此外,由于签名是使用头和有效载荷计算的,您还可以验证内容没有被篡改。

1.3 JWT请求流程

  1. 用户使用账号和密码发出post请求;
  2. 服务器使用私钥创建一个jwt;
  3. 服务器返回这个jwt给浏览器;
  4. 浏览器将该jwt串在请求头中像服务器发送请求;
  5. 服务器验证该jwt;
  6. 返回响应的资源给浏览器。

1.4 JWT结构

JWT是由三段信息构成的,将这三段信息文本用.连接一起就构成了JWT字符串。

就像这样:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

 JWT包含了三部分:

  • Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型)

  • Payload 负载 (类似于飞机上承载的物品)

  • Signature 签名/签证

Header

JWT的头部承载两部分信息:token类型和采用的加密算法。

  1. {
  2. "alg": "HS256",
  3. "typ": "JWT"
  4. }

声明类型:这里是jwt

声明加密的算法:通常直接使用 HMAC SHA256

加密算法是单向函数散列算法,常见的有MD5、SHA、HAMC。

MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值

SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5

HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证

Payload

载荷就是存放有效信息的地方。

有效信息包含三个部分:

  1. 标准中注册的声明

  2. 公共的声明

  3. 私有的声明

标准中注册的声明 (建议但不强制使用) :

  • iss: jwt签发者

  • sub: 面向的用户(jwt所面向的用户)

  • aud: 接收jwt的一方

  • exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间)

  • nbf: 定义在什么时间之前,该jwt都是不可用的.

  • iat: jwt的签发时间

  • jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。

公共的声明:

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

私有的声明:

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

Signature

jwt的第三部分是一个签证信息

这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。最主要的目的:服务器应用在接受到JWT后,会首先对头部和载荷的内容用同一算法再次签名,如果服务器应用对头部和载荷再次以同样方法签名之后发现,自己计算出来的签名和接受到的签名不一样,那么就说明这个Token的内容被别人动过的,我们应该拒绝这个Token,返回一个HTTP 401 Unauthorized响应。

密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。

这些信息在官网上也有相关信息的说明:

二,SpringBoot集成JWT具体实现过程

这里由于只涉及对验证功能的实现,因此其他数据库,业务类编写一概省略,只对相关步骤做说明。

2.1添加相关依赖

既然要使用JWT我们肯定需要引入其依赖

  1. <dependency>
  2. <groupId>com.auth0</groupId>
  3. <artifactId>java-jwt</artifactId>
  4. <version>3.10.3</version>
  5. </dependency>

2.2自定义跳出拦截器的注解

方便我们后续不用对在配置拦截器时排除每一个接口,只用自定义注解去标记即可。

  1. import java.lang.annotation.ElementType;
  2. import java.lang.annotation.Retention;
  3. import java.lang.annotation.RetentionPolicy;
  4. import java.lang.annotation.Target;
  5. /**
  6. * @author young
  7. * @date 2022/11/24 14:53
  8. * @description: 自定义通过token注解,如果不加该注解直接拦截
  9. */
  10. @Target({ElementType.METHOD,ElementType.TYPE})
  11. @Retention(RetentionPolicy.RUNTIME)
  12. public @interface PassToken {
  13. boolean required() default true;
  14. }

@Retention注解说明

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

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

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

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

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

2.3自定义全局统一返回值方法,异常类及相关枚举

定义全局枚举类

  1. package com.yy.enums;
  2. import lombok.AllArgsConstructor;
  3. import lombok.Data;
  4. import lombok.Getter;
  5. import java.util.UUID;
  6. /**
  7. * @author young
  8. * @date 2022/8/19 21:36
  9. * @description: 响应结果枚举
  10. */
  11. @AllArgsConstructor
  12. @Getter
  13. public enum ResponseEnum {
  14. /**响应成功**/
  15. SUCCESS(200, "操作成功"),
  16. /**操作失败*/
  17. FAIL(201,"获取数据失败"),
  18. NO_TOKEN(400,"无token,请重新登录"),
  19. TOKEN_EX(401,"token验证失败,请重新登录"),
  20. USER_EX(402,"用户不存在,请重新登录"),
  21. /**错误请求**/
  22. ERROR(400,"错误请求");
  23. /**响应码**/
  24. private final Integer code;
  25. /** 结果 **/
  26. private final String resultMessage;
  27. public static ResponseEnum getResultCode(Integer code){
  28. for (ResponseEnum value : ResponseEnum.values()) {
  29. if (code.equals(value.getCode())){
  30. return value;
  31. }
  32. }
  33. return ResponseEnum.ERROR;
  34. }
  35. /*
  36. 简单测试一下
  37. */
  38. public static void main(String[] args) {
  39. ResponseEnum resultCode = ResponseEnum.getResultCode(100);
  40. System.out.println(resultCode);
  41. }
  42. }

定义全局异常类

  1. package com.yy.util;
  2. import com.yy.Enums.ResultEnum;
  3. import io.swagger.annotations.ApiModel;
  4. import io.swagger.annotations.ApiModelProperty;
  5. import lombok.Data;
  6. /**
  7. * @author young
  8. * @date 2022/9/25 19:15
  9. * @description: 自定义运行时异常
  10. */
  11. @Data
  12. @ApiModel(value = "自定义全局异常类")
  13. public class MyException extends RuntimeException{
  14. @ApiModelProperty(value = "异常状态码")
  15. private final Integer code;
  16. /**
  17. * 通过状态码和异常信息创建异常对象
  18. * @param code
  19. * @param message
  20. */
  21. public MyException(Integer code,String message) {
  22. super(message);
  23. this.code = code;
  24. }
  25. /**
  26. * 接受枚举类型对象
  27. * @param resultEnum
  28. */
  29. public MyException(ResponseEnum responseEnum){
  30. super(responseEnum.getMessage());
  31. this.code = responseEnum.getCode();
  32. }
  33. }
  1. package com.yy.Config;
  2. import com.yy.utils.MyException;
  3. import com.yy.utils.R;
  4. import org.springframework.web.bind.annotation.ExceptionHandler;
  5. import org.springframework.web.bind.annotation.RestControllerAdvice;
  6. /**
  7. * @author young
  8. * @date 2022/9/12 15:43
  9. * @description: 自定义异常配置
  10. */
  11. @RestControllerAdvice
  12. public class GlobalExceptionConfig{
  13. @ExceptionHandler(MyException.class)
  14. public R<MyException> handle(MyException e){
  15. e.printStackTrace();
  16. return R.exception(e.getCode(),e.getMessage());
  17. }
  18. }

定义统一返回结果类

  1. package com.yy.utils;
  2. import com.yy.enums.ResponseEnum;
  3. import lombok.Data;
  4. import java.io.Serializable;
  5. import java.util.HashMap;
  6. import java.util.Map;
  7. /**
  8. * @author young
  9. * @date 2022/8/19 21:52
  10. * @description: 统一返回结果的类
  11. */
  12. @Data
  13. public class R<T> implements Serializable {
  14. private static final long serialVersionUID = 56665257248936049L;
  15. /**响应码**/
  16. private Integer code;
  17. /**返回消息**/
  18. private String message;
  19. /**返回数据**/
  20. private T data;
  21. private R(){}
  22. /**
  23. * 操作成功ok方法
  24. */
  25. public static <T> R<T> ok(T data) {
  26. R<T> response = new R<>();
  27. response.setCode(ResponseEnum.SUCCESS.getCode()); response.setMessage(ResponseEnum.SUCCESS.getResultMessage());
  28. response.setData(data);
  29. return response;
  30. }
  31. /**
  32. * 编译失败方法
  33. */
  34. public static <T> R<T> buildFailure(Integer errCode, String errMessage){
  35. R<T> response = new R<>();
  36. response.setCode(errCode);
  37. response.setMessage(errMessage);
  38. return response;
  39. }
  40. public static <T> R<T> exception(Integer errCode, String errMessage){
  41. R<T> response = new R<>();
  42. response.setCode(errCode);
  43. response.setMessage(errMessage);
  44. return response;
  45. }
  46. }

2.4编写JWT工具类,用于生成Token令牌

  1. package com.yy.utils;
  2. import cn.hutool.core.date.DateUtil;
  3. import com.auth0.jwt.JWT;
  4. import com.auth0.jwt.algorithms.Algorithm;
  5. import java.util.Date;
  6. /**
  7. * @author young
  8. * @date 2022/9/12 14:46
  9. * @description: 整合JWT生成token
  10. */
  11. public class JwtTokenUtils {
  12. private JwtTokenUtils() {
  13. throw new IllegalStateException("Utility class");
  14. }
  15. /**
  16. * 生成token
  17. * @param userId
  18. * @param sign
  19. * @return
  20. */
  21. public static String getToken(String userId,String sign){
  22. return JWT.create()
  23. //签收者
  24. .withAudience(userId)
  25. //主题
  26. .withSubject("token")
  27. //2小时候token过期
  28. .withExpiresAt(DateUtil.offsetHour(new Date(),2))
  29. //以password作为token的密钥
  30. .sign(Algorithm.HMAC256(sign));
  31. }
  32. }

Algorithm.HMAC256():使用HS256生成token,密钥则是用户的密码,唯一密钥的话可以保存在服务端。

withAudience():存入需要保存在token的信息,这里我们把用户ID存入token中

2.5编写拦截器并注入容器

  1. package com.yy.Config.inteceptor;
  2. import cn.hutool.core.text.CharSequenceUtil;
  3. import com.auth0.jwt.JWT;
  4. import com.auth0.jwt.JWTVerifier;
  5. import com.auth0.jwt.algorithms.Algorithm;
  6. import com.auth0.jwt.exceptions.JWTDecodeException;
  7. import com.auth0.jwt.exceptions.JWTVerificationException;
  8. import com.yy.enums.ResponseEnum;
  9. import com.yy.admin.pojo.Admin;
  10. import com.yy.admin.service.Impl.AdminServiceImpl;
  11. import com.yy.utils.MyException;
  12. import org.springframework.beans.factory.annotation.Autowired;
  13. import org.springframework.stereotype.Component;
  14. import org.springframework.web.method.HandlerMethod;
  15. import org.springframework.web.servlet.HandlerInterceptor;
  16. import javax.servlet.http.HttpServletRequest;
  17. import javax.servlet.http.HttpServletResponse;
  18. import java.lang.reflect.Method;
  19. /**
  20. * @author young
  21. * @date 2022/9/12 15:37
  22. * @description: 获取token并验证
  23. */
  24. @Component
  25. public class MyJwtInterceptor implements HandlerInterceptor {
  26. @Autowired
  27. private AdminServiceImpl adminService;
  28. @Override
  29. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
  30. String token = request.getHeader("token");
  31. if (!(handler instanceof HandlerMethod)) {
  32. return true;
  33. }
  34. HandlerMethod handlerMethod = (HandlerMethod) handler;
  35. Method method = handlerMethod.getMethod();
  36. //检查是否通过有PassToken注解
  37. if (method.isAnnotationPresent(PassToken.class)) {
  38. //如果有则跳过认证检查
  39. PassToken passToken = method.getAnnotation(PassToken.class);
  40. if (passToken.required()) {
  41. return true;
  42. }
  43. }
  44. //否则进行token检查
  45. if (CharSequenceUtil.isBlank(token)) {
  46. throw new MyException(ResponseEnum.TOKEN_EX.getCode(), ResponseEnum.TOKEN_EX.getResultMessage());
  47. }
  48. //获取token中的用户id
  49. String userId;
  50. try {
  51. userId = JWT.decode(token).getAudience().get(0);
  52. } catch (JWTDecodeException j) {
  53. throw new MyException(ResponseEnum.TOKEN_EX.getCode(), ResponseEnum.TOKEN_EX.getResultMessage());
  54. }
  55. //根据token中的userId查询数据库
  56. Admin user = adminService.getById(userId);
  57. if (user == null) {
  58. throw new MyException(ResponseEnum.USER_EX.getCode(), ResponseEnum.USER_EX.getResultMessage());
  59. }
  60. //验证token
  61. JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(user.getPwd())).build();
  62. try {
  63. jwtVerifier.verify(token);
  64. } catch (JWTVerificationException e) {
  65. throw new MyException(406, "权限验证失败!");
  66. }
  67. return true;
  68. }
  69. }

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

boolean preHandle ()

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

void postHandle()

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

void afterCompletion():

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

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

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

主要流程:

  1. 从 http 请求头中取出 token,

  2. 判断是否映射到方法

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

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

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

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

  1. package com.yy.Config.inteceptor;
  2. import org.springframework.context.annotation.Bean;
  3. import org.springframework.context.annotation.Configuration;
  4. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
  5. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
  6. /**
  7. * @author young
  8. * @date 2022/9/12 15:36
  9. * @description: JWT拦截配置
  10. */
  11. @Configuration
  12. public class InterceptorConfig implements WebMvcConfigurer {
  13. @Override
  14. public void addInterceptors(InterceptorRegistry registry) {
  15. registry.addInterceptor(jwtInterceptor())
  16. //拦截所有请求,通过判断token来决定是否需要登陆
  17. .addPathPatterns("/**");
  18. }
  19. @Bean
  20. public MyJwtInterceptor jwtInterceptor(){
  21. return new MyJwtInterceptor();
  22. }
  23. }

至此,我们对于JWT在SpringBoot中的基本配置就算完成了。我们只需在controller层在自己想要放行的api接口上添加我们自定义的放行注解,即可实现对api接口的放行,其他接口均要进行Token令牌的验证判断。如果没有Token则返回自定义的异常信息。

三,测试

3.1放行一般类接口

这里我们先只对一个业务接口进行放行。

  1. /**
  2. * 查找指定id信息
  3. *
  4. * @param id
  5. * @return
  6. */
  7. @GetMapping("/getOne/{id}")
  8. @CostTime
  9. @PassToken
  10. public R<TestUser> selectOne(@PathVariable Integer id) {
  11. TestUser user = testUserService.getById(id);
  12. return R.ok(user);
  13. }

进行接口测试后发现,该接口获取数据正常。

 其他没有加@PassToken的接口由于没有token进行验证,均会被拦截器拦截,并返回我们预期的异常信息"token验证失败,请重新登录"

因此,同理我们只需要在登陆注册或其他不需要token验证的接口上添加自定义注解即可实现拦截。

为了达到业务需求,我们需要在用户登录成功后获取到token,然后将token信息存放在每次的接口请求头(headers)上,这样就能实现对用户接口信息基本保护了。

3.2放行登录接口

在业务层处理token,将生成的token信息带到用户实体类中,这样登录获取用户信息时就能读取到token信息了

  1. package com.yy.admin.service.Impl;
  2. import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
  3. import com.yy.admin.pojo.Admin;
  4. import com.yy.admin.service.AdminService;
  5. import com.yy.admin.dao.AdminDao;
  6. import com.yy.utils.JwtTokenUtils;
  7. import lombok.extern.slf4j.Slf4j;
  8. import org.springframework.beans.factory.annotation.Autowired;
  9. import org.springframework.lang.Nullable;
  10. import org.springframework.stereotype.Service;
  11. import javax.annotation.Resource;
  12. import java.util.Optional;
  13. /**
  14. * @author young
  15. * @description 针对表【admin】的数据库操作Service实现
  16. * @createDate 2022-09-05 13:41:54
  17. */
  18. @Service
  19. @Slf4j
  20. public class AdminServiceImpl extends ServiceImpl<AdminDao, Admin>
  21. implements AdminService{
  22. @Resource
  23. private AdminDao adminDao;
  24. @Override
  25. public Admin getMsg(String username, String pwd){
  26. Admin admin = adminDao.selectByUsernameAndPwd(username, pwd);
  27. Optional.ofNullable(admin).ifPresent(u->{
  28. //添加token信息设置到用户实体上
  29. String token = JwtTokenUtils.getToken(String.valueOf(admin.getId()),pwd);
  30. log.info("token的值为:{}",token);
  31. admin.setToken(token);
  32. });
  33. return admin;
  34. }
  35. }

编写用户登录的业务接口并放行Token

  1. @PostMapping("/login")
  2. @PassToken
  3. public Object loginStatus(@RequestParam("username") String username, @RequestParam("password") String password){
  4. JSONObject object = new JSONObject();
  5. Admin admin = adminService.getMsg(username, password);
  6. if (admin!=null){
  7. object.put("code",1);
  8. object.put("msg","登陆成功");
  9. object.put("success",true);
  10. object.put("type","success");
  11. object.put("userMsg",admin);
  12. }else {
  13. object.put("code",0);
  14. object.put("success",false);
  15. object.put("msg","用户名或密码错误");
  16. object.put("type","error");
  17. }
  18. return object;
  19. }

进行接口测试

 这样就可以看到生成的Token信息了,然后我们将token信息设置在请求头上对其他接口进行测试。

此时就能看到,接口请求成功。

注意:这里的参数名token对应拦截器配置String token=request.getHeader("token")中的getHeader中设置的的参数名。

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/你好赵伟/article/detail/368036?site
推荐阅读
相关标签
  

闽ICP备14008679号