在我们的开发项目中,经常需要用到用户ID,比如在小程序商城系统中,我们将商品加入购物车,这时前端就需要发送请求,携带上用户的ID。基本上很多种请求操作都需要携带用户ID,如果每个请求都需要我们往data中添加id的话,那样需要写很多重复代码,并且代码也不美观;所以我们可以利用JWT跟注解的方式来实现;
一、编写token管理器
1.1、导入jwt包
在maven中加入该依赖
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
1.2、创建JwtHelper,用于创建和验证token
package com.maomao.demo.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTCreationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.*;
public class JwtHelper {
//秘钥
public static final String SECRET = "hdriverbird-token";
//签名是由谁生成的
public static final String ISSUSER = "hdriverbird";
//签名的主题
public static final String SUBJECT = "token test";
//观众
public static final String AUDIENCE = "miniapp";
public String createToken(String userId) {
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
Map<String, Object> m = new HashMap<>();
m.put("alg", "HS256");
m.put("typ", "JWT");
//签名时间
Date nowDate = new Date();
//过期时间Date对象
Date expire = getAfterDate(nowDate, 0, 0, 0, 1, 0, 0);
String token = JWT.create().
//设置头部
withHeader(m)
// 设置 载荷 Payload
.withClaim("userId", userId)
.withIssuer(ISSUSER)
.withSubject(SUBJECT)
.withAudience(AUDIENCE)
//签名时间
.withIssuedAt(nowDate)
//过期时间
.withExpiresAt(expire)
.sign(algorithm);
return token;
}catch (JWTCreationException exception){
exception.printStackTrace();
}
return null;
}
/**
* 验证token
* @return
*/
public String verifyToken(String token){
try {
Algorithm algorithm = Algorithm.HMAC256(SECRET);
JWTVerifier build = JWT.require(algorithm)
.withIssuer(ISSUSER)
.build();
DecodedJWT verify = build.verify(token);
//获取声明信息
Map<String, Claim> claims = verify.getClaims();
Claim claim = claims.get("userId");
//转为字符串
return claim.asString();
}catch (JWTCreationException e){
e.printStackTrace();
}
return "";
}
//获取某个时间点的日期对象
public Date getAfterDate(Date date, int year, int month, int day, int hour, int minute, int second) {
if (date == null) {
date = new Date();
}
Calendar cal = new GregorianCalendar();
cal.setTime(date);
if (year != 0) {
cal.add(Calendar.YEAR, year);
}
if (month != 0) {
cal.add(Calendar.MONTH, month);
}
if (day != 0) {
cal.add(Calendar.DATE, day);
}
if (hour != 0) {
cal.add(Calendar.HOUR_OF_DAY, hour);
}
if (minute != 0) {
cal.add(Calendar.MINUTE, minute);
}
if (second != 0) {
cal.add(Calendar.SECOND, second);
}
return cal.getTime();
}
}
1.3、创建token管理工具
package com.maomao.demo.utils;
import org.springframework.stereotype.Component;
/**
* 维护用户token
*/
//加入spring类管理
@Component
public class UserTokenManager {
public static String generateToken(String id) {
JwtHelper jwtHelper = new JwtHelper();
return jwtHelper.createToken(id);
}
public static String getUserId(String token) {
JwtHelper jwtHelper = new JwtHelper();
String userId = jwtHelper.verifyToken(token);
if(userId==null||userId.length()==0){
return null;
}
return userId;
}
}
二、编写controller,提供api接口
2.1、创建用户实体类
package com.maomao.demo.entity;
public class User {
private String username;
private String password;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
2.2、编写登陆方法,并传递token
因为是demo,所以就简单定义了账号和密码,就不跟数据库交互啦
package com.maomao.demo.controller;
import com.maomao.demo.annotation.LoginUser;
import com.maomao.demo.entity.User;
import com.maomao.demo.utils.UserTokenManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
UserTokenManager tokenManager;
/**
* 登录
*/
@RequestMapping("login")
public Object login(@RequestBody User user){
Map<String,Object> map=new HashMap<>();
//账号
String myUsername="xiaoming";
//密码
String myPassword="123";
//判断账号密码是否正确
if (user.getUsername().equals(myUsername)&&user.getPassword().equals(myPassword)){
map.put("code",0);
//创建token
String token = tokenManager.generateToken(user.getUsername());
map.put("token",token);
return map ;
}
map.put("code",-1);
return map;
}
}
2.3、在小程序中调用登陆接口,进行登陆操作
xml文件中,放入两个按钮,一个用来登陆,一个用来后续步骤中测试结果使用的
<button bindtap="login">登陆</button>
<button bindtap="submit">测试</button>
js:
const app = getApp()
Page({
data: {
},
onLoad: function () {
},
//登陆
login() {
wx.request({
url: 'http://localhost:8888/token/login',
data: {
username: "xiaoming",
password: "123"
},
method: "POST",
header: {
'Content-Type': "application/json"
},
success: function (res) {
console.log(res)
if (res.data.code == 0) {
//登陆成功后,将我们的token存入本地存储中
wx.setStorageSync('token', res.data.token)
}
if (res.data.code == -1) {
wx.showToast({
title: '账号或密码不正确!',
icon: "none"
})
}
}
})
}
})
2.4、在小程序中我们可以使用Promise来构建我们的请求
2.4.1、创建一个request.js的工具类
2.4.2、在我们每次请求时,在请求头部中加入我们的token
/**
* 封装微信请求方法
*/
function request(url,data={},method="GET"){
return new Promise(function(resolve,reject){
wx.request({
url: url,
method:method,
data:data,
header:{
'Content-Type':'application/json',
'myToken':wx.getStorageSync('token')
},
success:function(res){
if(res.data.code==401){
wx.showToast({
title: '没有权限',
})
return ;
}
resolve(res)
},
fail:function(err){
reject(err)
}
})
})
}
module.exports={
request:request
}
三、自定义方法参数构造器和参数注解
3.1、创建一个注解类
package com.maomao.demo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
//声明为参数注解
@Target(ElementType.PARAMETER)
//声明为在class和jvm中都有效
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {
}
3.2、创建自定义方法参数解析器
3.2.1、创建类并实现 HandlerMethodArgumentResolver接口
3.2.2、实现该接口后,需要重写它的两个方法supportsParameter和resolveArgument
supportsParameter:用于判断是否支持该参数,如果支持则返回true,这时的resolveArgument方法就会被调用
resolveArgument:参数解析器,最后需要返回该方法参数的值
3.2.3、我们需要在第一个函数中,判断方法中函数的类型以及它是否有使用我们自定义的注解
@Override
public boolean supportsParameter(MethodParameter methodParameter) {
//判断方法参数是否是字符串类型或其子类&&判断方法参数是否有使用@LoginUser注解
return methodParameter.getParameterType().isAssignableFrom(String.class)&&methodParameter.hasParameterAnnotation(LoginUser.class);
}
3.2.4、我们需要参数解析方法中获取请求头中的token,并对其进行有效性判断,最后返回值
@Override
public Object resolveArgument(MethodParameter methodParameter, ModelAndViewContainer modelAndViewContainer, NativeWebRequest nativeWebRequest, WebDataBinderFactory webDataBinderFactory) throws Exception {
//从请求头中获取携带的token
String myToken = nativeWebRequest.getHeader("myToken");
//判断值是否有效
if (myToken == null || myToken.isEmpty()) {
return null;
}
return UserTokenManager.getUserId(myToken);
}
3.3、编写完解析器后,我们需要在springmvc中加入该解析器
3.3.1、因为项目使用的是springboot,所以可以直接实现WebMvcConfigurer该接口,并将解析器加入即可
3.3.2、实现代码
package com.maomao.demo.config;
import com.maomao.demo.annotation.support.UserLoginHandleMethodArgumentResolver;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.List;
//注解为配置类,这样springboot启动时就会加载
@Configuration
public class loginWebMvcConfiguration implements WebMvcConfigurer {
@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(new UserLoginHandleMethodArgumentResolver());
}
}
四、测试
4.1、我们创建一个测试submit接口,来验证我们的username是否有被赋值
package com.maomao.demo.controller;
import com.maomao.demo.annotation.LoginUser;
import com.maomao.demo.entity.User;
import com.maomao.demo.utils.UserTokenManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
UserTokenManager tokenManager;
/**
* 登录
*/
@RequestMapping("login")
public Object login(@RequestBody User user){
Map<String,Object> map=new HashMap<>();
//账号
String myUsername="xiaoming";
//密码
String myPassword="123";
//判断账号密码是否正确
if (user.getUsername().equals(myUsername)&&user.getPassword().equals(myPassword)){
map.put("code",0);
//创建token
String token = tokenManager.generateToken(user.getUsername());
map.put("token",token);
return map ;
}
map.put("code",-1);
return map;
}
/**
* 提交订单
*/
@RequestMapping("submit")
public Object submit(@LoginUser String username){
Map<String,Object> map=new HashMap<>();
System.out.println("username:"+username);
if (username==null||username.length()==0){
map.put("code",401);
return map;
}
map.put("code",0);
return map;
}
}
4.2、在小程序中创建submit方法调用接口进行测试
const app = getApp()
var utils = require('../utils/request')
Page({
data: {
},
onLoad: function () {
},
//登陆
login() {
wx.request({
url: 'http://localhost:8888/token/login',
data: {
username: "xiaoming",
password: "123"
},
method: "POST",
header: {
'Content-Type': "application/json"
},
success: function (res) {
console.log(res)
if (res.data.code == 0) {
wx.setStorageSync('token', res.data.token)
}
if (res.data.code == -1) {
wx.showToast({
title: '账号或密码不正确!',
icon: "none"
})
}
}
})
},
//提交订单
submit() {
utils.request('http://localhost:8888/token/submit', {}, "POST")
.then(function (res) {
console.log(res)
})
}
})
4.3、测试结果
请求信息:
后端服务器: