JWT 实现登录

我们首先回顾下过去登录的方式,用户页面中输入账号和密码,点击提交发送到 controller 类中,controller 类接收账号和密码,并且调用业务和数据层去判断账号和密码是否正确,如果错误,返回到登录页面,如果正确,将用户信息保存到 session 中并跳转到主页面。

这里会发现,我们是通过 session 来记录用户登录状态,跟踪用户信息,那么就要在这里提一提 session 登录的缺陷:

  1. session 是将客户端数据储存在服务器的内存,当客户端的数据过多,连接较多,服务器的内存开销大。
  2. session 的数据储存在某台服务器,在分布式的项目中无法做到共享。
  3. 前后端分离的项目中共享 session 比较困难。
  4. jwt 不需要在服务端去保留用户的认证信息或者会话信息。

什么是 JWT

jwt 全称是 json web token。是由用户以用户名、密码登录,服务端验证后,会生成一个 token,返回给客户端,客户端在下次访问的过程中携带这个 token,服务端责每次验证这个token。 

JWT 的构成

jwt 由三部分组成,每一部分之间用符号"."进行分割,整体可以看做是一个长字符串。一个经典的jwt的样子:xxx.xxx.xxx。

Header 头部

头部由两部分组成:第一部分是声明类型,在 jwt 中声明类型就 jwt,第二部分是声明加密的算法,加密算法通常使用 HMAC|SHA256。一个经典的头部:

{
  'type': 'JWT',      //  'type':'声明类型'
  'alg': 'HS256'	//	'alg':'声明的加密算法'
}

Payload 载体、载荷

这一部分是jwt的主体部分,这一部分也是json对象,可以包含需要传递的数据,其中jwt指定了七个默认的字段选择,这七个字段是推荐但是不强制使用的:

  • iss:发行人
  • exp:到期时间
  • sub:主题
  • aud:用户
  • nbf:在此之前不可用
  • iat:发布时间
  • jti:JWT ID 用于识别该 JWT

除了上述的七个默认字段之外,还可以自定义字段,通常我们说 JWT 用于用户登陆,就可以在这个地方放置用户的id和用户名。下面这个json对象是一个 jwt 的 Payload 部分:

{
"sub": "一个演示",
"nickname": "dailyblue",
"id": "001"
}

这里注意虽然可以放自定的信息,但是不要存放一些敏感信息,除非是加密过的,因为这里的信息可能会被截获。 

signature  签证

这部分是对前两部分进行base64编码在进行加密,这个加密的方式使用的是jwt的头部声明中的加密方式,在加上一个密码(secret)组成的,secret 通常是一个随机的字符串,这个 secret是服务器特有的,不能够让其他人知道。这部分的组成公式是:

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)

JWT 的优点

  1. json形式,而json非常通用性可以让它在很多地方使用
  2. jwt所占字节很小,便于传输信息
  3. 需要服务器保存信息,易于扩展 

使用 JWT 登录流程

JWT 实现登录_第1张图片

  1. 第一次登录的时候,前端调后端的登陆接口,发送帐号和密码。
  2. 后端收到请求,验证帐号和密码,验证成功,就给前端返回一个 jwt。
  3. 前端拿到 jwt,将 jwt 存储到 localStroage 或 header 中,并跳转到页面。
  4. 前端每次跳转页面,就判断 localStroage 中有无 jwt ,没有就跳转到登录页面,有则跳转到对应页面。
  5. 每次调后端接口,都要在请求头中加 jwt。
  6. 后端判断请求头中有无 jwt,有 jwt,就拿到 jwt 并验证 jwt,验证成功就返回数据,验证失败(例如:jwt 过期)就返回401,请求头中没有 jwt 也返回401。
  7. 如果前端拿到状态码为401,就清除 jwt 信息并跳转到登录页面。

所需依赖


   org.springframework.boot
   spring-boot-starter-web


   org.projectlombok
   lombok


   io.jsonwebtoken
   jjwt
   0.9.1



   javax.xml.bind
   jaxb-api
   2.3.0


   com.sun.xml.bind
   jaxb-impl
   2.3.0


   com.sun.xml.bind
   jaxb-core
   2.3.0


   javax.activation
   activation
   1.1.1

JWT 工具类

package com.dailyblue.java.spring.config;

import com.dailyblue.java.spring.bean.User;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;

import javax.servlet.http.HttpServletRequest;
import java.util.Date;

/**
 * @author dailyblue
 * @since 2022/6/23
 */
public class JwtConfig {

    //常量
    public static final long EXPIRE = 1000 * 60 * 60 * 24; //token过期时间
    public static final String APP_SECRET = "1234"; //秘钥,加盐

    //	@param id 当前用户ID
    //	@param issuer 该JWT的签发者,是否使用是可选的
    //	@param subject 该JWT所面向的用户,是否使用是可选的
    //	@param ttlMillis 什么时候过期,这里是一个Unix时间戳,是否使用是可选的
    //	@param audience 接收该JWT的一方,是否使用是可选的
    //生成token字符串的方法
    public static String getJwtToken(User user) {

        String JwtToken = Jwts.builder()
                .setHeaderParam("type", "JWT")    //头部信息
                .setHeaderParam("alg", "HS256")    //头部信息
                //下面这部分是payload部分
                // 设置默认标签
                .setSubject("dailyblue")    //设置jwt所面向的用户
                .setIssuedAt(new Date())    //设置签证生效的时间
                .setExpiration(new Date(System.currentTimeMillis() + EXPIRE))    //设置签证失效的时间
                //自定义的信息,这里存储id和姓名信息
                .claim("id", user.getId())  //设置token主体部分 ,存储用户信息
                .claim("name", user.getUserName())
                //下面是第三部分
                .signWith(SignatureAlgorithm.HS256, APP_SECRET)
                .compact();
        // 生成的字符串就是jwt信息,这个通常要返回出去
        return JwtToken;
    }

