【JavaEE进阶】用户登录场景中的令牌技术(JWT令牌)

目录

1.问题场景

2. 令牌技术

3.JWT令牌

3.1 介绍

3.2 JWT组成

4. 实际使用示例

4.1 服务器代码示例

4.2 客户端代码示例

4.3 服务器拦截器示例


1.问题场景

在我们开发网站实现用户登录的过程中, 需要将用户的一些信息存储在服务器中.

传统思路:

  • 登陆页面把用户名密码提交给服务器.
  • 服务器端验证用户名密码是否正确,并返回校验结果给后端
  • 如果密码正确,则在服务器端创建Session .通过Cookie把sessionld返回给浏览器

但是这种方式存在一些问题: 集群环境下无法直接使用Session. 以及当服务器重启后, 保存的信息会丢失

原因分析:

我们开发的项目,在企业中很少会部署在一台机器上,容易发生单点故障.(单点故障: 一旦这台服务器挂了,整个应用都没法访问了). 所以通常情况下,一个Web应用会部署在多个服务器上,通过Nginx等进行负载均衡.  此时,来自一个用户的请求就会被分发到不同的服务器上.

假如我们使用Session进行会话跟踪,我们来思考如下场景:

1. 用户登录 用户登录请求, 经过负载均衡, 把请求转给了第一台服务器, 第一台服务器进行账号密码
验证, 验证成功后,把Session存在了第一台服务器上.

2. 查询操作用户 登录成功之后,携带Cookie(里面有SessionId)继续执行查询操作, 比如查询博客列
表. 此时请求转发到了第二台机器, 第二台机器会先进行权限验证操作 (通过Sessionld验证用户是否
登录),  此时第二台机器上没有该用户的Session, 就会出现问题, 提示用户登录, 这是用户不能忍受的.

接下来我们介绍一种解决方案: 令牌技术

2. 令牌技术

令牌其实就是一个用户身份的标识,名称起的很高大上, 其实本质就是一个字符串.

比如我们出行在外,会带着自己的身份证,需要验证身份时,就掏出身份证
身份证不能伪造,可以辨别真假.

服务器具备生成令牌和验证令牌的能力

我们使用令牌技术, 继续思考上述场景:

1.用户登录用户登录请求, 经过负载均衡, 把请求转给了第一台服务器, 第一台服务器进行账号密码
验证,验证成功后,生成一个令牌, 并返回给客户端. 

2.客户端收到令牌之后,把令牌存储起来. 可以存储在Cookie中,也可以存储在其他的存储空间 (比如localStorage)

3.查询操作用户登录成功之后, 携带令牌继续执行查询操作, 比如查询博客列表. 此时请求转发到了
第二台机器, 第二台机器会先进行权限验证操作. 服务器验证令牌是否有效, 如果有效, 就说明用户已经执行了登录操作, 如果令牌是无效的, 就说明用户之前未执行登录操作.
 

令牌的优缺点

优点:

解决了集群环境下的认证问题

减轻服务器的存储压力(无需在服务器端存储)

缺点:

需要自己实现(包括令牌的生成,令牌的传递, 令牌的校验)

当前企业开发中,解决会话跟踪使用最多的方案就是令牌技术.

3.JWT令牌

令牌本质就是一个字符串,他的实现方式有很多,我们采用一个JWT令牌来实现.

3.1 介绍

JWT全称: JSON Web Token
官网: https://jwt.io/ 

JSON Web Token(JWT) 是一个开放的行业标准(RFC 7519),  用于客户端和服务器之间传递安全可靠的信息.

其本质是一个token, 是一种紧凑的URL安全方法.
 

3.2 JWT组成

JWT由三部分组成, 每部分中间使用点 (.) 分隔,比如: aaaa.bbbb.cccc

Header(头部) 头部包括令牌的类型(即JWT) 及使用的哈希算法 (如HMAC SHA256或RSA)

