SpringBoot+vue+Axios实现Token令牌

发布时间 2023-10-14 02:34:22作者: 凉冰24

前后端Token实现

认识Token

对Token有了解可以跳过。

使用Token存储用户信息,认证用户。 Token是一个访问系统的令牌(字符串)。Token 是在服务端产生的。前端可以使用用户名/密码向服务端请求认证(登录),服务端认证成功,服务端会返回 Token 给前端。前端可以在每次请求的时候带上 Token 证明自己的身份。服务器端在处理请求之前,先验证Token,验证通过,可以访问系统,执行业务请求处理。

Token可以使用自己的算法自定义,比如给每个用户分配一个UUID ,UUID值就看做是一个Token 。

Token统一的规范是JWT( json web token ) 。用json表示token。JWT定义了一种紧凑而独立的方法,用于在各方之间安全地将信息作为JSON对象传输。

常使用的Token库是 jjwt

image-20231014011615711

JWT组成

JWT这是一个很长的字符串,中间用点(.)分隔成三个部分。JWT 内部是没有换行的,一行数据。

JWT 的三个部分依次如下。

Header(头部)

Payload(负载)

Signature(签名)

形如:

eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTE5NzgzNDQsImlhdCI6MTY5MTk3Nzc0NCwianRpIjoiNDEyNjRhNTctYTY0ZC00Y2E5LTljMzMtY2I1MGJmNDc5YjEzIiwicm9sZSI6Iue7j-eQhiIsIm5hbWUiOiLmnY7lm5siLCJ1c2VySWQiOjEwMDF9.t4IrCIs8Y1zLwehouUugeAc-8PBlndpXz5xhibtQIgo

Header: 是一个json对象,存储元数据

{
	"alg": "HS256", // alg:是签名的算法名称(algorithm),默认是 HMAC SHA256(写成 HS256);
	"typ": "JWT"	//typ属性表示这个令牌(token)的类型(type),JWT 令牌统一写为JWT。
}

元数据的json对象使用base64URL编码.

Payload

Payload :负载,是一个json对象。是存放传递的数据 ,数据分为Public的和Private的。

Public是JWT中规定的一些字段,可以自己选择使用。

  • iss (issuer):签发人
  • exp (expiration time):过期时间
  • sub (subject):该JWT所面向的用户
  • aud (audience):受众,接收该JWT的一方
  • nbf (Not Before):生效时间
  • iat (Issued At):签发时间
  • jti (JWT ID):编号

Private是自己定义的字段

{
 "role": "经理",
 "name": "张凡",
 "id": 2345
}

Playload默认是没有加密的, 以上数据都明文传输的,所以不要放敏感数据.此部分数据也是json对象使用base64URL编码,是一个字符串

Signature

Signature:签名。签名是对Header和Payload两部分的签名,目的是防止数据被篡改

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

签名算法:先指定一个 secret秘钥, 把base64URL的header , base64URL的payload 和 secret秘钥 使用 HMAC SHA256 生成签名字符串

JWT简单使用

  1. 首先用户使用类似像用户名,密码登录服务器,校验用户身份,服务器返回jwt token

  2. 客户端获取 jwt token , 使用cookie或者localStorage存储jwt token,但这样不能跨域, 常用的方式:

    • 放在请求header 的 Authorization中 Authorization: Bearer

    • 也可以放在 get 或者 post请求的参数中

  3. 客户端访问其他api接口, 传递token给服务器, 服务器认证token后,返回给请求的数据

创建JWT

@Test
public void testCreateJwt(){
    String key = "7d8e742c14674758b9162892eec9c59c";// 32位、全局唯一

    // 创建secereKey
    SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));

    // 存储用户数据
    Map<String,Object> map = new HashMap();
    map.put("userId",1001);
    map.put("name","李四");
    map.put("role","经理");
	// 获取签发时间
    Date curDate = new Date();
    // 创建jwt
    String jwt = Jwts.builder().signWith(secretKey, SignatureAlgorithm.HS256)		// Header
                                .setExpiration(DateUtils.addMinutes(curDate, 10))	// 设置10分钟后过期
                                .setIssuedAt(curDate)								// 设置签发时间
                                .setId(UUID.randomUUID().toString())				// Token唯一编号
                                .addClaims(map).compact();							// 添加有关用户或其他实体的声明信息
    System.out.println("jwt = " + jwt);
    // eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTE5NzgzNDQsImlhdCI6MTY5MTk3Nzc0NCwianRpIjoiNDEyNjRhNTctYTY0ZC00Y2E5LTljMzMtY2I1MGJmNDc5YjEzIiwicm9sZSI6Iue7j-eQhiIsIm5hbWUiOiLmnY7lm5siLCJ1c2VySWQiOjEwMDF9.t4IrCIs8Y1zLwehouUugeAc-8PBlndpXz5xhibtQIgo
}

