参考资料

官网: https://jwt.io/

SpringBoot集成Jwt(详细步骤+图解)

jwt解析token报错:Signed Claims JWSs are not supported.

Spring拦截器的实现以及通过注解实现拦截

一个简单的 JWT 认证

代码地址

https://github.com/laolang2016/jwt-study/tree/master/jwt-01

关键代码图解

Spring Boot 中 JWT 的基本使用_spring


登录接口与登录服务

AuthController

package com.laolang.shop.modules.auth.controller;

import com.laolang.shop.common.domain.R;
import com.laolang.shop.modules.auth.domain.LoginUser;
import com.laolang.shop.modules.auth.logic.AuthLogic;
import com.laolang.shop.modules.auth.rsp.LoginRsp;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequiredArgsConstructor
@RequestMapping("auth")
@RestController
public class AuthController {
    private final AuthLogic authLogic;

    /**
     * 登录接口
     */
    @PostMapping("login")
    public R<LoginRsp> login(@RequestBody LoginUser loginUser) {
        return R.ok(authLogic.login(loginUser));
    }
}

AuthLogic

package com.laolang.shop.modules.auth.logic;

import cn.hutool.core.util.StrUtil;
import com.google.common.collect.Sets;
import com.laolang.shop.common.exception.BusinessException;
import com.laolang.shop.modules.auth.domain.AuthUser;
import com.laolang.shop.modules.auth.domain.LoginUser;
import com.laolang.shop.modules.auth.rsp.LoginRsp;
import com.laolang.shop.modules.auth.service.TokenService;
import java.util.Set;
import javax.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Slf4j
@RequiredArgsConstructor
@Service
public class AuthLogic {

    private final TokenService tokenService;

    private Set<String> usernames;

    @PostConstruct
    public void init() {
        usernames = Sets.newHashSet();
        usernames.add("admin");
        usernames.add("laolang");
    }

    public LoginRsp login(LoginUser loginUser) {

        if (!usernames.contains(loginUser.getUsername())) {
            log.info("用户不存在");
            throw new BusinessException("用户名或密码错误");
        }
        if (!StrUtil.equals("123456", loginUser.getPassword())) {
            log.info("密码错误");
            throw new BusinessException("用户名或密码错误");
        }
        AuthUser authUser = new AuthUser();
        authUser.setUsername(loginUser.getUsername());

        // 生成 token
        String token = tokenService.buildToken(authUser);

        log.info("token:{}", token);
        LoginRsp rsp = new LoginRsp();
        rsp.setToken(token);
        return rsp;
    }
}

TokenService

package com.laolang.shop.modules.auth.service;

import cn.hutool.core.util.IdUtil;
import com.google.common.collect.Maps;
import com.laolang.shop.common.consts.GlobalConst;
import com.laolang.shop.modules.auth.consts.logic.AuthBizCode;
import com.laolang.shop.modules.auth.domain.AuthUser;
import com.laolang.shop.modules.auth.exception.AuthBusinessException;
import com.laolang.shop.modules.auth.properties.TokenProperties;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

/**
 * token 工具服务
 */
@RequiredArgsConstructor
@Service
public class TokenService {

    private final TokenProperties tokenProperties;

    /**
     * 生成 token
     */
    public String buildToken(AuthUser authUser) {
        authUser.setUuid(IdUtil.fastSimpleUUID());
        Map<String, Object> claims = Maps.newHashMap();
        Date iat = new Date();
        Date exp = new Date(iat.getTime() + tokenProperties.getExpireTime() * 60 * 1000);
        claims.put(GlobalConst.LOGIN_USER_KEY, authUser.getUuid());
        claims.put("username", authUser.getUsername());
        return Jwts.builder()
                .setIssuedAt(iat) // 签发时间
                .setExpiration(exp) // 过期时间
                .setClaims(claims) // 自定义数据
                .signWith(SignatureAlgorithm.HS512, tokenProperties.getSecret()).compact();
    }

    /**
     * 校验 token
     */
    public AuthUser verify(String token) {
        try {
            Claims claims = Jwts.parser().setSigningKey(tokenProperties.getSecret()).parseClaimsJws(token).getBody();
            AuthUser authUser = new AuthUser();
            authUser.setUsername(claims.get("username", String.class));
            authUser.setUuid(claims.get(GlobalConst.LOGIN_USER_KEY, String.class));
            return authUser;
        } catch (ExpiredJwtException e) {
            throw new AuthBusinessException(AuthBizCode.login_expired);
        } catch (Exception e) {
            throw new AuthBusinessException(AuthBizCode.error_token);
        }
    }
}

拦截器

