• springboot整合jwt认证


    引言:什么是jwt

    Json Web Token(JWT):JSON网络令牌,是为了在网络应用环境间传递声明而制定的一种基于JSON的开放标准((RFC 7519)。JWT是一个轻便的安全跨平台传输格式,定义了一个紧凑的自包含的方式用于通信双方之间以 JSON 对象行使安全的传递信息。因为数字签名的存在,这些信息是可信的。

    JWT的组成

    jwt含有三个部分

    • 头部(header)
    • 载荷(payload)
    • 签证(signature)

    头部(header)

    头部一般有两部分信息:类型、加秘的算法(通常使用HMAC SHA256)

    载荷(payload)

    该部分一般存放一些有效的信息。jwt的标准定义包含五个字段:

    • iss: jwt的签发者
    • sub:jwt所面向的用户
    • aud:接收该jwt的一方
    • exp(expires):什么时候过期,这里是一个Unit的时间戳
    • iat(issued at):在什么时候签发的

    签证(signature)

    jwt最后一个部分。该部分是使用了HS256加密后的数据;包含了三个部分:

    • header(base64后的)
    • payload(base64后的)
    • secret 私钥
      secret是保存在服务器端(server)的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证。所以,它就是服务端的密钥,z在任何场景都不应该流露出去。一旦客户端得知这个secret,那就有客户端自我签发jwt的安全危险了。

    jwt特点

    • 紧凑: 意味着这个字符串很小,甚至可以放在URL参数,POST Parameter中以Http Header的方式传输。
    • 自包含: 传输的字符串包含很多信息,别人拿到以后就不需要多次访问数据库获取信息,并且通过其中的信息就可以知道加密类型和方式(当然解密需要公钥和密钥)。

    如何使用jwt

    在身份鉴定的实现中,传统的方法是在服务端存储一个 session,给客户端返回一个 cookie,而使用JWT之后,当用户使用它的认证信息登录系统之后,会返回给用户一个JWT(token), 用户只需要本地保存该 token(通常使用localStorage,也可以使用cookie)即可。

    当用户希望访问一个受保护的路由或者资源的时候,通常应该在 Authorization头部使用 Bearer 模式添加JWT,其内容格式:

    Authorization: Bearer <token>
    

    因为用户的状态在服务端内容中是不存储的,所以这是一种无状态的认证机制。服务端的保护路由将会检查请求头 Authorization 中的JWT信息,如果合法,则允许用户的行为。由于JWT是自包含的,因此,减少了需要查询数据库的需要。

    JWT的这些特征使得我们可以完全依赖无状态的特性提供数据API服务。因为JWT并不使用Cookie的,所以你可以在任何域名提供你的API服务而不需要担心跨域资源共享问题(CORS)
    下面的序列图展示了该过程,流程图如下:

     

    中文流程介绍:

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

    说了这么多JWT到底如何应用到我们的项目中,下面我们就使用SpringBoot 结合 JWT完成用户的登录验证。

    应用

    初次登录生成jwt流程图

    用户访问资源流程图

    集成

    坏境:

    • spring boot 2.2.4.RELEASE
    • jjwt 0.9.1
      其它工具版本可以查看我gitee上pom.xml的依赖。

    下面通过代码来实现用户认证的功能,博主这里主要采用Spring Boot与JWT整合的方式实现
    1、我的pom.xml相关依赖版本代码

    1. <?xml version="1.0" encoding="UTF-8"?>
    2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    4. <modelVersion>4.0.0</modelVersion>
    5. <parent>
    6. <groupId>org.springframework.boot</groupId>
    7. <artifactId>spring-boot-starter-parent</artifactId>
    8. <version>2.2.4.RELEASE</version>
    9. <relativePath/> <!-- lookup parent from repository -->
    10. </parent>
    11. <groupId>com.jinzheyi</groupId>
    12. <artifactId>jwt</artifactId>
    13. <version>0.0.1-SNAPSHOT</version>
    14. <packaging>war</packaging>
    15. <name>jwt</name>
    16. <description>Spring Boot集成jwt</description>
    17. <properties>
    18. <java.version>1.8</java.version>
    19. </properties>
    20. <dependencies>
    21. <!--引入jwt依赖-->
    22. <dependency>
    23. <groupId>io.jsonwebtoken</groupId>
    24. <artifactId>jjwt</artifactId>
    25. <version>0.9.1</version>
    26. </dependency>
    27. <dependency>
    28. <groupId>com.alibaba</groupId>
    29. <artifactId>fastjson</artifactId>
    30. <version>1.2.60</version>
    31. </dependency>
    32. <dependency>
    33. <groupId>org.apache.commons</groupId>
    34. <artifactId>commons-lang3</artifactId>
    35. <version>3.9</version>
    36. </dependency>
    37. <dependency>
    38. <groupId>org.springframework.boot</groupId>
    39. <artifactId>spring-boot-starter-web</artifactId>
    40. </dependency>
    41. <dependency>
    42. <groupId>org.projectlombok</groupId>
    43. <artifactId>lombok</artifactId>
    44. <optional>true</optional>
    45. </dependency>
    46. <dependency>
    47. <groupId>org.springframework.boot</groupId>
    48. <artifactId>spring-boot-starter-tomcat</artifactId>
    49. <scope>provided</scope>
    50. </dependency>
    51. <dependency>
    52. <groupId>org.springframework.boot</groupId>
    53. <artifactId>spring-boot-starter-test</artifactId>
    54. <scope>test</scope>
    55. <exclusions>
    56. <exclusion>
    57. <groupId>org.junit.vintage</groupId>
    58. <artifactId>junit-vintage-engine</artifactId>
    59. </exclusion>
    60. </exclusions>
    61. </dependency>
    62. </dependencies>
    63. <build>
    64. <plugins>
    65. <plugin>
    66. <groupId>org.springframework.boot</groupId>
    67. <artifactId>spring-boot-maven-plugin</artifactId>
    68. </plugin>
    69. </plugins>
    70. </build>
    71. </project>

    2、在工程 application.yml 配置文件中添加JWT的配置信息:

    1. ##jwt配置
    2. audience:
    3. #代表这个jwt的接收对象,存入audience
    4. clientId: 098f6bcd4621d373cade4e832627b4f6
    5. #密钥,经过Base64加密,可自行替换
    6. base64Secret: MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
    7. # JWT的签发主体,存入issuer
    8. name: restapiuser
    9. # 过期时间,时间戳
    10. expiresSecond: 172800
    11. server:
    12. port: 8081

    3、新建配置信息的实体类,以便获取JWT配置:

    1. package com.jinzheyi.jwt.entity;
    2. import lombok.Data;
    3. import org.springframework.boot.context.properties.ConfigurationProperties;
    4. import org.springframework.stereotype.Component;
    5. /**
    6. *@Description 创建的关于yml文件中jwt配置属性的实体类
    7. *@Author jinzheyi
    8. *@Date 2020/2/15 22:49
    9. *@version 0.1
    10. */
    11. @Data
    12. @ConfigurationProperties(prefix = "audience")
    13. @Component
    14. public class Audience {
    15. //客户端id
    16. private String clientId;
    17. //经过base64加密后密钥
    18. private String base64Secret;
    19. //签发主题
    20. private String name;
    21. //过期时间
    22. private int expiresSecond;
    23. }

    JWT验证主要是通过过滤器验证,所以我们需要添加一个拦截器来演请求头中是否包含有后台颁发的 token


    4、创建JWT验证拦截器(进行了一层封装):

    1. package com.jinzheyi.jwt.config;
    2. import com.jinzheyi.jwt.config.interceptor.JwtInterceptor;
    3. import org.springframework.context.annotation.Configuration;
    4. import org.springframework.web.servlet.config.annotation.CorsRegistry;
    5. import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    6. import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    7. /**
    8. *@Description 全局配置公共类,含拦截器、允许跨域请求处理
    9. *@Author jinzheyi
    10. *@Date 2020/2/16 14:01
    11. *@version 0.1
    12. */
    13. @Configuration
    14. public class WebConfig implements WebMvcConfigurer {
    15. /**
    16. * 添加拦截器
    17. * @param interceptorRegistry
    18. */
    19. @Override
    20. public void addInterceptors(InterceptorRegistry interceptorRegistry){
    21. //拦截路径可自行配置多个 可用,分隔开
    22. interceptorRegistry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
    23. }
    24. /**
    25. * 跨域支持
    26. * @param corsRegistry
    27. */
    28. @Override
    29. public void addCorsMappings(CorsRegistry corsRegistry){
    30. corsRegistry.addMapping("/**")
    31. .allowedOrigins("*")
    32. .allowCredentials(true)
    33. .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
    34. .maxAge(3600 * 24);
    35. }
    36. }
    1. package com.jinzheyi.jwt.config.interceptor;
    2. import com.jinzheyi.jwt.common.Constant;
    3. import com.jinzheyi.jwt.common.annotation.JwtIgnore;
    4. import com.jinzheyi.jwt.common.exception.CustomException;
    5. import com.jinzheyi.jwt.common.response.ResultCode;
    6. import com.jinzheyi.jwt.entity.Audience;
    7. import com.jinzheyi.jwt.utils.JwtUtil;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.apache.commons.lang3.StringUtils;
    10. import org.springframework.beans.factory.BeanFactory;
    11. import org.springframework.beans.factory.annotation.Autowired;
    12. import org.springframework.http.HttpMethod;
    13. import org.springframework.web.context.support.WebApplicationContextUtils;
    14. import org.springframework.web.method.HandlerMethod;
    15. import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    16. import javax.servlet.http.HttpServletRequest;
    17. import javax.servlet.http.HttpServletResponse;
    18. /**
    19. *@Description 封装好的jwt拦截器处理类
    20. *@Author jinzheyi
    21. *@Date 2020/2/16 14:00
    22. *@version 0.1
    23. */
    24. @Slf4j
    25. public class JwtInterceptor extends HandlerInterceptorAdapter {
    26. @Autowired
    27. private Audience audience;
    28. @Override
    29. public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception{
    30. //忽略带JwtIgnore注解的请求,不做后续token认证校验
    31. if (handler instanceof HandlerMethod){
    32. HandlerMethod handlerMethod = (HandlerMethod) handler;
    33. JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
    34. if (jwtIgnore != null) {
    35. return true;
    36. }
    37. }
    38. if (HttpMethod.OPTIONS.equals(request.getMethod())) {
    39. response.setStatus(HttpServletResponse.SC_OK);
    40. return true;
    41. }
    42. //获取请求头信息authorization信息
    43. final String authHeader = request.getHeader(Constant.AUTH_HEADER_KEY);
    44. log.info("##authHeader = {}",authHeader);
    45. if (StringUtils.isBlank(authHeader)||!authHeader.startsWith(Constant.TOKEN_PREFIX)) {
    46. log.info("###用户未登录,请先登录###");
    47. throw new CustomException(ResultCode.USER_NOT_LOGGED_IN);
    48. }
    49. //获取token
    50. final String token = authHeader.substring(7);
    51. if (audience == null) {
    52. BeanFactory beanFactory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
    53. audience = (Audience) beanFactory.getBean("audience");
    54. }
    55. JwtUtil.parseJwt(token, audience.getBase64Secret());
    56. return true;
    57. }
    58. }

    5、然后我们创建JWT工具类:

    1. package com.jinzheyi.jwt.utils;
    2. import com.jinzheyi.jwt.common.exception.CustomException;
    3. import com.jinzheyi.jwt.common.response.ResultCode;
    4. import com.jinzheyi.jwt.entity.Audience;
    5. import io.jsonwebtoken.*;
    6. import org.slf4j.Logger;
    7. import org.slf4j.LoggerFactory;
    8. import javax.crypto.spec.SecretKeySpec;
    9. import javax.xml.bind.DatatypeConverter;
    10. import java.security.Key;
    11. import java.util.Date;
    12. /**
    13. *@Description jwt工具类
    14. *@Author jinzheyi
    15. *@Date 2020/2/16 14:03
    16. *@version 0.1
    17. */
    18. public class JwtUtil {
    19. //创建日志对象
    20. private static Logger logger = LoggerFactory.getLogger(JwtUtil.class);
    21. /**
    22. * 解析 jwt
    23. * @param jsonWebToken token
    24. * @param base64Security
    25. * @return
    26. */
    27. public static Claims parseJwt(String jsonWebToken, String base64Security){
    28. try {
    29. Claims claims = Jwts.parser()
    30. .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
    31. .parseClaimsJws(jsonWebToken).getBody();
    32. return claims;
    33. } catch (ExpiredJwtException e){
    34. logger.error("===token过期===");
    35. throw new CustomException(ResultCode.PERMISSION_TOKEN_EXPIRED_EXCEPTION);
    36. } catch (MalformedJwtException e){
    37. logger.error("====json web token格式错误====");
    38. throw new CustomException(ResultCode.PERMISSION_TOKEN_MALFORMEDJWT_EXCEPTION);
    39. }catch (Exception e){
    40. logger.error("=====token解析异常=====");
    41. throw new CustomException(ResultCode.PERMISSION_TOKEN_INVALID);
    42. }
    43. }
    44. /**
    45. * 构建jwt
    46. * @param userId 用户id
    47. * @param username jwt的所有者
    48. * @param role 权限
    49. * @param audience 配置信息对象
    50. * @return
    51. */
    52. public static String createJwt(String userId, String username, String role, Audience audience){
    53. try {//使用HS256加密算法
    54. SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
    55. Long nowMillis = System.currentTimeMillis();
    56. Date now = new Date(nowMillis);
    57. //生成签名
    58. byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
    59. Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());
    60. //userId是重要的信息,进行简要加密下
    61. String encryId = Base64Util.encode(userId);
    62. //添加构成JWT的参数
    63. JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("type", "JWT")
    64. //可以将基本不重要的对象信息放到claims
    65. .claim("role", role)
    66. .claim("userId", encryId)
    67. .setSubject(username) //代表这个jwt的主体,即它的所有人
    68. .setIssuer(audience.getClientId()) //代表这个jwt的签发主体
    69. .setIssuedAt(new Date()) //一个时间戳,代表签发时间
    70. .setAudience(audience.getName()) //代表jwt的接收对象
    71. .signWith(signatureAlgorithm, signingKey);
    72. //添加token过期时间
    73. int expiresMillis = audience.getExpiresSecond();
    74. if (expiresMillis >= 0) {
    75. long expMillis = nowMillis + expiresMillis;
    76. Date expireTime = new Date(expMillis);
    77. jwtBuilder.setExpiration(expireTime) //是一个时间戳,代表jwt的过期时间
    78. .setNotBefore(now); //是一个时间戳,代表这个jwt生效的开始时间,意味在这个时间之前验证jwt都是无效的
    79. }
    80. return jwtBuilder.compact();
    81. } catch (Exception e){
    82. logger.error("生成签名失败");
    83. throw new CustomException(ResultCode.PERMISSION_SIGNATURE_ERROR);
    84. }
    85. }
    86. /**
    87. * 获取用户
    88. * @param token
    89. * @param base64Security
    90. * @return
    91. */
    92. public static String getUsername(String token, String base64Security){
    93. return parseJwt(token, base64Security).getSubject();
    94. }
    95. /**
    96. * 获取用户id
    97. * @param token
    98. * @param base64Security
    99. * @return
    100. */
    101. public static String getUserId(String token, String base64Security){
    102. return Base64Util.decode(parseJwt(token, base64Security).get("userId",String.class));
    103. }
    104. /**
    105. * token是否过期
    106. * @param token
    107. * @param base64Security
    108. * @return
    109. */
    110. public static boolean isExpiration(String token, String base64Security){
    111. return parseJwt(token, base64Security).getExpiration().before(new Date());
    112. }
    113. }

    其中创建jwt工具类的时候,内部使用到的一些封装方法,可以到我的gitee分享的代码里面进行查看。
    6、添加全局异常处理

    1. package com.jinzheyi.jwt.common.exception;
    2. import com.jinzheyi.jwt.common.response.ResultCode;
    3. import com.jinzheyi.jwt.common.response.ResultData;
    4. import org.slf4j.Logger;
    5. import org.slf4j.LoggerFactory;
    6. import org.springframework.validation.BindException;
    7. import org.springframework.validation.BindingResult;
    8. import org.springframework.validation.FieldError;
    9. import org.springframework.validation.ObjectError;
    10. import org.springframework.web.bind.MethodArgumentNotValidException;
    11. import org.springframework.web.bind.annotation.ExceptionHandler;
    12. import org.springframework.web.bind.annotation.RestControllerAdvice;
    13. import java.util.List;
    14. /**
    15. *@Description 定义全局异常处理类
    16. *@Author jinzheyi
    17. *@Date 2020/2/16 13:58
    18. *@version 0.1
    19. */
    20. @RestControllerAdvice
    21. public class GlobalExceptionHandler {
    22. public static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);
    23. /**
    24. * 处理自定义异常
    25. * @param e
    26. * @return
    27. */
    28. @ExceptionHandler(CustomException.class)
    29. public ResultData handleException(CustomException e){
    30. // 打印异常信息
    31. logger.error("### 异常信息:{} ###", e.getMessage());
    32. return new ResultData(e.getResultCode());
    33. }
    34. /**
    35. * 参数错误异常
    36. * @param e
    37. * @return
    38. */
    39. @ExceptionHandler({MethodArgumentNotValidException.class, BindException.class})
    40. public ResultData handleException(Exception e) {
    41. if (e instanceof MethodArgumentNotValidException) {
    42. MethodArgumentNotValidException validException = (MethodArgumentNotValidException) e;
    43. BindingResult result = validException.getBindingResult();
    44. StringBuffer errorMsg = new StringBuffer();
    45. if (result.hasErrors()) {
    46. List<ObjectError> errors = result.getAllErrors();
    47. errors.forEach(p ->{
    48. FieldError fieldError = (FieldError) p;
    49. errorMsg.append(fieldError.getDefaultMessage()).append(",");
    50. logger.error("### 请求参数错误:{"+fieldError.getObjectName()+"},field{"+fieldError.getField()+ "},errorMessage{"+fieldError.getDefaultMessage()+"}");
    51. });
    52. }
    53. } else if (e instanceof BindException) {
    54. BindException bindException = (BindException)e;
    55. if (bindException.hasErrors()) {
    56. logger.error("### 请求参数错误: {}", bindException.getAllErrors());
    57. }
    58. }
    59. return new ResultData(ResultCode.PARAM_IS_INVALID);
    60. }
    61. /**
    62. * 处理所有不可知的异常
    63. * @param e
    64. * @return
    65. */
    66. @ExceptionHandler(Exception.class)
    67. public ResultData handleOtherException(Exception e){
    68. //打印异常堆栈信息
    69. e.printStackTrace();
    70. // 打印异常信息
    71. logger.error("### 不可知的异常:{} ###", e.getMessage());
    72. return new ResultData(ResultCode.SYSTEM_INNER_ERROR);
    73. }
    74. }

    7、最后添加用户Controller进行测试

    1. package com.jinzheyi.jwt.web.controller;
    2. import com.alibaba.fastjson.JSONObject;
    3. import com.jinzheyi.jwt.common.Constant;
    4. import com.jinzheyi.jwt.common.annotation.JwtIgnore;
    5. import com.jinzheyi.jwt.common.response.ResultData;
    6. import com.jinzheyi.jwt.entity.Audience;
    7. import com.jinzheyi.jwt.utils.JwtUtil;
    8. import lombok.extern.slf4j.Slf4j;
    9. import org.springframework.beans.factory.annotation.Autowired;
    10. import org.springframework.web.bind.annotation.GetMapping;
    11. import org.springframework.web.bind.annotation.PostMapping;
    12. import org.springframework.web.bind.annotation.RestController;
    13. import javax.servlet.http.HttpServletResponse;
    14. import java.util.UUID;
    15. /**
    16. *@Description 用户业务控制层
    17. *@Author jinzheyi
    18. *@Date 2020/2/16 14:03
    19. *@version 0.1
    20. */
    21. @Slf4j
    22. @RestController
    23. public class UserController {
    24. @Autowired
    25. private Audience audience;
    26. @PostMapping("/login")
    27. @JwtIgnore
    28. public ResultData login(HttpServletResponse response, String username, String password){
    29. //这里模拟测试,默认登录成功,返回用户id和角色信息
    30. String userId = UUID.randomUUID().toString();
    31. String role = "admin";
    32. //创建token
    33. String token = JwtUtil.createJwt(userId, username, role, audience);
    34. log.info("### 登录成功, token={} ###", token);
    35. response.setHeader(Constant.AUTH_HEADER_KEY, Constant.TOKEN_PREFIX + token);
    36. //将token响应给客户端
    37. JSONObject result = new JSONObject();
    38. result.put("token", token);
    39. return ResultData.successResultData(result);
    40. }
    41. @GetMapping("/users")
    42. public ResultData userList() {
    43. log.info("### 查询所有用户列表 ###");
    44. return ResultData.successResult();
    45. }
    46. }

    8、接下来我们使用PostMan工具进行测试:
    没有登录时候直接访问:http://localhost:8081/users 接口:

    然后我们去执行登录接口:http://localhost:8081/login?username=zhangsan接口 

     

    此时我们获取到了后台服务器生成的token。将token带入到获取用户的接口中,如图 

     

    注意:这里选择 Bearer Token类型,就把不要在 Token中手动Bearer,postman会自动拼接。
    补充:如果带入错误的token会提示token已失效,或者超时之后带入token,会提示已超时。拦截器里面已经做了判断。登录接口不用验证token是因为我们在controller层login方法加了@JwtIgnore注解。

  • 相关阅读:
    求m的n次方你会吗(优化时间复杂度)
    常用的分布式事务解决方案
    JAVA毕业设计129—基于Java+Springboot+thymeleaf的物业管理系统(源代码+数据库)
    A Philosophy of Software Design读书笔记——分or合
    C++初阶(运算符重载汇总+实例)
    【不三不四的脑洞】一个梦所引发关于排序算法的思考
    C语言竞赛
    C语言练习之消失的数字(两种解法)
    【网页设计】期末大作业html+css (个人生活记录介绍网站)
    10.正则表达式匹配
  • 原文地址:https://blog.csdn.net/lichongxyz/article/details/125409489