SpringBoot从零到全栈商城(二)jwt创建token进行用户校验

1,087 阅读5分钟

介绍

记录SpringBoot从零到实现一整个商城后端的过程。

项目组成及技术栈

  • 接口 SpingBoot + JPA + mySql
  • 后台 vue + vue-element-admin
  • 移动端 uniapp + colorui + vuex + scss

开场白

token是什么?有什么用?这些理论性的知识就不介绍了。
这篇文章主要介绍了:

  • 用户登录验证通过创建token
  • 返回token给前端
  • 前端在每个接口的header中带上自定义参数token
  • 需要用户信息的接口,通过自定义注解解析token,获取用户信息

pom依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.0</version>
</dependency>

application.properties配置

##jwt配置
# 代表这个JWT的接收对象,存入audience
audience.clientId=098f6bcd4621d373cade4e832627b4f6
# 密钥, 经过Base64加密, 可自行替换
audience.base64Secret=MDk4ZjZiY2Q0NjIxZDM3M2NhZGU0ZTgzMjYyN2I0ZjY=
# JWT的签发主体,存入issuer
audience.name=restapiuser
# 过期时间,时间戳 7*24*60*60*1000
audience.expiresSecond=604800000

创建Current类

Current类与token中存储的用户信息对应,后面自定义注解解析token会用到

package com.smxy.mall.model;

import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.Date;

@Data
public class Current {
    @ApiModelProperty(hidden = true)
    private Integer userId; //用户id
    @ApiModelProperty(hidden = true)
    private String userName; //用户名
    @ApiModelProperty(hidden = true)
    private String phone; //手机
    @ApiModelProperty(hidden = true)
    private String type;
    @ApiModelProperty(hidden = true)
    private String openId;  //微信 openId
    @ApiModelProperty(hidden = true)
    private String issuer;
    @ApiModelProperty(hidden = true)
    private Date issuedAt;
    @ApiModelProperty(hidden = true)
    private String audience;
}

JwtTokenUtil工具类

package com.smxy.mall.utils;

import com.smxy.mall.common.CustomException;
import com.smxy.mall.common.Response;
import com.smxy.mall.entity.User;
import com.smxy.mall.model.Audience;
import com.smxy.mall.model.Current;
import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.security.Key;
import java.util.Date;

/**
 * jwt 工具类
 */
public class JwtTokenUtil {

    //日志
    private static Logger log = LoggerFactory.getLogger(JwtTokenUtil.class);

    public static final String AUTH_HEADER_KEY = "token";
    

    /**
     * 解析jwt
     * @param jsonWebToken
     * @param base64Security
     * @return
     */
    public static Claims parseJWT(String jsonWebToken, String base64Security) {
        try {
            Claims claims = Jwts.parser()
                    .setSigningKey(DatatypeConverter.parseBase64Binary(base64Security))
                    .parseClaimsJws(jsonWebToken).getBody();
            return claims;
        } catch (ExpiredJwtException  eje) {
            log.error("===== Token过期 =====", eje);
            throw new CustomException(Response.fail("401","Token过期"));
        } catch (Exception e){
            log.error("===== token解析异常 =====", e);
            throw new CustomException(Response.fail("401","token解析异常"));
        }
    }

    /**
     * 构建jwt
     * @param user
     * @param audience jwt配置
     * @return
     */
    public static String createJWT(User user, Audience audience) {
        try {
            // 使用HS256加密算法
            SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

            long nowMillis = System.currentTimeMillis();
            Date now = new Date(nowMillis);

            //生成签名密钥
            byte[] apiKeySecretBytes = DatatypeConverter.parseBase64Binary(audience.getBase64Secret());
            Key signingKey = new SecretKeySpec(apiKeySecretBytes, signatureAlgorithm.getJcaName());

            //userId是重要信息,进行加密下
//            String encryId = Base64Util.encode(userId);

            //添加构成JWT的参数
            JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                    // 可以将基本不重要的对象信息放到claims
                    .claim("type", user.getType())
                    .claim("userId", user.getId())
                    .claim("phone",user.getPhone())
                    .claim("userName",user.getUserName())
                    .claim("openId",user.getOpenId())
                    .setSubject(String.valueOf(user.getId()))           // 代表这个JWT的主体,即它的所有人
                    .setIssuer(audience.getClientId())              // 代表这个JWT的签发主体;
                    .setIssuedAt(new Date())        // 是一个时间戳,代表这个JWT的签发时间;
                    .setAudience(audience.getName())          // 代表这个JWT的接收对象;
                    .signWith(signatureAlgorithm, signingKey);
            //添加Token过期时间
            int TTLMillis = audience.getExpiresSecond();
            if (TTLMillis >= 0) {
                long expMillis = nowMillis + TTLMillis;
                Date exp = new Date(expMillis);
                builder.setExpiration(exp)  // 是一个时间戳,代表这个JWT的过期时间;
                        .setNotBefore(now); // 是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的
            }

            //生成JWT
            return builder.compact();
        } catch (Exception e) {
            log.error("签名失败", e);
            throw new CustomException(Response.fail("401","签名失败"));
        }
    }

    /**
     * 从token中获取uerId (Subject)
     * @param token
     * @param base64Security
     * @return
     */
    public static Integer getUserId(String token, String base64Security){
        return Integer.parseInt(parseJWT(token, base64Security).getSubject());
    }

    /**
     * 解析token,获取token中的用户信息
     * @param token
     * @param base64Security
     * @return
     */
    public static Current getCurrentUser(String token, String base64Security){
        Claims claims = parseJWT(token, base64Security);
        Current currentCurrent = new Current();
        currentCurrent.setUserId(Integer.parseInt(claims.getSubject()));
        currentCurrent.setUserName(claims.get("userName",String.class));
        currentCurrent.setPhone(claims.get("phone",String.class));
        currentCurrent.setType(claims.get("type",String.class));
        currentCurrent.setIssuer(claims.getIssuer());
        currentCurrent.setAudience(claims.getAudience());
        currentCurrent.setIssuedAt(claims.getIssuedAt());
        return currentCurrent;
    }