解析JWT

@Test
public void testReadJwt(){
    String jwt = "eyJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2OTE5Nzk4MTMsImlhdCI6MTY5MTk3OTIxMywianRpIjoiYzAyYzRiMzMtODAzZS00MDk1LWI0NTctZjVmY2NkYTMyOTU2Iiwicm9sZSI6Iue7j-eQhiIsIm5hbWUiOiLmnY7lm5siLCJ1c2VySWQiOjEwMDF9.vUsmvOF-rv6XVCzuNgJZCGBohzDeROUFUf-c3iRv6NA";
    String key = "7d8e742c14674758b9162892eec9c59c";// 加密用的32位key

    // 创建secretKey
    SecretKey secretKey = Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));

    // 解析Jwt 没有异常 解析成功
    Jws<Claims> claims = Jwts.parserBuilder() 				// 1.获取Buider对象
        .setSigningKey(secretKey) 	// 2.设置key
        .build()					// 3.获取Parser
        .parseClaimsJws(jwt);		// 4.解析数据
    // 读数据
    Claims body = claims.getBody();

    Integer userId = body.get("userId",Integer.class);
    System.out.println("userId = " + userId);

    Object uId = body.get("userId");
    System.out.println("uId = " + uId);

    Object name = body.get("name");
    if(name != null){
        System.out.println("name = " + name);
    }

    String jwtId = body.getId();
    System.out.println("jwtId = " + jwtId);

    Date expiration = body.getExpiration();
    System.out.println("expiration = " + expiration);

}

常见异常

ClaimJwtException 获取Claim异常

ExpiredJwtException token过期异常

IncorrectClaimException token无效

MalformedJwtException 密钥验证不一致

MissingClaimException JWT无效

RequiredTypeException 必要类型异常

SignatureException 签名异常

UnsupportedJwtException 不支持JWT异常

后端

Maven依赖

<!--jwt-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred -->
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-lang3</artifactId>
</dependency>

封装JWT工具

public class JwtUtile {
	// 全局唯一、乱序的32位字符串。存储在配置文件中
    private String selfKey;

    public JwtUtile(String selfKey) {
        this.selfKey = selfKey;
    }

    // 创建Token
    public String creatJwt(Map<String,Object> data,Integer minute) throws Exception{
        Date cruDate = new Date();
        SecretKey secretKey = Keys.hmacShaKeyFor(selfKey.getBytes(StandardCharsets.UTF_8));
        String jwt = Jwts.builder().signWith(secretKey, SignatureAlgorithm.HS256)
                .setExpiration(DateUtils.addMinutes(cruDate, minute))
                .setIssuedAt(cruDate)
                .setId(UUID.randomUUID().toString().replaceAll("-", "").toUpperCase())
                .addClaims(data)
                .compact();
        return jwt;
    }
    // 读取Jwt
    public Claims readJWT(String jwt) throws Exception{
        SecretKey secretKey = Keys.hmacShaKeyFor(selfKey.getBytes(StandardCharsets.UTF_8));
        Claims body = Jwts.parserBuilder().setSigningKey(secretKey)
                .build().parseClaimsJws(jwt).getBody();
        return body;
    }

}

获取并响应Token

@PostMapping("/login")
public RespResult userLogin(@RequestParam String phone,
                            @RequestParam String pword,
                            @RequestParam String scode) throws Exception{
    RespResult result = RespResult.fail();
    if (CommonUtil.checkPhone(phone) && (pword != null || pword.length() == 32)) {
        // 检查验证码是否正确
        if (loginSmsService.checkSmsCode(phone, scode)) {
            // 查找是否存账号密码匹配用户
            User user = userService.userLogin(phone, pword);
            if (user != null) {
                // 登录成功 , 生成Token
                Map<String ,Object> data = new HashMap<>();
                data.put("uid",user.getId());// 设置荷载位uid
                // 设置120分钟过期,并获取Token
                String jwtToken = jwtUtile.creatJwt(data, 120);

                result = RespResult.ok();
                // 响应Token
                result.setAccessToken(jwtToken);

                Map<String,Object> userInfo = new HashMap<>();
                userInfo.put("uid",user.getId());
                userInfo.put("phone",user.getPhone());
                userInfo.put("name",user.getName());
                result.setData(userInfo);
            }else {
                result.setRCode(RCode.PHONE_LOGIN_PASSWORD_INVALID);
            }
        } else {
            result.setRCode(RCode.SMS_CODE_INVALID);
        }
    } else {
        result.setRCode(RCode.REQUEST_PARAM_ERR);
    }
    return result;
}