package com.laolang.shop.config.web.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.laolang.shop.common.consts.GlobalConst;
import com.laolang.shop.common.domain.R;
import com.laolang.shop.common.util.ServletKit;
import com.laolang.shop.modules.auth.consts.logic.AuthBizCode;
import com.laolang.shop.modules.auth.consts.logic.AuthConsts;
import com.laolang.shop.modules.auth.domain.AuthUser;
import com.laolang.shop.modules.auth.exception.AuthBusinessException;
import com.laolang.shop.modules.auth.properties.TokenProperties;
import com.laolang.shop.modules.auth.service.TokenService;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * jwt 拦截器
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final TokenService tokenService;
    private final TokenProperties tokenProperties;

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
                             @NonNull Object handler) throws Exception {
        try {
            String token = getToken(request);
            if (StrUtil.isBlank(token)) {
                log.warn("token 不存在");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.doOverdue()));
                return false;
            }
            AuthUser authUser = tokenService.verify(token);
            // 填充 authUser 信息
            if (StrUtil.equals(authUser.getUsername(), "admin")) {
                authUser.setId(1L);
            } else {
                authUser.setId(2L);
            }
            // TODO 一些其他操作, 比如刷新 token

            // 放入到 request 中, 供后续使用
            request.setAttribute(AuthConsts.AUTH_USER_ATTR_NAME, authUser);
        } catch (AuthBusinessException e) {
            log.warn("token 解析异常");
            if (StrUtil.equals(AuthBizCode.login_expired.getCode(), e.getCode())) {
                log.warn("token 过期");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.doOverdue()));
            } else {
                log.warn("非法的 token");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.failed("身份验证异常")));
            }
            return false;
        } catch (Exception e) {
            log.error("auth 拦截器错误:{}", ExceptionUtils.getMessage(e));
            ServletKit.writeJson(response, JSONUtil.toJsonStr(R.error("服务器内部错误")));
            return false;
        }
        return true;
    }

    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(tokenProperties.getHeader());
        if (StrUtil.isNotEmpty(token) && token.startsWith(GlobalConst.TOKEN_PREFIX)) {
            token = token.replace(GlobalConst.TOKEN_PREFIX, "");
        }
        return token;
    }
}

效果

vscode .http 文件

@baseurl = http://localhost:8091

# @name login
POST {{baseurl}}/auth/login HTTP/1.1
Content-Type: application/json

{
    "username":"admin",
    "password":"123456"
}

@authToken = {{login.response.body.body.token}}



### 
GET {{baseurl}}/admin/sysdict/info HTTP/1.1
Authorization: {{authToken}}

效果图

登录

Spring Boot 中 JWT 的基本使用_java_02

校验

Spring Boot 中 JWT 的基本使用_java_03


添加一个匿名访问注解

注解

package com.laolang.shop.common.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AnonymousAccess {
}

拦截器

package com.laolang.shop.config.web.interceptor;

import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.laolang.shop.common.annotation.AnonymousAccess;
import com.laolang.shop.common.consts.GlobalConst;
import com.laolang.shop.common.domain.R;
import com.laolang.shop.common.util.ServletKit;
import com.laolang.shop.modules.auth.consts.logic.AuthBizCode;
import com.laolang.shop.modules.auth.consts.logic.AuthConsts;
import com.laolang.shop.modules.auth.domain.AuthUser;
import com.laolang.shop.modules.auth.exception.AuthBusinessException;
import com.laolang.shop.modules.auth.properties.TokenProperties;
import com.laolang.shop.modules.auth.service.TokenService;
import java.util.Objects;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.exception.ExceptionUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;

/**
 * jwt 拦截器
 */
@Slf4j
@RequiredArgsConstructor
@Component
public class AuthInterceptor implements HandlerInterceptor {

    private final TokenService tokenService;
    private final TokenProperties tokenProperties;

    @Override
    public boolean preHandle(@NonNull HttpServletRequest request, @NonNull HttpServletResponse response,
                             @NonNull Object handler) throws Exception {
        try {
            // 有匿名访问注解, 直接跳过
            if( isAnonymousAccess(handler)){
                return true;
            }

            // 开始校验 token
            String token = getToken(request);
            if (StrUtil.isBlank(token)) {
                log.warn("token 不存在");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.doOverdue()));
                return false;
            }
            AuthUser authUser = tokenService.verify(token);
            // 填充 authUser 信息
            if (StrUtil.equals(authUser.getUsername(), "admin")) {
                authUser.setId(1L);
            } else {
                authUser.setId(2L);
            }
            // TODO 一些其他操作, 比如刷新 token

            // 放入到 request 中, 供后续使用
            request.setAttribute(AuthConsts.AUTH_USER_ATTR_NAME, authUser);
        } catch (AuthBusinessException e) {
            log.warn("token 解析异常");
            if (StrUtil.equals(AuthBizCode.login_expired.getCode(), e.getCode())) {
                log.warn("token 过期");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.doOverdue()));
            } else {
                log.warn("非法的 token");
                ServletKit.writeJson(response, JSONUtil.toJsonStr(R.failed("身份验证异常")));
            }
            return false;
        } catch (Exception e) {
            log.error("auth 拦截器错误:{}", ExceptionUtils.getMessage(e));
            ServletKit.writeJson(response, JSONUtil.toJsonStr(R.error("服务器内部错误")));
            return false;
        }
        return true;
    }

    /**
     * 是否为匿名访问接口
     */
    private boolean isAnonymousAccess(Object handler) {
        HandlerMethod handlerMethod = (HandlerMethod) handler;
        AnonymousAccess anonymousAccess = handlerMethod.getMethod().getAnnotation(AnonymousAccess.class);
        return Objects.nonNull(anonymousAccess);
    }

    /**
     * 获取请求头中的 token
     */
    private String getToken(HttpServletRequest request) {
        String token = request.getHeader(tokenProperties.getHeader());
        if (StrUtil.isNotEmpty(token) && token.startsWith(GlobalConst.TOKEN_PREFIX)) {
            token = token.replace(GlobalConst.TOKEN_PREFIX, "");
        }
        return token;
    }
}

使用

package com.laolang.shop.modules.admin.controller;

import com.google.common.collect.Maps;
import com.laolang.shop.common.annotation.AnonymousAccess;
import com.laolang.shop.common.domain.R;
import java.util.Map;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RequestMapping("admin/test")
@RestController
public class SysTestController {

    @AnonymousAccess
    @GetMapping("testAnonymousAccess")
    public R<Map<String, Object>> testAnonymousAccess() {
        log.info("admin sysdict info");
        Map<String, Object> body = Maps.newHashMap();

        body.put("msg","这是一个匿名访问接口");

        return R.ok(body);
    }
}

效果

Spring Boot 中 JWT 的基本使用_json_04