JWT小程序登录的使用,基于Spring

JWT

JSON Web Token(JWT)是一种跨域身份验证解决方案。
最早的时候我写的安全验证都是基于session 的,后来做的一个分布式项目直接用redis做安全验证了。
最近的项目是小程序。
因为小程序本身的关系,用session的话会导致小程序挂起一阵子不理后可能会导致失效,虽然可以改造seasion的时间或者写一些监听事件。但是怪麻烦。
而且本身体积比较小,redis也是要基于时间存储,选型是有的不太舒服。
所以这里我用了jwt,虽然写工具也会有存储时间限制,但是他的本质状态,我觉得才是最适合我这个项目的。

区别

由于http协议是无状态的,每一次请求都无状态。当一个用户通过用户名和密码登录了之后,他的下一个请求不会携带任何状态,应用程序无法知道他的身份,那就必须重新认证。因此我们需要用户登录成功之后的每一次http请求,都能够保存他的登录状态。

这里单独说下他和session的区别

session

  • 用户输入其登录信息
  • 服务器验证信息是否正确,并创建session,存储
  • 服务器为用户生成一个sessionId,将具有sesssionId的Cookie将放置在用户浏览器中
  • 在后续请求中,会根据验证sessionID,如果有效,则接受请求
  • 一旦用户注销应用程序,会话将在客户端和服务器端都被销毁

jwt

基于token(令牌)的用户认证

  • 用户输入其登录信息
  • 服务器验证信息是否正确,并返回已签名的token
  • token储在客户端,小程序使用StorageSync存储
  • 之后的HTTP请求都将token添加到请求头里
  • 服务器解码JWT,并且如果令牌有效,则接受请求
  • 用户注销,令牌将在客户端被销毁,不需要与服务器进行交互一个关键是,令牌是无状态的。后端服务器不需要保存令牌

jwt组成

jwt的认证原理

一个jwt实际上就是一个字符串,它由三部分组成,头部、载荷与签名,这三个部分都是json格式。

头部(Header)

头部用于描述关于该JWT的最基本的信息,例如其类型以及签名所用的算法等。

{
  "typ": "JWT",
  "alg": "HS256"
}
载荷(Payload)

载荷可以用来放信息。

{
    "iss": "sharenotes",
    "exp": 1412252225,
  "iat": 1441593502,
    "aud": "MiNiapp",
    "sub": "Lets share notes",
}

这里面的前五个字段都是由JWT的标准所定义的。
● it’s: 该JWT的签发者
● sub: 该JWT所面向的用户
● aud: 接收该JWT的一方
● exp(expires): 什么时候过期,这里是一个Unix时间戳
● iat(issued at): 在什么时候签发的
把头部和载荷分别进行Base64编码之后得到两个字符串,然后再将这两个编码后的字符串用英文句号.连接在一起(头部在前),形成新的字符串:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIHNoYXJlTm90ZXMgdG9rZW4iLCJhdWQiOiJNSU5JQVBQIiw
签名

上面拼接完的字符串用HS256算法进行加密。在加密的时候,我们还需要提供一个密钥(secret)。加密后的内容也是一个字符串,最后这个字符串就是签名,把这个签名拼接在刚才的字符串后面就能得到完整的jwt。header部分和payload部分如果被篡改,由于篡改者不知道密钥是什么,也无法生成新的signature部分,服务端也就无法通过,在jwt中,消息体是透明的,使用签名可以保证消息不被篡改。

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0aGlzIGlzIHNoYXJlTm90ZXMgdG9rZW4iLCJhdWQiOiJNSU5JQVBQIiwiaXNzIjoic2hhcmVOb3RlcyIsImV4cCI6MTU3MjEwMTc1OCwidXNlcklkIjo4LCJpYXQiOjE1NzE0ODk3NTh9.ffma0A0gazhRGD9Mbu_jxTMPd3WrtK59dLdc2hhfhdwj

开始使用

根据上面的知识点后,大概应该知道jwt是啥了

我们以上一个文章中的登录为基础

到Controller下的WxAuthController.java
token String token = UserTokenManager.generateToken(1user.getId());
中。
我们以这里的UserTokenManager开始

先在这里打个断点,等等回回到这里。

开始操作

配置jwt

添加maven依赖

 <dependency>
           <groupId>com.auth0</groupId>
           <artifactId>java-jwt</artifactId>
           <version>3.4.1</version>
       </dependency>

创建工具类

JwtHelper.java

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.exceptions.JWTVerificationException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import lombok.extern.slf4j.Slf4j;

import java.util.*;

