简要
JWT是"JSON Web Token"的缩写,是一种用于在不同系统之间传输信息的开放标准。它通过将信息进行加密后生成一个安全的令牌,以便在网络请求中进行身份验证和授权。
具体来说,JWT可以用于以下几个方面:
-
身份验证:当用户登录系统时,服务器会生成一个JWT并将其发送给客户端。客户端在后续的请求中携带JWT作为身份凭证,服务器可以根据JWT验证用户的身份信息,从而实现无状态的身份验证。
-
授权:JWT中可以包含一些声明信息,如用户角色、权限等。服务器可以根据这些信息决定用户是否具有访问某些资源或执行某些操作的权限。
-
单点登录:JWT可以跨多个系统进行认证和授权,用户只需要登录一次,即可在不同系统中使用同一个JWT进行访问控制,简化了用户的登录流程。
-
信息交换:JWT中的信息可以被加密和签名,确保信息在传输过程中不被篡改。各个系统可以通过验证JWT的签名来确认信息的完整性和真实性。
总之,JWT提供了一种安全、可靠且灵活的方式,用于在不同系统之间进行身份验证和授权,使得系统的集成和数据的传输更加便捷和可信。
jwt 组成结构
JWT由三部分组成,它们分别是头部(Header)、载荷(Payload)和签名(Signature)。
-
头部(Header):头部通常由两部分信息组成,它们是令牌的类型(即"JWT")和所使用的加密算法。这些信息以JSON格式表示,并进行Base64编码后放置在JWT的第一部分。
-
载荷(Payload):载荷是JWT的第二部分,它包含了一些声明性质的信息。这些信息可以是标准的声明(例如发行人、过期时间、主题等),也可以是自定义的声明(根据具体应用的需要)。载荷也是以JSON格式表示,并进行Base64编码后放置在JWT的第二部分。
-
签名(Signature):签名是JWT的第三部分,它由头部、载荷和一个私钥(或者称为密钥)进行加密生成。签名的目的是验证JWT是否被篡改过。接收JWT的服务端会使用相同的私钥解密签名,并与接收到的头部和载荷计算出来的签名进行比对,从而确认JWT的完整性和真实性。
JWT的结构示例如下:
xxxxx.yyyyy.zzzzz
其中,xxxxx
代表经过Base64编码的头部信息,yyyyy
代表经过Base64编码的载荷信息,zzzzz
代表签名。
通过这种组成结构,JWT实现了在不同系统间传输认证和授权信息的安全性和可靠性。
所以令牌组成为三部分,就是头部header,载荷 payload,签名 signature
eg:header
{
“alg”: “HS256”, //签名算法
“typ”: “JWT” //jwt令牌
}
payload
{
“sub”: “1234567890”, //这里载荷可以自定义一些不敏感的信息
“name”: “John Doe”,
“admin”: true
}
signature
HMACSHA256(base64UrlEncode(header) + “.” + base64UrlEncode(payload),secret);
用头部指定的签名算法对经过base64编码的头部和base64编码的载荷以及提供的一个秘钥进行加密组成签名。
JWT token 生成与解析
@SpringBootTest
class AuthJwtApplicationTests {
//生成token
@Test
String buildToken() {
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,90);
String token = JWT.create()
.withClaim("username","lll")
.withClaim("id",1) //设置负载
.withExpiresAt(instance.getTime()) //设置过期时间
.sign(Algorithm.HMAC256("jgdabc"));//指定签名算法和签名所用秘钥串
System.out.println(token);
return token;
}
// token验证
@Test
void VerifyToken()
{
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("jgdabc")).build();
DecodedJWT decodedJWT = jwtVerifier.verify(buildToken());
String username = decodedJWT.getClaim("username").asString();//获取负载
System.out.println(username);
}
}
测试运行第二个人测试verifyToken。
第二个测试代码调用了第一个测试的代码方法。
我们可以看到生成的token串一共是三部分,前两部分是头部和负载,都是采用base64编码1,这两部分可以经过base64很轻松的解码获得信息,后一部分是签名,这一部分内容是十分关键的,我们指定的秘钥是十分关键的,秘钥一定不能让别人知道。
这是jwt简单的token生成和token解析的过程。
封装工具类方法
考虑前后端分离的情况下,我们会使用到它,所以我们将jwt的一些使用封装,然后进行调用
public class JWTUtils {
private static String masterKey = "jgdabc";//指定秘钥
/**
* 生成token
* @param map //传入payload
* @return 返回token
*/
public static String getToken(Map<String,String> map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,7);
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(masterKey)).toString();
}
/**
* 验证token
* @param token
* @return
*/
public static void verify(String token){
JWT.require(Algorithm.HMAC256(master_key)).build().verify(token);
}
}
一个简单的案例
该案例只是作为本次jwt的一个简单使用的说明,不会结合安全框架。
建立数据库的用户表,然后存放用户登录信息。
create database auth_jwt;
use auth_jwt;
drop table if exists user;
create table user(
id int(11) primary key auto_increment comment "主键" ,
name varchar(80) default null comment "姓名",
password varchar(40) default null comment "密码"
)
然后在idea里面搭建起来框架,可以使用mybatis或者plus也可以。我使用的是mybatis。
实体类
package com.jgdabc.entity;
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/8 9:04
* @Description 功能描述:实体类
*/
@Data
public class User {
private String id;
private String name;
private String password;
}
dao层,也就是mapper接口
package com.jgdabc.dao;
import com.jgdabc.entity.User;
import org.apache.ibatis.annotations.Mapper;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/8 9:19
* @Description 功能描述:
*/
@Mapper
public interface UserDao {
User login(User user);
}
mapper xml
<mapper namespace="com.jgdabc.dao.UserDao">
<select id="login" parameterType="User" resultType="User">
select * from user where name = #{name} and password = #{password}
</select>
</mapper>
在springboot的配置文件中你需要指定一些扫描,其他的就是数据源哪些和端口
mybatis:
type-aliases-package: com.jgdabc.entity
mapper-locations: classpath:/mapper/*xml
我们需要写点业务逻辑
service层
package com.jgdabc.service;
import com.jgdabc.entity.User;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/8 9:59
* @Description 功能描述:
*/
public interface UserService {
User login(User user);
}
impl
package com.jgdabc.impl;
import com.jgdabc.Utils.JwtUtil;
import com.jgdabc.dao.UserDao;
import com.jgdabc.entity.User;
import com.jgdabc.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.HashMap;
import java.util.Map;
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/8 10:06
* @Description 功能描述:
*/
@Service
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
public User login(User user) {
User user_login = userDao.login(user);
if (user_login!=null)
{
return user_login;
}
throw new RuntimeException("登录失败");
}
}
前后端分离的情况下,其实是这样一种逻辑。我们可以去设置一个登录接口,然后用户登录成功之后后端将返回一个令牌,也就是token,那么用户再访问其他的接口的时候将携带这个token去访问,后端对token进行校验成功后授权访问。这是最简单的一种逻辑。当然后面结合springsecurity会变得复杂些。
controller
首先登录接口
@PostMapping ("/user/login/")
public Map<String, Object> login( @RequestBody User user) {
log.info("获取请求");
HashMap<String, String> map = new HashMap<>();
HashMap<String, Object> result = new HashMap<>();
try {
User userDB = userService.login(user);
map.put("id", userDB.getId());
map.put("username", userDB.getName());
String token = JwtUtil.getToken(map);
result.put("state",200);
result.put("message","登录成功");
result.put("token",token);
} catch (Exception e) {
result.put("state",false);
result.put("msg",e.getMessage());
}
return result;
}
以上只是简单的写,可以自己做出适当的封装。登录这里可以看到,我们登录成功后是会返回一段token的。然后我们再做一个接口需要去经过token校验才可以访问。
我们可以做一个简单的拦截器
public class JwtHander implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
JwtUtil.verify(token);
return true;
} catch (TokenExpiredException e) {
map.put("state", false);
map.put("msg", "Token已经过期!!!");
} catch (SignatureVerificationException e){
map.put("state", false);
map.put("msg", "签名错误!!!");
} catch (AlgorithmMismatchException e){
map.put("state", false);
map.put("msg", "加密算法不匹配!!!");
} catch (Exception e) {
e.printStackTrace();
map.put("state", false);
map.put("msg", "无效token~~");
}
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
然后配置并让它成为容器组件
/**
* @author 兰舟千帆
* @version 1.0
* @date 2023/7/8 12:58
* @Description 功能描述:
*/
@Component
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtHander())
.excludePathPatterns("/user/**").addPathPatterns("/");
}
}
这样我们就可以展开测试了
postman 测试
这样就返回一点token,然后呢我们可以定义别的接口,比如这样一个
@GetMapping("/users/test")
public String test_demo()
{
return "接口token校验成功";
}
}
接口的token要放到header这里,将来实际的前端给后端token的时候也可以这样去做。
案例代码地址auth_jwt