【java苍穹外卖项目实战四】JWT令牌技术(完善登录功能) 您所在的位置:网站首页 java电商项目用什么技术 【java苍穹外卖项目实战四】JWT令牌技术(完善登录功能)

【java苍穹外卖项目实战四】JWT令牌技术(完善登录功能)

2024-07-13 11:57| 来源: 网络整理| 查看: 265

文章目录 1、JWT介绍2、JWT简单实现3、登录下发令牌4、登录功能实现4、密码加密

1、JWT介绍

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

定义了一种简洁的、自包含的格式,用于在通信双方以json数据格式安全的传输信息。由于数字签名的存在,这些信息是可靠的。

简洁:是指jwt就是一个简单的字符串。可以在请求参数或者是请求头当中直接传递。

自包含:指的是jwt令牌,看似是一个随机的字符串,但是我们是可以根据自身的需求在jwt令牌中存储自定义的数据内容。如:可以直接在jwt令牌中存储用户的相关信息。

简单来讲,jwt就是将原始的json数据格式进行了安全的封装,这样就可以直接基于jwt在通信双方安全的进行信息传输了

JWT的组成: (JWT令牌由三个部分组成,三个部分之间使用英文的点来分割)

第一部分:Header(头), 记录令牌类型、签名算法等。 例如:{“alg”:“HS256”,“type”:“JWT”}

第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。 例如:{“id”:“1”,“username”:“Tom”}

第三部分:Signature(签名),防止Token被篡改、确保安全性。将header、payload,并加入指定秘钥,通过指定签名算法计算而来。

签名的目的就是为了防jwt令牌被篡改,而正是因为jwt令牌最后一个部分数字签名的存在,所以整个jwt 令牌是非常安全可靠的。一旦jwt令牌当中任何一个部分、任何一个字符被篡改了,整个令牌在校验的时候都会失败,所以它是非常安全可靠的。

image-20230106085442076

JWT是如何将原始的JSON格式数据,转变为字符串的呢?

其实在生成JWT令牌时,会对JSON格式的数据进行一次编码:进行base64编码

Base64:是一种基于64个可打印的字符来表示二进制数据的编码方式。既然能编码,那也就意味着也能解码。所使用的64个字符分别是A到Z、a到z、 0- 9,一个加号,一个斜杠,加起来就是64个字符。任何数据经过base64编码之后,最终就会通过这64个字符来表示。当然还有一个符号,那就是等号。等号它是一个补位的符号

需要注意的是Base64是编码方式,而不是加密方式。

image-20230112114319773

JWT令牌最典型的应用场景就是登录认证:

在浏览器发起请求来执行登录操作,此时会访问登录的接口,如果登录成功之后,我们需要生成一个jwt令牌,将生成的 jwt令牌返回给前端。前端拿到jwt令牌之后,会将jwt令牌存储起来。在后续的每一次请求中都会将jwt令牌携带到服务端。服务端统一拦截请求之后,先来判断一下这次请求有没有把令牌带过来,如果没有带过来,直接拒绝访问,如果带过来了,还要校验一下令牌是否是有效。如果有效,就直接放行进行请求的处理。

在JWT登录认证的场景中我们发现,整个流程当中涉及到两步操作:

在登录成功之后,要生成令牌。每一次请求当中,要接收令牌并对令牌进行校验。 2、JWT简单实现

首先我们先来实现JWT令牌的生成。要想使用JWT令牌,需要先引入JWT的依赖:

io.jsonwebtoken jjwt 0.9.1

在引入完JWT来赖后,就可以调用工具包中提供的API来完成JWT令牌的生成和校验

工具类:Jwts

生成JWT代码实现:

@Test public void genJwt(){ Map claims = new HashMap(); claims.put("id",1); claims.put("username","Tom"); String jwt = Jwts.builder() .setClaims(claims) //自定义内容(载荷) .signWith(SignatureAlgorithm.HS256, "xiaolin") //签名算法 .setExpiration(new Date(System.currentTimeMillis() + 24*3600*1000)) //有效期 .compact(); System.out.println(jwt); }

运行测试方法:

eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk

实现了JWT令牌的生成,下面我们接着使用Java代码来校验JWT令牌(解析生成的令牌):

@Test public void parseJwt(){ Claims claims = Jwts.parser() .setSigningKey("xiaolin")//指定签名密钥(必须保证和生成令牌时使用相同的签名密钥) .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjcyNzI5NzMwfQ.fHi0Ub8npbyt71UqLXDdLyipptLgxBUg_mSuGJtXtBk") .getBody(); System.out.println(claims); }

运行测试方法:

{id=1, exp=1672729730}

令牌解析后,我们可以看到id和过期时间,如果在解析的过程当中没有报错,就说明解析成功了。

下面我们做一个测试:把令牌header中的数字9变为8,运行测试方法后发现报错:

原header: eyJhbGciOiJIUzI1NiJ9

修改为: eyJhbGciOiJIUzI1NiJ8

image-20230106205045658

结论:篡改令牌中的任何一个字符,在对令牌进行解析时都会报错,所以JWT令牌是非常安全可靠的。

我们继续测试:修改生成令牌的时指定的过期时间,修改为1分钟

@Test public void genJwt(){ Map claims = new HashMap(); claims.put(“id”,1); claims.put(“username”,“Tom”); String jwt = Jwts.builder() .setClaims(claims) //自定义内容(载荷) .signWith(SignatureAlgorithm.HS256, “itheima”) //签名算法 .setExpiration(new Date(System.currentTimeMillis() + 60*1000)) //有效期60秒 .compact(); System.out.println(jwt); //输出结果:eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro } @Test public void parseJwt(){ Claims claims = Jwts.parser() .setSigningKey("itheima")//指定签名密钥 .parseClaimsJws("eyJhbGciOiJIUzI1NiJ9.eyJpZCI6MSwiZXhwIjoxNjczMDA5NzU0fQ.RcVIR65AkGiax-ID6FjW60eLFH3tPTKdoK7UtE4A1ro") .getBody(); System.out.println(claims); }

等待1分钟之后运行测试方法发现也报错了,说明:JWT令牌过期后,令牌就失效了,解析的为非法令牌。

通过以上测试,我们在使用JWT令牌时需要注意:

JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的。如果JWT令牌解析校验时报错,则说明 JWT令牌被篡改 或 失效了,令牌非法。 3、登录下发令牌

接下来我们就需要在案例当中通过JWT令牌技术来跟踪会话。具体的思路我们前面已经分析过了,主要就是两步操作:

生成令牌 在登录成功之后来生成一个JWT令牌,并且把这个令牌直接返回给前端 校验令牌 拦截前端请求,从请求中获取到令牌,对令牌进行解析校验

JWT令牌怎么返回给前端呢?此时我们就需要再来看一下接口文档当中关于登录接口的描述(主要看响应数据):

响应数据

参数格式:application/json

参数说明:

名称类型是否必须默认值备注其他信息codenumber必须响应码, 1 成功 ; 0 失败msgstring非必须提示信息datastring必须返回的数据 , jwt令牌

响应数据样例:

{ "code": 1, "msg": "success", "data": "eyJhbGciOiJIUzI1NiJ9.eyJuYW1lIjoi6YeR5bq4IiwiaWQiOjEsInVzZXJuYW1lIjoiamlueW9uZyIsImV4cCI6MTY2MjIwNzA0OH0.KkUc_CXJZJ8Dd063eImx4H9Ojfrr6XMJ-yVzaWCVZCo" } 4、登录功能实现

JWT工具类介绍(已存在)

package com.sky.utils; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import java.nio.charset.StandardCharsets; import java.util.Date; import java.util.Map; public class JwtUtil { /** * 生成jwt * 使用Hs256算法, 私匙使用固定秘钥 * * @param secretKey jwt秘钥 * @param ttlMillis jwt过期时间(毫秒) * @param claims 设置的信息 * @return */ public static String createJWT(String secretKey, long ttlMillis, Map claims) { // 指定签名的时候使用的签名算法,也就是header那部分 SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // 生成JWT的时间 long expMillis = System.currentTimeMillis() + ttlMillis; Date exp = new Date(expMillis); // 设置jwt的body JwtBuilder builder = Jwts.builder() // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的 .setClaims(claims) // 设置签名使用的签名算法和签名使用的秘钥 .signWith(signatureAlgorithm, secretKey.getBytes(StandardCharsets.UTF_8)) // 设置过期时间 .setExpiration(exp); return builder.compact(); } /** * Token解密 * * @param secretKey jwt秘钥 此秘钥一定要保留好在服务端, 不能暴露出去, 否则sign就可以被伪造, 如果对接多个客户端建议改造成多个 * @param token 加密后的token * @return */ public static Claims parseJWT(String secretKey, String token) { // 得到DefaultJwtParser Claims claims = Jwts.parser() // 设置签名的秘钥 .setSigningKey(secretKey.getBytes(StandardCharsets.UTF_8)) // 设置需要解析的jwt .parseClaimsJws(token).getBody(); return claims; } }

登录成功,生成JWT令牌并返回(EmployeeController需要编写)

@ApiOperation("登录") @PostMapping("/login") public Result login(@RequestBody EmployeeLoginDTO employeeLoginDTO) { log.info("员工登录:{}", employeeLoginDTO); Employee employee = employeeService.login(employeeLoginDTO); //登录成功后,生成jwt令牌 Map claims = new HashMap(); claims.put(JwtClaimsConstant.EMP_ID, employee.getId()); String token = JwtUtil.createJWT( jwtProperties.getAdminSecretKey(), jwtProperties.getAdminTtl(), claims); EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder() .id(employee.getId()) .userName(employee.getUsername()) .name(employee.getName()) .token(token) .build(); return Result.success(employeeLoginVO); }

拦截器,登录校验解析JWT(JwtTokenAdminInterceptor需要编写)

package com.sky.interceptor; import com.sky.constant.JwtClaimsConstant; import com.sky.context.BaseContext; import com.sky.properties.JwtProperties; import com.sky.utils.JwtUtil; import io.jsonwebtoken.Claims; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.web.method.HandlerMethod; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * jwt令牌校验的拦截器 */ @Component @Slf4j public class JwtTokenAdminInterceptor implements HandlerInterceptor { @Autowired private JwtProperties jwtProperties; /** * 校验jwt * * @param request * @param response * @param handler * @return * @throws Exception */ public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //判断当前拦截到的是Controller的方法还是其他资源 if (!(handler instanceof HandlerMethod)) { //当前拦截到的不是动态方法,直接放行 return true; } //1、从请求头中获取令牌 String token = request.getHeader(jwtProperties.getAdminTokenName()); //2、校验令牌 try { log.info("jwt校验:{}", token); Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token); Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString()); log.info("当前员工id:", empId); //3、通过,放行 return true; } catch (Exception ex) { //4、不通过,响应401状态码 response.setStatus(401); return false; } } } 4、密码加密

**问题:**员工表中的密码是明文存储,安全性太低。

image-20221107160529803

解决思路:

将密码加密后存储,提高安全性

image-20221107161918913

使用MD5加密方式对明文密码加密

image-20221107160739680

实现步骤:

修改数据库中明文密码,改为MD5加密后的密文

打开employee表,修改密码

image-20221107161446710

修改Java代码,前端提交的密码进行MD5加密后再跟数据库中密码比对

打开EmployeeServiceImpl.java,修改比对密码

/** * 员工登录 * * @param employeeLoginDTO * @return */ public Employee login(EmployeeLoginDTO employeeLoginDTO) { //1、根据用户名查询数据库中的数据 //2、处理各种异常情况(用户名不存在、密码不对、账号被锁定) //....... //密码比对 // TODO 后期需要进行md5加密,然后再进行比对 password = DigestUtils.md5DigestAsHex(password.getBytes()); if (!password.equals(employee.getPassword())) { //密码错误 throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR); } //........ //3、返回实体对象 return employee; }


【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有