    /**
     * 是否已过期
     * @param token
     * @param base64Security
     * @return
     */
    public static boolean isExpiration(String token, String base64Security) {
        return parseJWT(token, base64Security).getExpiration().before(new Date());
    }
}

JwtInterceptor token拦截器

拦截token,解析token,将token存入session中

package com.smxy.mall.config;

import com.smxy.mall.annotation.JwtIgnore;
import com.smxy.mall.common.CustomException;
import com.smxy.mall.common.Response;
import com.smxy.mall.model.Audience;
import com.smxy.mall.model.Current;
import com.smxy.mall.utils.JwtTokenUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.util.StringUtils;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

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

/**
 * token拦截器
 */
@Slf4j
public class JwtInterceptor extends HandlerInterceptorAdapter{

    @Autowired
    private Audience audience;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");

        // 忽略带JwtIgnore注解的请求, 不做后续token认证校验
        if (handler instanceof HandlerMethod) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            JwtIgnore jwtIgnore = handlerMethod.getMethodAnnotation(JwtIgnore.class);
            if (jwtIgnore != null) {
                return true;
            }
        }

        if (HttpMethod.OPTIONS.equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            return true;
        }

        // 获取请求头信息authorization信息
        final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
        log.info("## authHeader= {}", authHeader);

        if (StringUtils.isEmpty(authHeader)) {
            log.info("### 用户未登录,请先登录 ###");
            response.setStatus(401);
            throw new CustomException(Response.fail("401","用户未登录,请先登录"));
        }

        // 获取token
       // final String token = authHeader.substring(7);

        if(audience == null){
            BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
            audience = (Audience) factory.getBean("audience");
        }

//        // 验证token是否有效--无效已做异常抛出,由全局异常处理后返回对应信息
//        Claims claims = JwtTokenUtil.parseJWT(authHeader, audience.getBase64Secret());
//        System.out.println(claims);
        /**
         * 获取token中的用户信息
         */
        Current currentCurrent = JwtTokenUtil.getCurrentUser(authHeader,audience.getBase64Secret());
        System.out.println("----------------JwtInterceptor currentUser-----------------");
        System.out.println(currentCurrent);
        if(!StringUtils.isEmpty(currentCurrent.getUserId())){
            request.getSession().setAttribute("currentUser", currentCurrent);
            return true;
        }else{
            response.setStatus(401);
            throw new CustomException("401","登录失效");
        }
    }

}

自定义注解

  • JwtIgnore JWT验证忽略注解

  • CurrentUser 存储当前token对应的用户信息 在annotation中新建CurrentUser

package com.smxy.mall.annotation;

import java.lang.annotation.*;

@Target({ElementType.PARAMETER})//Annotation所修饰的对象范围:方法参数
@Retention(RetentionPolicy.RUNTIME)//Annotation被保留时间:运行时保留(有效)
@Documented
public @interface CurrentUser {
}
  • CurrentUser注解实现 取出session中的用户信息,并存入Current类中
package com.smxy.mall.annotation.impl;


import com.smxy.mall.annotation.CurrentUser;
import com.smxy.mall.model.Current;
import org.springframework.core.MethodParameter;
import org.springframework.web.bind.support.WebDataBinderFactory;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.method.support.ModelAndViewContainer;

import javax.servlet.http.HttpServletRequest;

public class CurrentUserHandlerMethodArgReslover implements HandlerMethodArgumentResolver {

    /**
     * 判断是否支持使用@CurrentUser注解的参数
     */
    @Override
    public boolean supportsParameter(MethodParameter methodParameter) {
        //如果该参数注解有@CurrentUser且参数类型是Current
        return methodParameter.getParameterAnnotation(CurrentUser.class) != null &&methodParameter.getParameterType() == Current.class;
    }

    /**
     * 注入参数值
     */
    @Override
    public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
        //取得HttpServletRequest
        HttpServletRequest request= (HttpServletRequest) nativeWebRequest.getNativeRequest();
        //取出session中的User
        return (Current)request.getSession().getAttribute("currentUser");
    }
}

WebConfig 配置

通过通配符配置拦截哪些controller

package com.smxy.mall.config;

import com.smxy.mall.annotation.impl.CurrentUserHandlerMethodArgReslover;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
public class WebConfig implements WebMvcConfigurer {
    /**
     * 所有的WebMvcConfigurerAdapter组件都会一起起作用
     * @return
     */
    @Bean //将组件注册在容器中
    public WebMvcConfigurer webMvcConfigurerAdapter(){
        return new WebMvcConfigurer(){

            //注册拦截器
            @Override
            public void addInterceptors(InterceptorRegistry registry) {
                // /**  表示拦截所有路径下的所有请求
                registry.addInterceptor(new JwtInterceptor())
                        .addPathPatterns("/user/**")
                        .addPathPatterns("/address/**")
                        .addPathPatterns("/car/**")
                        .addPathPatterns("/order/**");
            }
        };
    }

    /**
     * 跨域支持
     * @param registry
     */
    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
                .allowedOrigins("*")
                .allowCredentials(true)
                .allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
                .maxAge(3600 * 24);
    }

    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers){
        //注册@CurrentUser注解的实现类
        argumentResolvers.add(new CurrentUserHandlerMethodArgReslover());
    }

}

在Controller使用

Current中是token对应的用户信息

说明

jwt工具类中使用了自定义异常,可先注释掉,自定义异常和常用的审计功能将在下一章节更新