    /**
     * 判断token是否存在与有效
     * 直接判断字符串形式的jwt字符串
     *
     * @param jwtToken
     * @return
     */
    public static boolean checkToken(String jwtToken) {
        if (StringUtils.isEmpty(jwtToken)) return false;
        try {
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 判断token是否存在与有效
     * 因为通常jwt都是在请求头中携带,此方法传入的参数是请求
     *
     * @param request
     * @return
     */
    public static boolean checkToken(HttpServletRequest request) {
        try {
            String jwtToken = request.getHeader("token");//注意名字必须为token才能获取到jwt
            if (StringUtils.isEmpty(jwtToken)) return false;
            Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }

    /**
     * 根据token字符串获取会员id
     * 这个方法也直接从http的请求中获取id的
     *
     * @param request
     * @return
     */
    public static String getMemberIdByJwtToken(HttpServletRequest request) {
        String jwtToken = request.getHeader("token");
        if (StringUtils.isEmpty(jwtToken)) return "";
        Jws claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
        Claims claims = claimsJws.getBody();
        return (String) claims.get("id");
    }

    /**
     * 解析JWT
     * @param jwt
     * @return
     */
    public static Claims parseJWT(String jwt) {
        Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwt).getBody();
        return claims;
    }
}

相关工具类在网上有很多,有兴趣的童鞋也可以相互借鉴。 

返回结果工具类

状态码返回

package com.dailyblue.java.spring.util;

/**
 * @Author: Dailyblue
 * @Description: 返回码定义
 * 规定:
 * #1表示成功
 * #1001~1999 区间表示参数错误
 * #2001~2999 区间表示用户错误
 * #3001~3999 区间表示接口异常
 * @Date Create in 2022/06/21 19:28
 */
public enum ResultCode {
    /* 成功 */
    SUCCESS(200, "成功"),

    /* 默认失败 */
    COMMON_FAIL(999, "失败"),

    /* 参数错误:1000~1999 */
    PARAM_NOT_VALID(1001, "参数无效"),
    PARAM_IS_BLANK(1002, "参数为空"),
    PARAM_TYPE_ERROR(1003, "参数类型错误"),
    PARAM_NOT_COMPLETE(1004, "参数缺失"),

    /* 用户错误 */
    USER_NOT_LOGIN(2001, "用户未登录"),
    USER_ACCOUNT_EXPIRED(2002, "账号已过期"),
    USER_CREDENTIALS_ERROR(2003, "密码错误"),
    USER_CREDENTIALS_EXPIRED(2004, "密码过期"),
    USER_ACCOUNT_DISABLE(2005, "账号不可用"),
    USER_ACCOUNT_LOCKED(2006, "账号被锁定"),
    USER_ACCOUNT_NOT_EXIST(2007, "账号不存在"),
    USER_ACCOUNT_ALREADY_EXIST(2008, "账号已存在"),
    USER_ACCOUNT_USE_BY_OTHERS(2009, "账号下线"),

    /* 业务错误 */
    NO_PERMISSION(3001, "没有权限");
    private Integer code;
    private String message;

    ResultCode(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public void setCode(Integer code) {
        this.code = code;
    }

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    /**
     * 根据code获取message
     *
     * @param code
     * @return
     */
    public static String getMessageByCode(Integer code) {
        for (ResultCode ele : values()) {
            if (ele.getCode().equals(code)) {
                return ele.getMessage();
            }
        }
        return null;
    }
}

统一返回实体

package com.dailyblue.java.spring.util;

import lombok.Data;

import java.io.Serializable;

/**
 * @Author: Dailyblue
 * @Description: 统一返回实体
 * @Date Create in 2022/06/21 19:28
 */
@Data
public class JsonResult implements Serializable {
    private Boolean success;
    private Integer errorCode;
    private String errorMsg;
    private T data;

    public JsonResult() {
    }

    public JsonResult(boolean success) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
    }

    public JsonResult(boolean success, ResultCode resultEnum) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
    }

    public JsonResult(boolean success, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : ResultCode.COMMON_FAIL.getCode();
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : ResultCode.COMMON_FAIL.getMessage();
        this.data = data;
    }

    public JsonResult(boolean success, ResultCode resultEnum, T data) {
        this.success = success;
        this.errorCode = success ? ResultCode.SUCCESS.getCode() : (resultEnum == null ? ResultCode.COMMON_FAIL.getCode() : resultEnum.getCode());
        this.errorMsg = success ? ResultCode.SUCCESS.getMessage() : (resultEnum == null ? ResultCode.COMMON_FAIL.getMessage() : resultEnum.getMessage());
        this.data = data;
    }

}

返回体构造器

package com.dailyblue.java.spring.util;

/**
 * @Author: Dailyblue
 * @Description: 返回体构造工具
 * @Date Create in 2022/06/21 19:28
 */
public class ResultTool {
    public static JsonResult success() {
        return new JsonResult(true);
    }

    public static  JsonResult success(T data) {
        return new JsonResult(true, data);
    }

    public static JsonResult fail() {
        return new JsonResult(false);
    }

    public static JsonResult fail(ResultCode resultEnum) {
        return new JsonResult(false, resultEnum);
    }
}

前端发送时携带 jwt

axios.get('url',{
  headers: 
  {
    'key': value
  }
})

详细的代码会在课程中涉及到,这里只罗列了流程和概念性问题。

你可能感兴趣的:(Spring,spring)