拦截器验证Token

public class TokenInterceptor implements HandlerInterceptor {

    private String secret = "";

    public TokenInterceptor(String secret) {
        this.secret = secret;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        // 1 如果是OPTIONS ,放行
        if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
            return true;
        }

        boolean requestSend = false;

        try {
            // 请求头中的 uid
            String headerUid = request.getHeader("uid");
            // 2 获取Token ,验证
            String headToken = request.getHeader("Authorization");
 
            if (StringUtils.isNotBlank(headToken)) {
                // Bearer。。 Authorization中 Authorization: Bearer <token>
                String jwt = headToken.substring(7);
                // 读jwt
                JwtUtile jwtUtile = new JwtUtile(secret);
                Claims claims = jwtUtile.readJWT(jwt);

                // 获取 Jwt中的uid
                Integer jwtUid = claims.get("uid", Integer.class);
                // 对比 headerUid 和 JwtUid
                if (headerUid.equals(String.valueOf(jwtUid))) {
                    // token 和发起请求的用户是同一个, 请求可以被处理
                    requestSend = true;
                }
            }
        } catch (Exception e) {
            requestSend = false;
        }

        // token 没有通过,需要给vue错误提示
        if (requestSend == false) {
            // 返回失败的JSON数据给前端
            RespResult result = RespResult.fail();
            result.setRCode(RCode.TOKEN_INVALID);

            // 使用HTTPServletResponse 输出JSON
            String respJson = JSONObject.toJSONString(result);
            response.setContentType("application/json;charset=utf-8");
            PrintWriter out = response.getWriter();
            out.print(respJson);
            out.flush();
            out.close();
        }

        return requestSend;
    }
}

前端

登录+存储Token

userLogin(){
    this.checkPhone();
    this.checkPassword();
    this.checkCode();
    if( this.phoneErr == '' && this.passwordErr == '' & this.codeErr==''){
        //发起登录请求
        let param = {
            phone:this.phone,pword:md5(this.password),scode:this.code
        }
        doPost('/v1/user/login',param).then(resp=>{
            if( resp && resp.data.code == 1000){
                //登录成功,存储数据到localStorage,存的是字符串
                window.localStorage.setItem("token",resp.data.accessToken);
                //把 json对象转为 string
                window.localStorage.setItem("userinfo", JSON.stringify(resp.data.data));
                //登录之后,如果name没有值,需要进入到实名认证页面
                //如果name有值,进入到用户中心
                if(resp.data.data.name == ''){
                    //实名认证
                    this.$router.push({
                        path:'/page/user/realname'
                    })
                } else {
                    //用户中心
                    this.$router.push({
                        path:'/page/user/usercenter'
                    })
                }
            }
        })
    }
}

前端拦截器

import axios from 'axios'

//创建拦截器
axios.interceptors.request.use(function (config) {

    // 在需要用户登录后的操作,在请求的url中加入token
    // 判断访问服务器的url地址, 需要提供身份信息,加入token
    let storageToken = window.localStorage.getItem("token");
    let userinfo = window.localStorage.getItem("userinfo");
    if (storageToken && userinfo) {
        // 添加需要Token验证的url
        if (config.url == '/v1/user/realname' || config.url == '/v1/user/usercenter' ||
            config.url == '/v1/recharge/records' || config.url=='/v1/invest/product') {
            //在header中传递token 和一个userId
            config.headers['Authorization'] = 'Bearer ' + storageToken;
            config.headers['uid'] = JSON.parse(userinfo).uid;
        }
    }
    return config;
}, function (err) {
    console.log("请求错误" + err);
})

//创建应答拦截器,统一对错误处理, 后端返回 code > 1000 都是错误
axios.interceptors.response.use(function (resp) {
    if (resp && resp.data.code > 1000) {
        let code = resp.data.code;
        if (code == 3000) {
            //token无效,重新登录
            window.location.href = '/page/user/login';
        } else {
            layx.msg(resp.data.msg, {dialogIcon: 'warn', position: 'ct'});
        }
    }
    return resp;
}, function (err) {
    console.log("应答拦截器错误:" + err)
    //回到首页
    window.location.href = '/';
})

其他请求正常请求就行

axios.get("/v1/user/usercenter").then(resp => {
    if (resp && resp.data.code == 1000) {
    	this.userBaseInfo = resp.data.data;
    }
});