public class JwtHelper {
// 秘钥
static final String SECRET = "F-shareNotes-Token";
// 签名是有谁生成
static final String ISSUSER = "shareNotes";
// 签名的主题
static final String SUBJECT = "this is shareNotes token";
// 签名的观众
static final String AUDIENCE = "MINIAPP";
public String createToken(Integer userId){
try {
    Algorithm algorithm = Algorithm.HMAC256(SECRET);
    Map<String, Object> map = new HashMap<String, Object>();
    Date nowDate = new Date();
    // 过期时间:7天2小时
    Date expireDate = getAfterDate(nowDate,0,0,7,2,0,0);
        map.put("alg", "HS256");
        map.put("typ", "JWT");
    String token = JWT.create()
    // 设置头部信息 Header
    .withHeader(map)
    // 设置 载荷 Payload
    .withClaim("userId", userId)
        .withIssuer(ISSUSER)
        .withSubject(SUBJECT)
        .withAudience(AUDIENCE)
        // 生成签名的时间 
        .withIssuedAt(nowDate)
        // 签名过期的时间 
        .withExpiresAt(expireDate)
        // 签名 Signature
        .sign(algorithm);
    return token;
} catch (JWTCreationException exception){
exception.printStackTrace();
}
return null;
}
public Integer verifyTokenAndGetUserId(String token) {
try {
    Algorithm algorithm = Algorithm.HMAC256(SECRET);
    JWTVerifier verifier = JWT.require(algorithm)
        .withIssuer(ISSUSER)
        .build();
    DecodedJWT jwt = verifier.verify(token);
    Map<String, Claim> claims = jwt.getClaims();
    Claim claim = claims.get("userId");
    return claim.asInt();
} catch (JWTVerificationException exception){
// exception.printStackTrace();
}
return 0;
}
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();
}
}

这里的代码分成了createToken,verifyTokenAndGetUserId,getAfterDate

createToken(Integer userId)

这里声明加密方法,还有过期时间,根据我们getAfterDate生成了时间数字
接着是设置了头部。
和我们上面介绍jwt的组成,载荷,签名一样。
重点是

withClaim("userId", userId)

我们这里把需要的userId根据key,value设置了对应的值随便存入了token中。
如果你有其他想要存入的也可以继续加上去。
生成token就这样了。

Integer verifyTokenAndGetUserId(String token)

这里是根据我们从http头部中获取的token了,解析。

Algorithm algorithm = Algorithm.HMAC256(SECRET);
    JWTVerifier verifier = JWT.require(algorithm)
        .withIssuer(ISSUSER)
        .build();
    DecodedJWT jwt = verifier.verify(token);
    Map<String, Claim> claims = jwt.getClaims();

这里就单纯的解密。
注意这一句

Map<String, Claim> claims = jwt.getClaims();

获取claims,也就是我们刚刚注入的kv

Claim claim = claims.get("userId");
return claim.asInt();

返回userId

测试

在这里插入图片描述

这里的头就是我们刚刚在createToken 声明的头部。
后面的值就是token值

我们放到代码中

工具类
public class UserTokenManager {

    public static String generateToken(Integer id) {
        JwtHelper jwtHelper = new JwtHelper();
        return jwtHelper.createToken(id);
    }
    public static Integer getUserId(String token) {
        JwtHelper jwtHelper = new JwtHelper();
        Integer userId = jwtHelper.verifyTokenAndGetUserId(token);
        if(userId == null || userId == 0){
            return null;
        }
        return userId;
    }

}

这里就不多做解释了,代码很简单

使用
public static final String LOGIN_TOKEN_KEY = "F-shareNotes-Token";


       HttpServletRequest request = HttpContextUtil.getHttpServletRequest();
        String token = request.getHeader(LOGIN_TOKEN_KEY);
        if (token == null || token.isEmpty()) {
            return null;
        }

        Integer userId = UserTokenManager.getUserId(token);

这里有一个request 封装类。网上可自寻查询。或者可直接用Controller的request 中请求

封装方法到注解中

我们创建一个java LoginUser

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface LoginUser {

}

这里Target声明了他只能使用在方法参数上面。

实现注解使用

创建 新的一个方法实现HandlerMethodArgumentResolver接口,他是在springframwork包中的。

public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {

同理,实现方法和上面引用一样。
不过我们要先实现两个接口

@Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
    }

这是是判断变量类型的正确性。
只要是Integer类型,然后是前缀是LoginUser注解。
这里。我们就可以取得刚刚实现的注解。并且给他赋值了。

######## 赋值

@Override
   public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                 NativeWebRequest request, WebDataBinderFactory factory) throws Exception {


       String token = request.getHeader(LOGIN_TOKEN_KEY);
       if (token == null || token.isEmpty()) {
           return null;
       }

       return UserTokenManager.getUserId(token);
   }

这里不做说明,就是在上面使用的方法。只是request 是根据NativeWebRequest中,但是request的本质都一样。
其是Spring 中的request方法。无大碍。

####### 具体代码

public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver {
    public static final String LOGIN_TOKEN_KEY = "F-shareNotes-Token";

    @Override
    public boolean supportsParameter(MethodParameter parameter) {
        return parameter.getParameterType().isAssignableFrom(Integer.class) && parameter.hasParameterAnnotation(LoginUser.class);
    }

    @Override
    public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container,
                                  NativeWebRequest request, WebDataBinderFactory factory) throws Exception {


        String token = request.getHeader(LOGIN_TOKEN_KEY);
        if (token == null || token.isEmpty()) {
            return null;
        }

        return UserTokenManager.getUserId(token);
    }
}

全局使用

我们可以直接在controller中直接

@GetMapping("/getDetail/{msg_id}")
   public Object getAllCategories(@LoginUser Integer userId, @PathVariable("msg_id") Integer msg_id){

取得userId

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值