Payload(负载) 负载部分是存放有效信息的地方, 里面是一些自定义内容. 比如:
{"userId":"123","userName":" zhangsan"'}, 也可以存在 jwt 提供的现场字段, 比如exp(过期时间戳)等.

此部分不建议存放敏感信息,因为此部分可以解码还原原始内容.

Signature(签名) 此部分用于防止 jwt 内容被篡改, 确保安全性.

防止被篡改,而不是防止被解析.
JWT之所以安全,就是因为最后的签名. jwt当中任何一个字符被篡改, 整个令牌都会校验失败.
就好比我们的身份证, 之所以能标识一个人的身份, 是因为他不能被篡改, 而不是因为内容加密.(任何人都可以看到身份证的信息, jwt也是)

对上面部分的信息,使用Base64Url进行编码, 合并在一起就是jwt令牌
Base64是编码方式,而不是加密方式

1.  引⼊JWT令牌的依赖

 <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>0.11.5</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is
preferred -->
            <version>0.11.5</version>
            <scope>runtime</scope>
        </dependency>

2. 使用Jar包中提供的API来完成JWT令牌的生成和校验

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;
import org.junit.Test;
import javax.crypto.SecretKey;
import java.security.Key;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

public class JwtUtilsTest {
    //设置过期时间1小时
    private static final long JWT_EXPIRATION = 60 * 60 * 1000;
    //密钥  长度有要求
    private static final String secretStr = "CIw6DzttvHA+XnrTa2B1EMhLoai1R0vC6jr0Q6y/qsU=";
    //生成 key
    private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));

    @Test
    public void genJwt() {
        Map<String,Object> claim = new HashMap<>();
        claim.put("id", 2);
        claim.put("name","zhangsan");
        String token = Jwts.builder()
                .setClaims(claim) //自定义内容(负载)
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))   //设置过期时间
                .signWith(key) //设置签名
                .compact();

        //将生成的 token 打印出来
        System.out.println(token);
    }

    //生成密钥
    @Test
    public void genKey() {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        String str = Encoders.BASE64.encode(secretKey.getEncoded());
        System.out.println(str);
    }
}

注意:对于密钥有长度和内容有要求, 建议使用
io.jsonwebtoken.security.Keys#secretKeyFor(signaturealgalgorithm) 方法来创建一个密钥 
 

    //生成密钥
    @Test
    public void genKey() {
        SecretKey secretKey = Keys.secretKeyFor(SignatureAlgorithm.HS256);
        String str = Encoders.BASE64.encode(secretKey.getEncoded());
        System.out.println(str);
    }

运行程序:

输出的内容, 就是JWT令牌.
通过点(.)对三个部分进行分割, 我们把生成的令牌通过官网进行解析, 就可以看到我们存储的信息了


1. HEADER部分可以看到,使用的算法为HS256.

2. PAYLOAD部分是我们自定义的内容, exp表示过期时间

3. VERIFY SIGNATURE部分是经过签名算法计算出来的, 所以不会解析
 

校验令牌

完成了令牌的生成,我们需要根据令牌,来校验令牌的合法性(以防客户端伪造)

    @Test
    public void parseToken() {
        String token = "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoiemhhbmdzYW4iLCJpZCI6MiwiZXhwIjoxNzIzMTk1Njc0fQ.xdsDIsJeR24S3XnS16iLA8Jd5hQGK9DPWJ9OuevM3LE";
        JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
        Claims claims = build.parseClaimsJws(token).getBody();
        System.out.println(claims);
    }

 运行结果:

令牌解析后, 我们可以看到里面存储的信息,如果在解析的过程当中没有报错,就说明解析成功了

令牌解析时, 也会进行时间有效性的校验, 如果令牌过期了, 解析也会失败.

修改令牌中的任何一个字符, 都会校验失败, 所以令牌无法篡改.

4. 实际使用示例

学习令牌的使用之后,接下来我们通过令牌来完成用户的登录

1.登陆页面把用户名密码提交给服务器.

2.服务器端验证用户名密码是否正确,如果正确,服务器生成令牌, 下发给客户端.

3.客户端把令牌存储起来(此如Cookie, local storage等), 后续请求时, 把token发给服务器 

4.服务器对令牌进行校验, 如果令牌正确,进行下一步操作
 



约定前后端交互接口

4.1 服务器代码示例

创建JWT工具类:

import com.example.blog.constant.Constants;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import java.security.Key;
import java.util.Date;
import java.util.Map;

@Slf4j
public class JwtUtils {
    //设置过期时间1小时
    private static final long JWT_EXPIRATION = 60 * 60 * 1000;
    //密钥  长度有要求
    private static final String secretStr = "CIw6DzttvHA+XnrTa2B1EMhLoai1R0vC6jr0Q6y/qsU=";
    //生成 key
    private static final Key key = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretStr));

    /**
     * 生成token
     */
    public static String genJwtToken(Map<String, Object> claim) {
        //签名算法
        String token = Jwts.builder()
                .setClaims(claim) //自定义内容(负载)
                .setExpiration(new Date(System.currentTimeMillis() + JWT_EXPIRATION))   //设置过期时间
                .signWith(key) //设置签名
                .compact();

        //将生成的 token 返回
        return token;
    }

    /**
     * 校验token
     * Claims 为null: 表示校验失败
     */
    public static Claims parseToken(String token) {
        //创建解析器, 设置签名密钥
        JwtParser build = Jwts.parserBuilder().setSigningKey(key).build();
        Claims claims = null;
        try {
            //解析token
            claims = build.parseClaimsJws(token).getBody();
        } catch (Exception e) {
            //签名验证失败
            log.error("解析token失败, token:{}", token);
            return null;
        }
        return claims;
    }

    /**
     * 从token中获取用户id
     * @param token
     * @return
     */
    public static Integer getIdByToken(String token) {
        Claims claims = parseToken(token);
        if (claims != null) {
            Integer userId  = (Integer) claims.get(Constants.TOKEN_ID);
            if(userId > 0) {
                return userId;
            }
        }
        return null;
    }
}

