Spring Boot + JWT 实现接口统一Token认证

为什么要使用 token?直接使用 cookie + session 不就完了吗?

我们来看看传统方式实现的接口鉴权,客户端调用登录接口登录成功,服务端将登录状态保存在 session 中,客户端调用后续接口,服务端根据 session 统一校验。
这样看起来是没问题的,但是服务端接口变成分布式以后,这种方式就有问题了,假如一个服务端署了2个实例(A,B),这时客户端请求A实例登录成功了,又发了一个订单请求跑到了B实例,那么这个订单请求肯定会鉴权失败,提示未登录,这样肯定不符合产品设计的,那么就需要将A,B实例的 session 进行共享,但是这种方式又会造成服务器网络IO的压力大,延迟太长等问题。

JWT就解决了上述的问题(还有这里没提到的避免 CSRF 攻击等),相当于将 session 保存在了客户端,解决了后台 session 复制的问题,客户端登录成功后,服务端响应一个 token,客户端在后续请求带上这个 token 随便到哪个实例都可以进行接口鉴权。

当然,正是由于 token 的无状态,也导致了一些缺点:比如当服务端在 token 有效期内废弃一个 token 或者更改它的权限的话,不会立即生效,一般需要等到有效期过后才可以。另外,当用户退出登录后,token 也不会失效。除非,我们在服务端增加额外的处理逻辑。

安装

详见github:https://github.com/jwtk/jjwt/
复制可得:

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.2</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.2</version>
    <scope>runtime</scope>
</dependency>

yml添加常用配置

jwt:
  secret: gU1lF5jB1bN1dK2kE2lH1bH3fL1gQ1gU1lF5jB1bN1d #密钥
  expiration: 30 #token有效期(S)

工具类

需要注意的是,生产token的signWith方法有更新,网上比较常见的signWith(SignatureAlgorithm.HS512, secret)方法在0.10.0及以上版本已提示过期,并提示将在1.0版本彻底删除;所以我们这里推荐使用Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret))方法生成SecretKey,另外secret的安全规范也需要遵守,比如不得含有’$'特殊字符,长度不得小于43等

import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * JwtToken操作工具类
 *
 * @author jason
 */
@Slf4j
@Component
public class JwtTokenUtil {
	private static final String CLAIM_KEY_USERNAME = "sub";
	private static final String CLAIM_KEY_CREATED = "created";

	/**
	 * 密钥
	 */
	@Value("${jwt.secret}")
	private String secret;
	/**
	 * token有效期(S)
	 */
	@Value("${jwt.expiration}")
	private Long expiration;

	/**
	 * 根据用户信息生成token
	 */
	@SneakyThrows
	public String generateToken(Object userInfo) {
		Map<String, Object> claims = new HashMap<>();
		claims.put(CLAIM_KEY_USERNAME, new ObjectMapper().writeValueAsString(userInfo));
		claims.put(CLAIM_KEY_CREATED, new Date());
		return generateToken(claims);
	}

	/**
	 * 从token中获取用户信息
	 */
	@SneakyThrows
	public <T> T getUserInfoFromToken(String token, Class<T> valueType) {
		Claims claims = getClaimsFromToken(token);
		return new ObjectMapper().readValue(claims.getSubject(), valueType);
	}

	/**
	 * 判断token是否有效
	 *
	 * @return true 有效
	 */
	public boolean isTokenExpired(String token) {
		Date expiredDate = getExpiredDateFromToken(token);
		return expiredDate.after(new Date());
	}

	/**
	 * 刷新token
	 */
	public String refreshToken(String token) {
		Claims claims = getClaimsFromToken(token);
		claims.put(CLAIM_KEY_CREATED, new Date());
		return generateToken(claims);
	}

	/**
	 * 生成token
	 */
	private String generateToken(Map<String, Object> claims) {
		return Jwts.builder()
				.setClaims(claims)
				.setExpiration(generateExpirationDate())
				// .signWith(SignatureAlgorithm.HS512, secret) // 已过期
				.signWith(generateKeyByDecoders())
				.compact();
	}

	/**
	 * 从token中获取
	 */
	private Claims getClaimsFromToken(String token) {
		return Jwts.parserBuilder()
				.setSigningKey(generateKeyByDecoders())
				.build()
				.parseClaimsJws(token)
				.getBody();
	}

	/**
	 * 生成token的过期时间
	 */
	private Date generateExpirationDate() {
		return new Date(System.currentTimeMillis() + expiration * 1000);
	}

	/**
	 * 生成自定义Key
	 */
	private SecretKey generateKeyByDecoders() {
		return Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
	}

	/**
	 * 从token中获取过期时间
	 */
	private Date getExpiredDateFromToken(String token) {
		Claims claims = getClaimsFromToken(token);
		return claims.getExpiration();
	}

}

接口实现

这里写了2个测试接口,一个<登录>接口,一个<获取用户信息>接口,大致流程如下:
客户端调用<登录>接口提交账号密码到服务端,成功后服务端响应一个token,客户端带上token调用<获取用户信息>接口,服务端对token鉴权成功后响应用户信息。
其它接口同理。

token 鉴权常见异常:
ExpiredJwtException( token 过期)
SignatureException(签名编码有误)
MalformedJwtException(构造有误)

	/**
	 * 登录
	 * -
	 * http://127.0.0.1:9090/user/login?username=jason&password=123456
	 */
	@GetMapping("login")
	public String login(UserInfoVo userInfoVo) {
		// TODO 参数校验...省略

		// TODO 登录校验...省略

		// 存放用户信息到token
		UserInfoDto userInfoDto = new UserInfoDto();
		userInfoDto.setId(20201203001L);
		userInfoDto.setUsername("jason");
		userInfoDto.setNickName("张三");
		// 响应token
		return jwtTokenUtil.generateToken(userInfoDto);
	}

	/**
	 * 通过token获取用户信息
	 * -
	 * http://127.0.0.1:9090/user/info
	 */
	@GetMapping("info")
	public UserInfoDto info() {
		try {
			return getUserInfo();
		} catch (ExpiredJwtException e) {
			log.error("token已过期", e);

			UserInfoDto userInfoDto = new UserInfoDto();
			userInfoDto.setNickName("token已过期");
			userInfoDto.setUsername("token已过期");
			return userInfoDto;
		}
	}

测试

使用 postman 调用登录接口
在这里插入图片描述
带上 token 获取用户信息
在这里插入图片描述
因为我们 token 有效期设置的是30s,过期后再调用就会提示已过期。这里响应的就比较随意了,按规范来搞应该是封装统一异常+统一响应类
在这里插入图片描述

源码下载

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值