springboot JWT项目实战demo

本文是一篇实战demo,使用框架为io.jsonwebtoken的jjwt。你会了解到token的生成,解析过程,最后将在项目中体验jwt的使用过程。如果不是很了解jwt,可以参考以下文章补充一下。

什么是 JWT -- JSON WEB TOKEN

一篇文章带你分清楚JWT,JWS与JWE

目录

  • 1、引入所用到的库
  • 2、生成一个token
  • 3、解析Token
  • 4、自定义加密数据
  • 5、实际运用
  • 6、注意事项
  • 7、其他可选项

1、引入所用到的库



    io.jsonwebtoken
    jjwt-api
    0.11.2


    io.jsonwebtoken
    jjwt-impl
    0.11.2
    runtime


    io.jsonwebtoken
    jjwt-jackson
    0.11.2
    runtime

(可以在 https://github.com/jwtk/jjwt找到最新版)

2、生成一个token

使用JwtBuilder可以很方便的帮助我们创建出一个jws,也就是我们平时拿来传的token

// 1、创建私钥,这里的私钥是建议随机生成的,这里只是个例子
String secretString = "12345678901234567890123456789012";
SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));

// 2、创建jwtBuilder
JwtBuilder jwtBuilder = Jwts.builder().setId("自定义id")
	.setSubject("自定义subject")
	.setIssuedAt(new Date())  // 签发时间
	.signWith(key);  // 签名

// 3、获取token
String token = jwtBuilder.compact();

3、解析Token

这里我们可以通过定义的私钥和token来解析出来有用的数据

// 1、解析token,这里的key要和私钥是一个
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();

// 2、获取参数
System.out.println(claims.getId());
System.out.println(claims.getSubject());
System.out.println(claims.getIssuedAt());

4、自定义加密数据

自定义数据的加密和解密和1、2步相同,只是使用了claim(String, Object)来定义数据。使用claims.get(String)来解释数据。

// 加密过程
JwtBuilder jwtBuilder = Jwts.builder().claim("para1","value1").claim("参数2","值2").signWith(key);

// 解析过程
Claims claims = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
claims.get("para1");
claims.get("参数2");

5、实际运用

概述

内容:在知道了jwt是如何生成和解析token的,接下来,作者将会举一个例子,其中包括

  • 用户登录获取token,token中包含用户id,用户昵称,用户权限信息
  • 访问无token拦截(使用拦截器)
  • 请求有token时进行解析,获取
    目录:

springboot JWT项目实战demo_第1张图片

过程简述:用户在登录时获取token。在访问其他链接时检查是否存在token,如果存在,进行解析以便后继使用,如果不存在,不允许访问。

①封装JwtUtils工具类

为便于操作,我们把jwt生成token和解析封装成一个工具类

package com.xxx.demo.util;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;
import java.nio.charset.StandardCharsets;
import java.util.Date;
import java.util.Map;

public class JwtUtils {
    private final String secretString = "12345678901234567890123456789012";
    private final SecretKey key = Keys.hmacShaKeyFor(secretString.getBytes(StandardCharsets.UTF_8));
    public String generateToken(String userId, String userNick, Map other) {
        // 设置有效时间
        long period = 7200000;
        JwtBuilder jwtBuilder = Jwts.builder()
                .setClaims(other) // 使用setClaims可以将Map数据进行加密,必须放在其他变量之前
                .setId(userId)
                .setSubject(userNick)
                .setExpiration(new Date(System.currentTimeMillis() + period)) // 设置有效期
                .signWith(key);
        return jwtBuilder.compact();
    }
    public Claims parseToken(String token){
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
    }
}

②定义并配置拦截器

在这一步主要是为了完成对于无token请求的拦截,你将使用到如下三个类,如果对拦截器还不是很了解,可以看看这篇文章https://blog.csdn.net/weixin_49736959/article/details/107843179。

定义拦截器

// InterceptorConfig.java
package com.xxx.demo.config.interceptor;

import com.alibaba.fastjson.JSONObject;
import com.xxx.demo.util.JwtUtils;
import io.jsonwebtoken.Claims;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.PrintWriter;

@Component
public class TokenInterceptor implements HandlerInterceptor {
    // 自动注入一下
    @Resource
    private JwtUtils jwtUtils;
    // 这个方法是在访问接口之前执行的,我们只需要在这里写验证登陆状态的业务逻辑,就可以在用户调用指定接口之前验证登陆状态了
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 设置返回值属性
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        String token = request.getHeader("token");
        PrintWriter out;
        // 对于注解的判断
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        if(handlerMethod.getMethodAnnotation(NoNeedToken.class)!=null || handlerMethod.getBeanType().isAnnotationPresent(NoNeedToken.class)){
            // 如果自己拥有NoNeedToken标注或者所属的class拥有NoNeedToken 就直接放行
            return true;
        }
        // 在这里写你的判断逻辑 return true是通过拦截器,可以继续访问controller,return false是不通过
        if (token != null) {
            Claims claims = null;
            try{
                claims = jwtUtils.parseToken(token);
            }catch (Exception ignored){
            }
            if(claims != null){
                request.setAttribute("user_claims", claims);
                return true;
            }
        }
        JSONObject res = new JSONObject();
        res.put("state","false");
        res.put("msg","token is null or wrong");
        out = response.getWriter();
        out.append(res.toString());
        return false;
    }
}

