SpringBoot、uniapp项目整合token验证
由于本小项目前端uniapp使用token进行验证,所以在写后端的时候我也学习了相关token的使用方法。
什么是token?
简单理解就是token是在服务端生成的一个字符串,传给客户端作为请求携带的令牌。当用户第一次登录的时候,服务器就会生成一个token返回给客户端,之后客户端的请求都会携带这个令牌,服务器解析会拦截这些请求并判断令牌是否有效,如果没有token令牌或者失效就会拦截下来。
前端配置
首先在前端配置好网格拦截器,对于请求都带上token以便后端进行身份验证,前端使用的是uView框架,安装并配置好uView之后,下面就是interceptor拦截器,在mian.js中引入
module.exports = (vm) => {
// 初始化请求配置
uni.$u.http.setConfig((config) => {
/* config 为默认全局配置*/
config.baseURL = 'http://192.168.194.251'; /* 根域名 */
return config
})
// 请求拦截,使请求携带token
uni.$u.http.interceptors.request.use((config) => {
const token = uni.getStorageSync("token");
config.header.Authorization = "Bearer " + token;
config.header.Accept = "application/json";
return config;
})
//响应拦截
uni.$u.http.interceptors.response.use((response) => {
console.log(response)
if (response.statusCode != 200) {
console.log('响应拦截成功!=200')
return response;
} else {
console.log('响应拦截返回')
return response;
}
}, (response) => {
console.log('这是响应错误的返回')
console.log(response)
return response
})
}
//引入拦截器
require('@/common/http.interceptor.js')(app)
后端配置
pom引入
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
application.yml
audience:
clientId: 098f6bcd4621d373cade4e832627b4f6
# 密钥, 经过Base64加密, 可自行替换。Base64加解密工具:http://tool.chinaz.com/Tools/Base64.aspx
base64Secret: eXViYW9aSk5VU0NFTkVSWVYxLjA=
# JWT的签发主体,存入issuer
iss: issued by yubao
# 过期时间毫秒
expiresSecond: 604800000
Audience
新建配置信息的实体类,以便获取JWT配置:
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@ConfigurationProperties(prefix = "audience")
@Component
public class Audience {
//代表这个JWT的接收对象,存入audience
private String aud;
private String base64Secret;
//JWT的签发主体,存入issuer
private String iss;
private int expiresSecond;
}
创建JWT工具类
package com.yubao.zjnu_demo.utils;
import com.yubao.zjnu_demo.common.CustomException;
import com.yubao.zjnu_demo.entity.Audience;
import io.jsonwebtoken.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Base64Utils;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.bind.DatatypeConverter;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
public class JwtTokenUtil {
public static final String AUTH_HEADER_KEY = "Authorization";
public static final String TOKEN_PREFIX = "Bearer ";
/**
* 解析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("PERMISSION_TOKEN_EXPIRED");
} catch (Exception e) {
log.error("===== token解析异常 =====", e);
throw new CustomException("PERMISSION_TOKEN_INVALID");
}
}
/**
* 构建jwt
*
* @param userId
* @param username
* @param audience
* @return
*/
public static String createJWT(String userId, String username, 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());
//添加构成JWT的参数
JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
// 可以将基本不重要的对象信息放到claims
.claim("userId", userId)
.setSubject(username) // 代表这个JWT的主体,即它的所有人
.setIssuer(audience.getIss()) // 代表这个JWT的签发主体;
.setIssuedAt(new Date()) // 是一个时间戳,代表这个JWT的签发时间;
.setAudience(audience.getAud()) // 代表这个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("PERMISSION_SIGNATURE_ERROR");
}
}
/**
* 从token中获取用户名
*
* @param token
* @param base64Security
* @return
*/
public static String getUsername(String token, String base64Security) {
return parseJWT(token, base64Security).getSubject();
}
/**
* 从token中获取用户ID
*
* @param token
* @param base64Security
* @return
*/
public static String getUserId(String token, String base64Security) {
Claims claims= parseJWT(token,base64Security);
String userId = (String)claims.get("userId");
return userId;
}
/**
* 是否已过期
*
* @param token
* @param base64Security
* @return
*/
public static boolean isExpiration(String token, String base64Security) {
return parseJWT(token, base64Security).getExpiration().before(new Date());
}
}
拦截器
拦截前端传来的请求,验证其是否携带令牌以及令牌是否有效
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpMethod;
import org.springframework.web.context.support.WebApplicationContextUtils;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* token验证拦截器
*/
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private Audience audience;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 忽略带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.isBlank(authHeader) || !authHeader.startsWith(JwtTokenUtil.TOKEN_PREFIX)) {
log.info("### 用户未登录,请先登录 ###");
throw new CustomException("USER_NOT_LOGGED_IN");
}
// 获取token
final String token = authHeader.substring(7);
if (audience == null) {
BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
audience = (Audience) factory.getBean("audience");
}
// 验证token是否有效--无效已做异常抛出,由全局异常处理后返回对应信息
JwtTokenUtil.parseJWT(token, audience.getBase64Secret());
return true;
}
}
编写JwtIgnore注解
忽略携带JwtIgnore的请求,不会做后续的token验证
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface JwtIgnore {
boolean required() default true;
}
配置拦截器
在配置类中配置拦截器
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截路径可自行配置多个 可用 ,分隔开
registry.addInterceptor(new JwtInterceptor()).addPathPatterns("/**");
}
/**
* 跨域支持
*
* @param registry
*/
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowCredentials(true)
.allowedMethods("GET", "POST", "DELETE", "PUT", "PATCH", "OPTIONS", "HEAD")
.maxAge(3600 * 24);
}
用户登录接口
//用户登录
@PostMapping("/login")
@JwtIgnore
public R<JSONObject> loginUser(User user, HttpServletResponse response){
LambdaQueryWrapper<User> queryWrapper =new LambdaQueryWrapper<>();
User loginUser=null;
if(user.getName()!=null){
String password= DigestUtils.md5Hex(user.getPassword());
user.setPassword(password);
queryWrapper.eq(User::getName,user.getName());
queryWrapper.eq(User::getPassword,user.getPassword());
loginUser=userService.getOne(queryWrapper);
}else{
Object codeInSession=stringRedisTemplate.opsForValue().get(user.getPhone());
if(codeInSession!=null&&codeInSession.equals(user.getVerifiableCode())) {
queryWrapper.eq(User::getPhone, user.getPhone());
loginUser = userService.getOne(queryWrapper);
}else{
return R.error("登陆失败");
}
}
if(loginUser==null){
return R.error("登录失败");
}
// 创建token
String token = JwtTokenUtil.createJWT(loginUser.getId().toString(), loginUser.getName(), audience);
log.info("### 登录成功, token={} ###", token);
// 将token放在响应头
response.setHeader(JwtTokenUtil.AUTH_HEADER_KEY, JwtTokenUtil.TOKEN_PREFIX + token);
// 将token响应给客户端
JSONObject result = new JSONObject();
result.put("token", token);
return R.success(result);
}
这样我们就将这个token传递给了前端,前端将token存起来,每次请求的时候都会在拦截器中携带
uni.setStorageSync('token', token)
对于携带token请求的处理
在许多接口中我们都需要获取当前用户的id,姓名等等,但是请求中都不会携带这些信息,我们只能从token中解析,怎么样优雅的在需要的接口中解析token信息呢?
我们使用方法参数解析器,参考这篇文章
Spring提供了多种解析器Resolver,比如常用的统一处理异常的HandlerExceptionResolver。同时,还提供了用来处理方法参数的解析器HandlerMethodArgumentResolver。它包含2个方法:supportsParameter和resolveArgument。其中前者用来判断是否满足某个条件,当满足条件(返回true)则可进入resolveArgument方法进行具体处理操作。
基于HandlerExceptionResolver,我们可以分以下部分来进行实现:
- 自定义注解@CurrentUser,用于Controller方法上的User参数;
- 自定义LoginUserHandlerMethodArgumentResolver,实现HandlerMethodArgumentResolver接口,通过supportsParameter检查符合条件的参数,通过resolveArgument方法来将token转换成User对象,并赋值给参数。
- 注册HandlerMethodArgumentResolver到MVC当中。
下面来看具体的实现,先定义注解@CurrentUser:
@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
注解就是用来做标识用的,标识指定的参数需要进行处理。对于注解了@CurrentUser的参数是由自定义的LoginUserHandlerMethodArgumentResolver来进行判断处理的:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
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;
@Component
public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
@Autowired
private Audience audience;
@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class) &&
parameter.getParameterType().isAssignableFrom(User.class);
}
@Override
public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
NativeWebRequest request, WebDataBinderFactory factory) {
final String authHeader = request.getHeader(JwtTokenUtil.AUTH_HEADER_KEY);
// 获取token
final String token = authHeader.substring(7);
Long userId = Long.valueOf(JwtTokenUtil.getUserId(token,audience.getBase64Secret()));
String name=JwtTokenUtil.getUsername(token,audience.getBase64Secret());
// TODO 根据userId获取User信息,这里省略,直接创建一个User对象。
User user = new User();
user.setName(name);
user.setId(userId);
return user;
}
}
supportsParameter方法中通过两个条件来过滤参数,首先参数需要使用CurrentUser注解,同时参数的类型为User。当满足条件时返回true,进入resolveArgument进行处理。
在resolveArgument中,从header中获取token,然后根据token获取对应User信息,这里可以注入UserService来获得更多的用户信息,然后将构造好的User对象返回。这样,后续就可以将返回的User绑定到Controller中的参数上。
但此时自定义的Resolver并没有生效,还需要添加到MVC当中:
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver;
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
argumentResolvers.add(loginUserHandlerMethodArgumentResolver);
}
}
至此,便可以在Controller中使用该注解来获取用户信息了,具体使用如下:
public R<FeedDto> getById(@PathVariable Long id,@CurrentUser User currentUser){
在参数前添加注解CurrentUser,这个user参数就是当前登录的用户了。