JWT令牌在登录中的应用

前言

最近在写小程序登录功能时用到了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"

有什么说的不对的地方请大家指正!!!

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值