前言
最近在写小程序登录功能时用到了JWT生成令牌,所以今天就在这做一下总结与记录。
什么是JWT?
给大家推荐一片文章:JWT简介,简单的来说JWT就是用来生成token的,里面包含了用户的自定义信息.还具有校验这个token的功能.这次我用这个保存用户的信息,完成登录功能。
保存用户信息的几种方式
1.前端将用户信息发送给后端,后端进行相应的处理并把保存到session中,在将session_id返回给前端,并保存到cookie当中,用户每次发送请求时带着此cookie,用于后端进行校验。
此方法可以用在传统的项目当中,并不适用于分布式集群的项目,而且session是保存在内存当中的,随着用户的增多会占用大量内存空间,到时服务器处理速度变慢。
2.前端将用户信息发送到后端,后端将用户信息保存到数据库中,并返回一个token,前端将token保存到cookie当中,用户每次发送请求时带着此cookie,用户后端进行校验。
此方法可以用于传统项目,也可以用于分布式集群项目,但是在每次校验用户信息的时候将会去查询数据库,这样对数据库会造成很大的压力。
3.前端将用户信息发送给后端,后端生成一个token(最早用UUID和时间戳组合)作为key,用户信息作为value存入到redis缓存服务器,然后后端将token返回给前端保存到cookie中,用户每次发送请求时带着此cookie,用户后端进行校验。
这样每次校验用户信息时直接从缓存中取出用户信息进行校验,减少了访问数据库的次数。也是一个比较好的实现session共享的方案。
4.以上三种方法都是后端保存用户信息。而这次用的JWT则是前端来保存用户信息。
以下我们着重说一下通过JWT方式实现这本次需求
需求分析:
1.用户在访问小程序首页时,将code码发送给后端,
2.后端进行对请求的header中是否有token进行判断,如果没有则调用获取token的方法。
3.后端根据code码像微信接口服务器请求用户openid(openid为用户在此应用程序的唯一标识)。
4.后端根据openid判断数据中是否有用户信息,如果没有则将注册用户信息。
5.生成token令牌,将入户信息保存在token中。并返回给前端。
6.前端将此token保存到缓存当中,当下次访问后端API时将此token放到header的Authorization中一起发送给后端。
7.让用户实现一个无感知的登录过程。
8.后端通过拦截器的方式对非公共接口请求中token的合法性进行校验,以及权限的判断。
微信登录流程图:
上代码(此次代码是从第3步开始)
pom文件导入auth0的依赖,auth0是实现JWT的SDK,JWT官网友很多SDK可供选择:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.8.1</version>
</dependency>
application.yml配置信息
wx:
appid: 从微信开放平台注册获取
appsecret: 从微信开放平台注册获取
code2session: https://api.weixin.qq.com/sns/jscode2session?appid={0}&secret={1}&js_code={2}&grant_type=authorization_code
sevencell:
security:
# 配置jwt加密算法的随机字符串
jwt-key: 66666
#令牌过期时间
token-expired-in: 86400000
前端测试获取token的代码:
onGetToken() {
// code
wx.login({
success: (res) => {
if (res.code) {
wx.request({
url: 'http://localhost:8080/v1/token',
method: 'POST',
data: {
account: res.code,
type: 0
},
success: (res) => {
console.log(res.data)
const code = res.statusCode.toString()
if (code.startsWith('2')) {
wx.setStorageSync('token', res.data.token)
}
}
})
}
}
})
}
Controller层代码
import com.my.sevencell.api.dto.TokenDTO;
import com.my.sevencell.api.dto.TokenGetDTO;
import com.my.sevencell.api.exception.http.NotFoundException;
import com.my.sevencell.api.service.AuthenticationService;
import com.my.sevencell.api.utils.JwtToken;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
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;
import javax.validation.constraints.NotBlank;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/token")
public class TokenController {
@Autowired
private AuthenticationService authenticationService;
/**
* 通过JWT获取令牌
* @param userData
* @return
*/
@PostMapping()
@Transactional(rollbackFor = Exception.class)
public Map<String,Object> getToken(@RequestBody @Validated TokenGetDTO userData){
HashMap<String, Object> hashMap = new HashMap<>();
String token = null;
switch (userData.getType()){
case USER_WX:
token = authenticationService.code2Session(userData.getAccount());
break;
case USER_EMAIL:
break;
default:
throw new NotFoundException(10003);
}
hashMap.put("token",token);
return hashMap;
}
TokenGetDTO 代码
import com.my.sevencell.api.core.enumeration.LoginType;
import com.my.sevencell.api.validators.TokenPassword;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
@Getter
@Setter
@ToString
public class TokenGetDTO {
/**
* 接收前端传过来的户名,如果是微信登录则接收code码
*/
@NotBlank
private String account;
/**
* 密码
*/
//自定义校验注解
@TokenPassword(min=6,max=32,message="{token_password}")
private String password;
/**
* 枚举类型,接收登录方式,是微信还是邮件
*/
private LoginType type;
}
LoginType 代码
@Getter
public enum LoginType {
USER_WX(1,"微信登录"),USER_EMAIL(2,"邮箱登录");
private int code;
private String msg;
LoginType(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
Service层代码
public interface AuthenticationService {
/**
* 微信登录后,后端验证code码,验证通过后通过相关规则生成Token
* 可参考以下网址,微信登录步骤
* https://developers.weixin.qq.com/miniprogram/dev/framework/open-ability/login.html
*
* @param code
* @return
*/
String code2Session(String code);
}
Service的实现层
import com.fasterxml.jackson.databind.ObjectMapper;
import com.my.sevencell.api.dao.UserDao;
import com.my.sevencell.api.exception.http.HttpException;
import com.my.sevencell.api.exception.http.ParameterException;
import com.my.sevencell.api.model.UserEntity;
import com.my.sevencell.api.service.AuthenticationService;
import com.my.sevencell.api.utils.JwtToken;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import java.text.MessageFormat;
import java.util.Map;
@Service
@Slf4j
@Transactional(rollbackFor = Exception.class)
public class AuthenticationServiceImpl implements AuthenticationService {
@Value("${wx.appid}")
private String appid;
@Value("${wx.appsecret}")
private String appsecret;
@Value("${wx.code2session}")
private String code2SessionUrl;
@Autowired
private ObjectMapper mapper;
@Autowired
private UserDao userDao;
@Override
public String code2Session(String code) {
String token = null;
try {
/*1.通过code码换区用户的openid*/
//拼接url地址
String url = MessageFormat.format(code2SessionUrl, appid, appsecret, code);
RestTemplate restTemplate = new RestTemplate();
String sessionText = restTemplate.getForObject(url, String.class);
Map<String, Object> session = mapper.readValue(sessionText, Map.class);
/*2.注册,查询用户信息,如果第一次登录则写入数据库进行注册*/
UserEntity user = registerUser(session);
/*3,通过JWT生成token返回给小程序*/
token = JwtToken.makeToken(user.getId());
} catch (Exception e) {
log.error(e.getMessage());
throw new HttpException();
}
return token;
}
private UserEntity registerUser(Map<String, Object> session) {
String openid = (String) session.get("openid");
if (openid.isEmpty()) {
throw new ParameterException(20004);
}
UserEntity user = userDao.findByOpenid(openid);
if (user == null) {
user = UserEntity.builder().openid(openid).build();
userDao.save(user);
}
return user;
}
}
JwtToken.makeToken方法的代码:
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.my.sevencell.api.exception.http.UnAuthenticatedException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.*;
/**
* @Description: 生成jwt token的工具类
* @Author: my
* @CreateDate: 2020/3/20 14:33
* @UpdateUser:
* @UpdateDate: 2020/3/20 14:33
* @UpdateRemark: 修改内容
* @Version: 1.0
*/
@Component
public class JwtToken {
private static String kwtKey;
private static Integer expiredTimeIn;
//默认登记
private static Integer defalutScope = 8;
public static final String TOKENPREFIX = "Bearer";
public static final int TOKENLENGTH = 2;
/**
* 提供给前端校验令牌用
* @param token
* @return
*/
public static boolean verifyToken(String token) {
Algorithm algorithm = Algorithm.HMAC256(JwtToken.kwtKey);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
try {
jwtVerifier.verify(token);
}catch (Exception e){
return false;
}
return true;
}
@Value("${sevencell.security.jwt-key}")
public void setKwtKey(String kwtKey) {
JwtToken.kwtKey = kwtKey;
}
@Value("${sevencell.security.token-expired-in}")
public void setExpiredTimeIn(Integer expiredTimeIn) {
JwtToken.expiredTimeIn = expiredTimeIn;
}
/**
* 生成令牌
* @param uid
* @param scope
* @return
*/
public static String makeToken(Long uid, Integer scope) {
return getToken(uid,scope);
}
/**
* 生成令牌
* @param uid
* @return
*/
public static String makeToken(Long uid) {
return getToken(uid,JwtToken.defalutScope);
}
/**
* 校验令牌,并获取其中的自定义参数
* @return
*/
public static Optional<Map<String, Claim>> getClaims(String token){
Algorithm algorithm = Algorithm.HMAC256(JwtToken.kwtKey);
JWTVerifier jwtVerifier = JWT.require(algorithm).build();
DecodedJWT decodedJWT = null;
try {
decodedJWT = jwtVerifier.verify(token);
}catch (JWTVerificationException e){
return Optional.empty();
}
Map<String, Claim> claims = decodedJWT.getClaims();
return Optional.of(claims);
}
private static String getToken(Long uid, Integer scope) {
//设置一种加密算法
Algorithm algorithm = Algorithm.HMAC256(JwtToken.kwtKey);
//获取过期时间
Date expiredTime = calculateExpiredIssues().get("expiredTime");
//获取注册时间
Date nowTime = calculateExpiredIssues().get("nowTime");
//生成token令牌
String token = JWT.create()
//添加的自定义数据
.withClaim("uid", uid)
.withClaim("scope", scope)
//签发时间
.withIssuedAt(nowTime)
//过期时间
.withExpiresAt(expiredTime)
//生成jwt令牌
.sign(algorithm);
return token;
}
/**
* 计算过期时间
*
* @return
*/
private static Map<String, Date> calculateExpiredIssues() {
HashMap<String, Date> map = new HashMap<>();
Calendar calendar = Calendar.getInstance();
Date nowTime = calendar.getTime();
calendar.add(Calendar.SECOND,JwtToken.expiredTimeIn);
Date expiredTime = calendar.getTime();
map.put("nowTime",nowTime);
map.put("expiredTime",expiredTime);
return map;
}
public static Optional<Map<String, Claim>> getStringClaimMap(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if(StringUtils.isEmpty(bearerToken)){
throw new UnAuthenticatedException(10004);
}
//所有的令牌是以Bearer开头的
if(!bearerToken.startsWith(TOKENPREFIX)){
throw new UnAuthenticatedException(10004);
}
String[] tokens = bearerToken.split(" ");
if (tokens.length != TOKENLENGTH){
throw new UnAuthenticatedException(10004);
}
String token = tokens[1];
return JwtToken.getClaims(token);
}
}
UserDao:
import com.my.sevencell.api.model.UserEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao extends JpaRepository<UserEntity,Long> {
UserEntity findByOpenid(String openid);
}
测试:
token:"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1aWQiOjM0LCJzY29wZSI6OCwiZXhwIjoxNjcxMzc2MDIxLCJpYXQiOjE1ODQ5NzYwMjF9.i9oe-E9CsZGqpjfvWyjmGIO-dH8K7pW_m-R80r5hfes"