创建UserController:

@RequestMapping("/user")
@RestController
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/login")
    public Result login(String username, String password) {
        log.info("用户登录: userName:" + username + " , password:" + password);
        //1.参数校验
        //2.校验密码是否正确
        //3.密码正确, 返回token
        //4.密码错误, 返回错误信息
        if(!StringUtils.hasLength(username) || !StringUtils.hasLength(password)) {
            log.error("账号或密码不能为空");
            return Result.fail("账号或密码不能为空!");
        }
        //从数据库中查找用户
        UserInfo userInfo = userService.selectByName(username);
        if(userInfo == null) {
            log.error("用户不存在");
            return Result.fail("用户不存在");
        }
        if(!password.equals(userInfo.getPassword())) {
            log.error("密码错误");
            return Result.fail("密码错误!");
        }
        //密码正确, 返回token
        Map<String, Object> claim = new HashMap<>();
        claim.put(Constants.TOKEN_ID,userInfo.getId());
        claim.put(Constants.TOKEN_USERNAME, userInfo.getUserName());
        String token = JwtUtils.genJwtToken(claim);
        log.info("用户登录成功, token:{}", token);
        return Result.success(token);
    }

    /**
     * 获取当前登录用户的信息
     * @return
     */
    @RequestMapping("/getUserInfo")
    public UserInfo getLoginUserInfo(HttpServletRequest request) {
        //获取token
        String token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);
        //从 token 中获取登录用户 id
        Integer userId = JwtUtils.getIdByToken(token);
        if(userId == null) {
            //用户未登录
            return null;
        }
        UserInfo userInfo = userService.selectById(userId);
        return userInfo;
    }
}

4.2 客户端代码示例

前端收到token之后,保存在localstorage中

function login() {
    $.ajax({
        type:'post',
        url:'user/login',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        success: function(result) {
            if(result.code == 'SUCCESS' && result.data!=null) {
                //后端处理成功, 将token存储起来
                localStorage.setItem("user_token", result.data);
                //页面跳转
                location.assign("blog_list.html");
            }else {
                alert(result.errMsg);
            }
        }
    });
}

local storage相关操作

1.存储数据

localStorage.setItem("user_token","value");

2.读取数据

localStorage.getItem("user_token");

3.删除数据

localStorage.removeItem("user_token");

前端请求时,header 中统一添加 token, 可以写在common.js中 :

// header中统⼀添加token
$(document).ajaxSend(function(e,xhr,opt) {
    var token = localStorage.getItem("user_token");
    xhr.setRequestHeader("user_token_header", token);
});

4.3 服务器拦截器示例

实现强制要求登陆

当用户访问前端页面时,如果用户当前尚未登陆, 就自动跳转到登陆页面.

我们可以采用拦截器来完成,token通常由前端放在header中,我们从header中获取token,并校验
token是否合法

import com.example.blog.constant.Constants;
import com.example.blog.utils.JwtUtils;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Slf4j
@Component
public class LoginInterceptor implements HandlerInterceptor {
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //获取handler
        String token = request.getHeader(Constants.REQUEST_HEADER_TOKEN);
        log.info("从header中获取token:{}", token);
        Claims claims = JwtUtils.parseToken(token);
        if(claims == null) {
            //校验失败
            response.setStatus(401);
            return false;
        }
        return true;
    }
}
import com.example.blog.interceptor.LoginInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import java.util.Arrays;
import java.util.List;
//拦截器配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private LoginInterceptor loginInterceptor;

    private final List excludes = Arrays.asList(
            "/**/*.html",
            "/blog-editormd/**",
            "/css/**",
            "/js/**",
            "/pic/**",
            "/login",
            "/user/login",
            "/favicon.ico"
    );

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(loginInterceptor)
                .addPathPatterns("/**")
                .excludePathPatterns(excludes);
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

夏微凉.

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值