将拦截器配置到项目中

// InterceptorConfig.java
package com.xxx.demo.config.interceptor;
import com.xxx.demo.util.JwtUtils;
import org.springframework.context.annotation.Bean;
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;

@Configuration
public class InterceptorConfig  implements WebMvcConfigurer {
    @Resource
    TokenInterceptor tokenInterceptor;
    @Bean
    public JwtUtils jwtUtils(){
        return new JwtUtils();
    }
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 设置所有的路径都要进行拦截,除了/test/login
        registry.addInterceptor(tokenInterceptor).addPathPatterns("/**");
    }
}

定义免token访问的注解

// NoNeedToken.java
package com.xxx.demo.config.interceptor;
import java.lang.annotation.*;


@Target({ElementType.METHOD, ElementType.TYPE}) //注解的范围是类、接口、枚举的方法上
@Retention(RetentionPolicy.RUNTIME)//被虚拟机保存,可用反射机制读取
@Documented
public @interface NoNeedToken {
}

③使用BaseController将解析的内容存储一下

一个请求访问进行访问的顺序是,拦截器 -> @ModelAttribute修饰的方法 -> 业务处理的controller, 所以在这里我们定义BaseController,写入@ModelAttribute 来统一解析token,后继需要进行token解析的controller,都继承这个类。

// BaseController
package com.xxx.demo.controller;

import io.jsonwebtoken.Claims;
import org.springframework.web.bind.annotation.ModelAttribute;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

public class BaseController {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    protected String UserId;  // 用户id
    protected String authority;  // 用户权限

    @ModelAttribute
    public void parseClaims(HttpServletRequest request, HttpServletResponse response){
        this.request = request;
        this.response = response;
        // 获取到在拦截器里设置的 user_claims, 并将其保存到类的成员变量中
        Claims userClaims = (Claims) request.getAttribute("user_claims");
        if(userClaims != null) {
            this.UserId = userClaims.getId();
            this.authority = userClaims.get("authority").toString();
        }
    }
}

④设置用户controller测试

// 
package com.xxx.demo.controller;
import com.xxx.demo.config.interceptor.NoNeedToken;
import com.xxx.demo.model.Users;
import com.xxx.demo.service.UsersService;
import com.xxx.demo.util.JwtUtils;

import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user")
public class UsersController extends BaseController{
    @Resource
    UsersService usersService; // 提供查询功能,根据用户nick查询到用户

    @Resource
    JwtUtils jwtUtils;

	// 用户登录方法
    @NoNeedToken // 用于取消token验证
    @RequestMapping(value = "/login")
    public String login(@RequestBody Map arg){
        String token = "";
        if(arg.get("nick")!= null){
            Users user = usersService.getPassByNick(arg.get("nick"));
            if(user.getPassword()!=null && user.getPassword().equals(arg.get("password"))){
                Map jwtArg = new HashMap<>();
                jwtArg.put("authority",user.getAuthority());
                System.out.println("" + user.getId().toString() +  user.getNick());
                token = jwtUtils.generateToken(user.getId().toString(), user.getNick(), jwtArg);
            }
        }
        return token;
    }
	// 获取用户信息方法
    @RequestMapping(value = "/test")
    public Map test(){
        Map res = new HashMap<>();
        res.put("userID", this.UserId);  // 这里可以用this.UserId是继承了BaseController
        res.put("authority",this.authority);
        return res;
    }
}

⑤设置用户实例类,和usersService

  • user实体类包含userid, nick, password, authority字段,
  • dao层设计有查询功能,可以通过用户nick找到用户
  • UsersService 使用getPassByNick调用dao层查询功能
    由于jpa实现的种类比较多,大家使用自己的方式实现一下就好

⑥测试结果

登录获取token


springboot JWT项目实战demo_第2张图片

直接访问test失败


springboot JWT项目实战demo_第3张图片

使用token进行访问,成功获取用户id和权限


springboot JWT项目实战demo_第4张图片

6、注意事项

setClaims方法

在使用 setClaims方法自定义加密数据时,会覆盖掉之前声明的加密数据,请务必把setClaim写在第一位

7、其他可选项

1、自动生成签名

这个方法也是官方推荐的,用于生成一个足够强的密钥用于JWT hmc-ha算法,请使用密钥. secretkeyfor (signaturealgalgorithm)助手方法

Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);

2、常用错误

在本文中,没有对错误类型进行区分,大家在使用时可以自行选择

  • SignatureException:签名错误异常
  • MalformedJwtException:JWT格式错误异常
  • ExpiredJwtException:JWT过期异常
  • UnsupportedJwtException:不支持的JWT异常

结语:希望对大家有帮助

你可能感兴趣的:(springboot JWT项目实战demo)