JWT
全称:JSON WEB TOKEN
JWT特点
1、JWT 组成
JWT 由三部分组成,并且各自都是 JSON 格式。
JWT 的样式举例:
xxx
.yyy
.zzz
如:abcdefg . hijklmn . opqrstuvwxyz
中间以点号分隔开来,三部分任一部分都是一个JSON对象编码得到的。
(1)header(头部)
举例中的xxx
header
中,制定了两个部分,其中alg
指定了签名算法(比如HS256),typ
指定了令牌类型(一般为JWT)。
举例:
原内容为:
{
"alg": "HS256",
"typ": "JWT"
}
由Base64
对此部分编码得到xxx
,成为第二部分内容。
(2)payload(有效载荷)
举例中的yyy
payload
中,包含了希望传递的有关用户的一些数据和其它的。例如用户名、权限等级等。
举例:
{
"id": "1234567890",
"name": "John Doe",
"something": "isToken"
}
由Base64
对此部分编码得到yyy
,成为第二部分内容。
注:
因为这部分仅仅是由Base64
编码得出,可以轻易地被客户端解码出内容,所以不能存放密码等隐私数据。
至于用户id、用户名等被获取也无关紧要。
(3)verify signature(验证签名)
举例中的zzz
也叫
signature
(签名),是JWT
真正实现验证机制的部分。
第三部分的zzz
,是根据:
xxx
、yyy
、myownkey(秘钥)
使用这三者,通过header
中指定的签名算法进行加密,再将最终得到的zzz
,作为第三部分。
其中,秘钥
是由我们写的程序内部自己定义的,绝对不能泄露。
怎么使用秘钥
验证?
Token验证的过程
1、浏览器请求登录。
2、服务器接收请求,判断是否携带有token
。
-
有
token
,再重复一次生成signature
的过程。也就是根据
header
、payload
加上秘钥
通过header
中指定的签名算法生成signature
。然后比较当前计算出的
signature
和浏览器提供的signature
是不是一致的,就知道这个token
是不是由自己颁发的。
因为
秘钥
只有服务器自己知道,如果是伪造的、胡拼乱凑的token
,基本上无法命中。
当然,不排除弱密码被暴力破解的可能。
- 无
token
,生成一个token
。
3、返回结果。
JWT 并没那么安全
实际上,JWT 只是 Token 的一种格式,并且提供了一种生成和校验数据的办法。
Token中携带的数据,一个是不能是隐私数据,再一个就是私钥
虽然是由我们自己提供定义,但现在仍存在很多供给破解的手段。
比如私钥比较弱、特殊header
组成、修改playload
中的固定字段值等等。
JWT 的本质应该是校验payload
数据的正确性与真实性,只是我们认为可以通过这种方式来验证登录、做权限管理等功能。
并不能因为使用了加密而掉以轻心,根据系统安全性需求应该考虑到多种方案,例如使用多种秘钥
,建立秘钥文件
等。
JWT 网址
JWT 官网:
https://jwt.io/
JWT类官方github地址:
https://github.com/auth0/java-jwt
在 README.md
中有介绍:
并且提供了 JWT 的Java文档地址:
https://javadoc.io/doc/com.auth0/java-jwt/latest/index.html
使用SpringBoot创建Token
先引入JWT依赖到SpringBoot 项目中:
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>4.2.1</version>
</dependency>
SpringBoot使用的配置文件:
# Spirng
server:
port: 7788
# 配置MySQL数据源
spring:
datasource:
url: jdbc:mysql://192.168.253.10:3306/cat-im?useUnicode=true&characterEncoding=utf8&autoReconnect=true
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: root
# mybatis-plus
mybatis-plus:
mapperPackage: com.cat.auth.mapper # Mapper 所在包路径
mapperLocations: classpath*:mapper/*Mapper.xml # XML 文件位置
typeAliasesPackage: com.cat.auth.entity # 实体类路径,多个包之间用逗号或者分号分隔
configuration:
mapUnderscoreToCamelCase: true # 驼峰处理
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
使用JWT 进行登录验证
先上项目结构:
JWTUtils
定义一个JWT工具,将私钥
藏在内部,仅对外提供创建和解码 Token 的方法即可。
/**JWT工具类**/
public class JWTUtils {
/**
* 秘钥作为私有的静态变量,只能暴露给工具类内部的方法用。
**/
private static String SECRET = "token@#¥%……¥@cat!!!";
/**
* 创建Token
*/
public static String getToken(Map<String, String> map) {
//新建一个 JWT 生成器
JWTCreator.Builder builder = JWT.create();
// 添加 payload
map.forEach((k, v) -> {
builder.withClaim(k, v);
});
//默认7天过期
Calendar outDate = Calendar.getInstance();
outDate.add(Calendar.DATE, 7);// 当前时间加7天,作为过期时间点
builder.withExpiresAt(outDate.getTime()); //设置过期时间
//只需要告知生成器应该使用什么算法、私钥是多少,header中的 typ会自动添加设置为JWT,
String token = builder.sign(Algorithm.HMAC256(SECRET));//签名
return token;
}
/**
* 验证token
*/
public static DecodedJWT verify(String token) {
return JWT.require(Algorithm.HMAC256(SECRET)).build().verify(token);
}
}
因为是JSON 对象,所以添加的时候就跟map一样,使用的是key-value 的形式。
JWT 的Builder
中:
-
withClaim(k,v)
是添加一个名称为k
,值为v
的数据到payload
中。v可以是以下几种数据类型:
- Boolean
- Integer
- Long
- String
- Double
- Instant
- List <?>
- Map <String,?>
-
如果要添加数组,有单独的方法:
withArrayClaim
添加一个名称为k
,值则是支持存入 Integer、Long、String三种数组。 -
JWT 的token里定义了几种固定的属性名,并且提供了相应的方法设置:
- kid (
秘钥
的id。因为Java程序中只使用一个秘钥
加密可能不太安全,可以制造一个秘钥
文件,根据秘钥
的id用来标识应该使用哪一个秘钥
进行生成令牌和验证令牌的操作。)
添加方法:withKeyId(String kid)
- issur(令牌的颁发者是谁?)
……
有时间再写……
- kid (
JWTInterceptor:
添加一个JWTToken
拦截器,定义拦截规则。
目的就是让请求在到达控制层(Controller)之前,没有携带正确 Token 的请求都回绝掉,返回false
及拒绝原因等一些其它信息给前端。
前端收到拒绝的信息后,再根据拒绝原因可以跳转到对应的登录页面。
重定向等操作交由前端。
(我没有写页面……)
/**
* 自定义拦截器
**/
@Slf4j
public class JWTInterceptor implements HandlerInterceptor {
/**
* 在业务处理器(Controller层)处理请求之前被调用
**/
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object handler) throws Exception {
//获取请求头中的令牌
String token = request.getHeader("token");
log.info("当前token为:{}", token);
Map<String, Object> map = new HashMap<>();
try {
if (token != null) {
JWTUtils.verify(token);
return true;
}else {
map.put("msg", "未携带token");
}
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg", "签名不一致");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg", "令牌过期");
} catch (AlgorithmMismatchException e) {
e.printStackTrace();
map.put("msg", "算法不匹配");
} catch (InvalidClaimException e) {
e.printStackTrace();
map.put("msg", "失效的payload");
} catch (Exception e) {
e.printStackTrace();
map.put("msg", "token无效");
}
map.put("state", false);
//响应到前台: 将map转为json
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
InterceptorConfig
制定好拦截器与令牌的生成与验证工具,还需要将拦截器添加到web中去,才能工作。
/**
* 自定义Web配置
* 可添加自定义拦截器、路由等……
**/
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
/**
* 添加拦截器
**/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor()) // 添加自定义的JWT拦截器
.addPathPatterns("/user/needJWTToken") //规定拦截器对指定的路径过滤
.excludePathPatterns("/user/login") // 设置不需要拦截的过滤规则
;
}
}
UserController
UserController
提供两个路径接口:
- jwtLogin 提供登录验证、颁发令牌。
- needJWTToken 则是一个需要正确令牌才能访问的路径接口。
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@RequestMapping(value = "/user/jwtLogin", method = RequestMethod.POST,consumes = "application/json")
public Map<String, Object> jwtLogin(@RequestBody User user) {
log.info("user_id:{}", user.getName());
log.info("password: {}", user.getPassword());
Map<String, Object> map = new HashMap<>();
User userDB = userService.login(user);
if (userDB != null) {
Map<String, String> payload = new HashMap<>();
payload.put("user_id", userDB.getId().toString());
payload.put("user_name", userDB.getName());
String token = JWTUtils.getToken(payload);
map.put("state", true);
map.put("msg", "登录成功");
map.put("token", token);
return map;
} else {
map.put("state", false);
map.put("msg", "登录失败,用户名或密码错误!");
map.put("token", "");
}
return map;
}
@RequestMapping(value = "/user/needJWTToken", method = RequestMethod.POST)
public Map<String, Object> needJWTToken(HttpServletRequest request) {
String token = request.getHeader("token");
DecodedJWT verify = JWTUtils.verify(token);
String id = verify.getClaim("id").asString();
String name = verify.getClaim("name").asString();
log.info("user_id:{}", id);
log.info("user_name: {}", name);
//TODO 业务逻辑
Map<String, Object> map = new HashMap<>();
map.put("state", true);
map.put("msg", "请求成功");
return map;
}
}
UserMapper
UserMapper 继承了BaseMapper,实现基础的crud操作。
为Service层实现具体的业务逻辑提供数据库操作。
@Mapper
public interface UserMapper extends BaseMapper<User> {
/**查询User**/
User selectUser(User user) ;
}
<select id="selectUser" parameterType="User" resultType="User">
select id,name,password from user where name =#{name} and password=#{password}
</select>
UserService
在 Service层面,接口中依旧需要自定义方法
,由实现类去实现。
因为方法中需要加入自己需要的判断或者业务层面的一些东西。
public interface UserService extends IService<User> {
User login(User user);
}
UserServiceImpl
service实现类中:
extends ServiceImpl<UserMapper, User>
能将UserMapper
注入到内部使用,然后我们在方法里进行判断。
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService{
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
return baseMapper.selectUser(user);
}
}
启动类
@SpringBootApplication
@ComponentScan(basePackages = {"com.cat.auth.*"})
@MapperScan("com.cat.auth.mapper")
public class AuthApplication {
public static void main(String[] args) {
SpringApplication.run(AuthApplication.class, args);
}
}
JWT Token 验证效果
数据库中新建用户:
用户名:admin
密码:admin123
(1)登录时密码错误,不颁发令牌
(2)用户名密码正确,颁发令牌
(3)访问需要权限的页面,没有携带Token
告知前端请求失败,并告诉失败原因。
(实际上,前端也需要定义一个拦截器,对于接收response,一旦收到约定好的错误状态及错误原因,知道是没有携带 Token就跳转到登录页面去。)
(4)访问需要权限的页面,携带错误的令牌
(5)访问需要权限的页面,携带正确的令牌
学习
学习自B站 尚硅谷、编程不良人的视频。
可以参考 jwt-java相关文档,下载下来用浏览器打开。
英文不好直接一键翻译就好。