Jwt
Jwt简介:
JWT(json web token)一般被用来在身份提供者和服务提供者间传递被认证用户的身份信息,以便于从资源服务器获取资源,也可以增加一些额外的业务逻辑所需的声明信息。
JWT规定了数据传输的结构,一串完整的JWT由三段落组成,每个段落用英文句号连接(.)连接,他们分别是:头部(Header)、负载(Payload)、签名(Signature),所以,常规的JWT内容格式是这样的:AAA.BBB.CCC
Header:
Header中存储了所使用的加密算法和Token类型
{
"alg" : "HS256",
"type" : "JWT"
}
Payload:
Payload表示负载,是一个JSON对象。
issuer : 签发人
expiration : 过期时间
subject : 主题
audience : 受众
IssuedAt : 签发时间
JWTID : id
JWT规定了7个官方字段供选用
Signature:
Signature部分是对前两部分的签名,防止数据篡改。
首先,需要指定一一个密钥(secret) 。 这个密钥只有服务器才知道,不能泄露给用户。然后,使用Header里面指定的签名算法(默认是HMACSHA256),按照下面的公式产生签名。
HMAXSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload),secret)
案例演示
1.新建一个demo或者在原有项目导入jwt依赖即可
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.1</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
2.编写Jwt工具类
package com.lyf.shiro.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.algorithms.Algorithm;
import org.apache.commons.lang3.time.DateUtils;
import java.time.Duration;
import java.util.Date;
import java.util.UUID;
public class JwtUtil {
/**
* 生成JWT Token
*
* @param username 用户名
* @param salt 盐值
* @param expireDuration 过期时间和单位
* @return token
*/
public static String generateToken(String username, String salt, Duration expireDuration) {
try {
// 过期时间,单位:秒
Long expireSecond = expireDuration.getSeconds();
// 默认过期时间为1小时
Date expireDate = DateUtils.addSeconds(new Date(), expireSecond.intValue());
// 生成token
//定义Header部分,并加密盐值 ,相对于secret
//withXxx开头定义负载 .sign表示签名
Algorithm algorithm = Algorithm.HMAC256(salt);
String token = JWT.create()
.withClaim("username", username)
// jwt唯一id
.withJWTId(UUID.randomUUID().toString().replace("-",""))
// 签发人
.withIssuer("jwt")
// 主题
.withSubject("jwt")
// 签发的目标
.withAudience("web")
// 签名时间
.withIssuedAt(new Date())
// token过期时间
.withExpiresAt(expireDate)
// 签名
.sign(algorithm);
return token;
} catch (Exception e) {
}
return null;
}
public static void main(String[] args) {
String token = JwtUtil.generateToken("user", "123456", Duration.ofSeconds(3600L));
System.out.println(token);
}
}
运行结果:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJqd3QiLCJhdWQiOiJ3ZWIiLCJpc3MiOiJqd3QiLCJleHAiOjE2Njg3NTE0NjksImlhdCI6MTY2ODc0Nzg2OSwianRpIjoiOTYzMjQxYzY0ZTUyNDA4OTg3OWRlZGM0NjVmZDU4ZjUiLCJ1c2VybmFtZSI6InVzZXIifQ.-ytoNCeyp8Mk7pS4ZVfpkWUZDzDWqFsA70XnFJr44eA
解析header与Payload如下,由于Signature使用salt进行HMAC256加密,是不可逆的,而header与Playload使用Base64进行编码是可逆的,所以能解析出信息
登录测试
成功返回token与用户信息,失败重新登录。
创建user、role 、permission实体类
User.java
@Data
public class User implements Serializable {
private static final long serialVersionUID = 2951987436867980763L;
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
}
Role.java
@Data
public class Role implements Serializable {
private static final long serialVersionUID = -8250617019976873581L;
/**
* 角色id
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
}
Permission.java
@Data
public class Permission implements Serializable {
private static final long serialVersionUID = -3561812452603746297L;
/**
* 权限id
*/
private Long permissionId;
/**
* 权限名称
*/
private String name;
}
封装返回给前端用户的数据vo对象
@Data
@Accessors(chain = true)
public class LoginUserVo implements Serializable {
private static final long serialVersionUID = 4577809751021629198L;
/**
* 主键
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 角色id
*/
private Long roleId;
/**
* 角色名称
*/
private String roleName;
/**
* 权限
*/
private Set<String> permissionCodes;
}
封装token与登录对象Vo一起
@Data
@Accessors(chain = true)
public class LoginUserTokenVo implements Serializable {
private static final long serialVersionUID = 322418505479803035L;
/**
* token
*/
private String token;
/**
* 登录用户信息
*/
private LoginUserVo loginUserVo;
}
创建登录service接口对象
public interface LoginUserService {
/**
* 登录
* @param username
* @param password
* @return
*/
LoginUserTokenVo login(String username,String password);
}
创建登录service实现类对象,里面的用户数据应该从数据库动态获取,我这里做了简化。
@Service
public class LoginServiceImpl implements LoginUserService {
@Override
public LoginUserTokenVo login(String username, String password)throws Exception {
User user = new User();
user.setUsername("user");
user.setPassword("123");
user.setId(1L);
if (user == null) {
throw new Exception("用户名或密码错误");
}
if (!password.equals(user.getPassword())) {
throw new Exception("角色不存在");
}
LoginUserVo loginUserVo = new LoginUserVo();
loginUserVo.setUsername(user.getUsername());
loginUserVo.setId(user.getId());
Role role = new Role();
role.setRoleId(1L);
role.setRoleName("user");
if (role == null) {
throw new Exception("角色不存在");
}
loginUserVo.setRoleId(role.getRoleId()).setRoleName(role.getRoleName());
Set<String> permission = new HashSet<>();
permission.add("system:user:add");
permission.add("system:user:view");
permission.add("system:user:delete");
permission.add("system:user:edit");
loginUserVo.setPermissionCodes(permission);
LoginUserTokenVo loginUserTokenVo = new LoginUserTokenVo();
loginUserTokenVo.setLoginUserVo(loginUserVo);
//生成token
/**
* username:用户名,salt:"123456"加密的盐值,Duration.ofSeconds(3600L 过期时间
*/
String token = JwtUtil.generateToken(username, "123456", Duration.ofSeconds(3600L));
loginUserTokenVo.setToken(token);
return loginUserTokenVo;
}
创建登录controller对象
@Controller
public class loginController {
@Autowired
LoginUserService loginService;
@PostMapping("/login")
@ResponseBody
public Object login(String username,String password ) throws Exception {
LoginUserTokenVo loginUserTokenVo = loginService.login(username,password);
Map<String,Object> map = new HashMap<>();
// 设置token响应头
map.put("操作成功",loginUserTokenVo);
return map;
}
@RequestMapping("/success")
@ResponseBody
public String success(){
return "恭喜您,登录成功";
}
}
使用PostMan测试接口
Jwt返回token给前端完成
前端只需要请求数据时,带上这个token的值即可,如果这个token在设置的时间内过期,则跳转到重新登录。
Jwt验证前端传人的token
修改JwtUtil.java,加入token验证的方法
/**
* 根据传人的token与salt验证token,salt相对于 secret 密钥
* @param token
* @param salt
* @return
*/
public static boolean verifyToken(String token, String salt) {
try {
Algorithm algorithm = Algorithm.HMAC256(salt);
JWTVerifier verifier = JWT.require(algorithm)
// 签发人
.withIssuer("jwt")
// 主题
.withSubject("jwt")
// 签发的目标
.withAudience("web")
.build();
DecodedJWT jwt = verifier.verify(token);
if (jwt != null) {
return true;
}
} catch (Exception e) {
}
return false;
}
定义一个拦截器,拦截所有请求,且放行login页面的请求。
public class LoginInterceptor extends HandlerInterceptorAdapter {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
String salt ="123456";
if (token !=null){
return JwtUtil.verifyToken(token,salt);
}
response.setCharacterEncoding("UTF-8");
PrintWriter writer = response.getWriter();
writer.write("您还没有登录");
writer.close();
System.out.println("还没有登录");
return false;
}
}
注册拦截器到web中
@Configuration
public class JwtWebConfig implements WebMvcConfigurer {
@Bean
LoginInterceptor loginInterceptor(){
return new LoginInterceptor();
}
/**
* 注册拦截器到web中
* @param registry
*/
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(loginInterceptor())
//拦截所有请求
.addPathPatterns("/")
//放行/login 请求
.excludePathPatterns("/login");
}
}
测试拦截器
没有带token的值去访问errorMessage接口,被拦截
使用token进行访问
Jwt的优点
跨语言:支持主流语言。
自包含:包含必要的所有信息,如用户信息和签名等。
易传递:很方便通过 HTTP 头部传递。
json形式,而json非常通用性可以让它在很多地方使用。jwt所占字节很小,便于传输信息,需要服务器保存信息,易于扩展。
Jwt的缺点
- 由于服务器不保存 session 状态,因此 JWT 无法在使用过程中废止某个 token,或更改 token 的权限。一旦 JWT 签发,在到期之前就会始终有效,除非服务器部署额外的逻辑。
- JWT 本身包含认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证。
- 为减少盗用,JWT 不应该使用 HTTP协议明码传输,要使用 HTTPS 协议传输。
JWT常用方法
JWT.java
该方法是JWT.java类的一个创建token的方法,里面有JWTCreator类,这个类中有一个Builder内部类,里面定义了两个map,一个是jwt载体,一个是jwt头部
public static JWTCreator.Builder create() {
return JWTCreator.init();
}
Builder内部类中,定义了头部与载体的所有设置的方法
该方法定义了头部
该方法定义了生成token的唯一id,发行人、主题等,所有方法
该sign方法进行加密签名,将头部与载体部分进行base64编码,然后在一起进行加密。
该方法是JWT.java解密方法。点击DecodeJWT进去
该接口继承了两个接口,分别是载体(Playload)与头部(Header),其中,签名方法在最后
JWTDecoder.java实现了该接口,在该类中导入了JWTParser类进行Json字符串解析,在JWTParser类中,用到了jackson jar包。
该方法是通过传人加密的secret参数进行验证
如下代码所示