介绍
记录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工具类中使用了自定义异常,可先注释掉,自定义异常和常用的审计功能将在下一章节更新