什么是JWT
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
为什么使用JWT
- 简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快。
- 自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库。
- 因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。
- 不需要在服务端保存会话信息,特别适用于分布式微服务。
JWT的使用场景
身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
JWT的结构
- 头部(包含令牌的类型与使用的签名算法)
{ "alg": "HS265", "typ": "JWT" }
- 载荷(有关用户实体和其他数据的声明)
{ "name": "admin", "pass": 123 }
- 签证(使用编码后的header和payload以及一个指定密钥,然后使用header中指定的算法(HS265)进行签名.
签名的作用是保证JWT没有被篡改过)
JWT的请求流程
SpringBoot整合JWT
引入依赖
<!--引入jwt--> <dependency> <groupId>com.auth0</groupId> <artifactId>java-jwt</artifactId> <version>3.4.0</version> </dependency> <!--引入mybatis--> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.1.3</version> </dependency> <!--引入mysql--> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.15</version> </dependency>
编写配置文件
server.port=8989 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://8.192.12.37:3306/test?characterEncoding=UTF-8 spring.datasource.username=root spring.datasource.password=root mybatis.type-aliases-package=com.boot.jwt.entity mybatis.mapper-locations=com/boot/jwt/mapper/*.xml logging.level.com.baizhi.dao=debug
token生成与验证工具类
package com.boot.jwt.config; 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.JWTDecodeException; import com.auth0.jwt.exceptions.JWTVerificationException; import com.auth0.jwt.interfaces.DecodedJWT; import com.boot.jwt.entity.User; import java.util.Date; /** * @author laz * @date 2022/09/09 14:55 */ public class TokenUtil { //token到期时间60s private static final long EXPIRE_TIME= 60*1000; //密钥盐 private static final String TOKEN_SECRET="123456qwertyuiop789"; /** * 创建一个token * @param user * @return */ public static String sign(User user){ String token=null; try { Date expireAt=new Date(System.currentTimeMillis()+EXPIRE_TIME); token = JWT.create() //发行人 .withIssuer("auth0") //存放数据 .withClaim("username",user.getUsername()) .withClaim("password",user.getPassword()) //过期时间 .withExpiresAt(expireAt) .sign(Algorithm.HMAC256(TOKEN_SECRET)); } catch (IllegalArgumentException|JWTCreationException je) { } return token; } /** * 对token进行验证 * @param token * @return */ public static Boolean verify(String token){ try { //创建token验证器 JWTVerifier jwtVerifier=JWT.require(Algorithm.HMAC256(TOKEN_SECRET)).withIssuer("auth0").build(); DecodedJWT decodedJWT=jwtVerifier.verify(token); System.out.println("认证通过:"); System.out.println("username: " + TokenUtil.getUserName(token)); System.out.println("过期时间: " + decodedJWT.getExpiresAt()); } catch (IllegalArgumentException |JWTVerificationException e) { //抛出错误即为验证不通过 return false; } return true; } /** * 获取用户名 */ public static String getUserName(String token){ try{ DecodedJWT jwt=JWT.decode(token); return jwt.getClaim("username").asString(); }catch (JWTDecodeException e) { return null; } } }
拦截器拦截token
package com.boot.jwt.config; import com.alibaba.fastjson.JSONObject; import org.springframework.stereotype.Component; import org.springframework.web.servlet.HandlerInterceptor; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; /** * @author laz * @date 2022/09/09 14:56 */ @Component public class TokenInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { //跨域请求会首先发一个option请求,直接返回正常状态并通过拦截器 if(request.getMethod().equals("OPTIONS")){ response.setStatus(HttpServletResponse.SC_OK); return true; } //获取到token String token = request.getHeader("token"); if (token!=null){ boolean result= TokenUtil.verify(token); if (result){ System.out.println("通过拦截器"); return true; } } try { JSONObject json=new JSONObject(); json.put("msg","token verify fail"); json.put("code","500"); response.getWriter().append(json.toString()); System.out.println("认证失败,未通过拦截器"); } catch (Exception e) { return false; } return false; } }
设置拦截规则
package com.boot.jwt.config; 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.ArrayList; import java.util.List; /** * @author laz * @date 2022/09/09 13:56 */ @Configuration public class WebConfiguration implements WebMvcConfigurer { @Autowired private TokenInterceptor tokenInterceptor; /** * 配置拦截器、拦截路径 * 每次请求到拦截的路径,就会去执行拦截器中的方法 * @param */ @Override public void addInterceptors(InterceptorRegistry registry) { List<String> excludePath = new ArrayList<>(); //排除拦截,除了登录,其他都拦截 excludePath.add("/test/login"); registry.addInterceptor(tokenInterceptor) .addPathPatterns("/**") .excludePathPatterns(excludePath); WebMvcConfigurer.super.addInterceptors(registry); } }
建立User实体类
package com.boot.jwt.entity; import lombok.Data; /** * @author laz * @date 2022/09/09 14:54 */ @Data public class User { private String id; private String username; private String password; }
编写mapper和xml文件
package com.boot.jwt.mapper; import com.boot.jwt.entity.User; /** * @author laz * @date 2022/09/09 14:54 */ public interface UserMapper { /** * 登录 * @param user * @return */ User login(User user); }
<?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" > <mapper namespace="com.boot.jwt.mapper.UserMapper"> <select id="login" parameterType="com.boot.jwt.entity.User" resultType="com.boot.jwt.entity.User"> select * from user where username=#{username} and password = #{password} </select> </mapper>
编写Service层接口以及实现类
package com.boot.jwt.service; import com.boot.jwt.entity.User; import com.boot.jwt.utils.LoginDto; /** * @author laz * @date 2022/09/09 14:54 */ public interface IUserService { /** * 登录接口 * @param user * @return */ LoginDto login(User user); }
package com.boot.jwt.service.impl; import com.boot.jwt.config.TokenUtil; import com.boot.jwt.entity.User; import com.boot.jwt.mapper.UserMapper; import com.boot.jwt.service.IUserService; import com.boot.jwt.utils.LoginDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @author laz * @date 2022/09/09 14:54 */ @Service public class IUserServiceImpl implements IUserService { @Autowired private UserMapper userMapper; @Override public LoginDto login(User user) { LoginDto loginDto = new LoginDto(); User login = userMapper.login(user); if (login == null){ loginDto.setCode(400); loginDto.setMsg("账号或密码错误!"); return loginDto; } String token= TokenUtil.sign(login); loginDto.setCode(200); loginDto.setMsg("登录成功!"); loginDto.setUser(login); loginDto.setToken(token); return loginDto; } }
编写controller层测试接口
package com.boot.jwt.controller; import com.boot.jwt.entity.User; import com.boot.jwt.service.IUserService; import com.boot.jwt.utils.LoginDto; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author laz * @date 2022/09/09 14:58 */ @RestController @RequestMapping("/test") public class LoginController { @Autowired private IUserService userService; /** * 登录 * @param user * @return */ @PostMapping("/login") public LoginDto login(@RequestBody User user){ LoginDto login = userService.login(user); return login; } /** * 测试 * @return */ @RequestMapping("/test") public Object test(){ return "访问成功!"; } }
到这里,代码就编写完成了,下面开始测试。
测试JWT
先调用测试接口
可以看到,接口被拦截了。
接下来调用login接口,获取token:
再次调用测试接口,带上token:
可以看到,接口成功访问。