使用JWT实现登录认证
一、介绍
1.1、Session、Cookie、Token区别
session:存储再服务端,无法引用与分布式场景,并且需要占用服务端的资源
cookie:存储再客户端,适用于分布式场景,但是存在安全问题,不支持垮域访问
token:存储在localstorage中,更加灵活
1.2、如何实现登录认证
- 用户使用账号密码登录成功
- 通过JWT生成一串字符串作为Token,返回给前端
- 前端每次请求的时候都在请求头中携带上这个Token
- 后端每次都使用JWT对该Token进行校验,还原出一些用户信息,以此来判断用户是否登录
1.3、JWT组成
1.3.1、样例
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjpbXSwiaWF0IjoxNjg0ODIzNzIzLCJleHAiOjE2ODQ4NTc1OTksImF1ZCI6IiIsImlzcyI6ImFxaSIsInN1YiI6IiJ9.W306xll5X2lHWL_B0AUZs7nf9e7Zn5QvgoasnviBeaQ
1.3.2、组成
JWT生成的字符串由三个部分组成
第一部分(header:JWT头,该部分只用Base64编码,未加密)
头部由2个属性组成
1、typ:令牌类型,固定设置为JWT
2、alg:加密算法,默认为HS256
第二部分(Payload:有效载荷,该部分只用Base64编码,未加密,避免存放隐私信息)
就是JWT的主体部分
1、issuer:发行者
2、IssuedAt:发布时间
3、expiration:到期时间
4、subject:主题
5、Not Before:生效时间
6、JWT ID:用于标识该 JWT
7、audience:用户
第三部分(Signature:签名,该部分是安全的,无法被解密)
该部分可以设置secret(俗称:加盐)的方式增加该部分的破解难度
1.4、优缺点
1.4.1、优点
- 是json格式,跨语言的
- 可以利用Payload存储一些非敏感的信息
- 不需要存储在服务端,可以用于分布式场景
- 一般存储在localStorage中,不存在于Cookie中,避免了一些安全性问题
- 便于实现单点登录功能
1.4.2、缺点
- 一旦生成就无法修改过期时间,需要搭配缓存来实现过期或者退出效果
- 同样Token过期无法进行续签
- 不可以在JWT中存储敏感信息
二、使用
2.1、引入POM依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
2.2、编写工具类
package com.xx.utils;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.util.StringUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Date;
/**
* @author aqi
* DateTime: 2020/11/9 3:27 下午
* Description: JWT工具类
*/
public class JwtUtils {
/**
* 设置Token过期时间
*/
public static final long EXPIRE = 1000 * 60 * 60 * 24;
/**
* 秘钥
*/
public static final String APP_SECRET = "ukc8BDbRigUDaY6pZFfWus2jZWLPHO";
/**
* 生成Token字符串(Token组成:头+荷载+签名)
* @param id 用户id
* @param nickname 用户名称
* @return Token字符串
*/
public static String getJwtToken(String id, String nickname){
return Jwts.builder()
// 设置JWT的头信息
.setHeaderParam("typ", "JWT")
.setHeaderParam("alg", "HS256")
// 设置Token过期时间
// 主题
.setSubject("guli-user")
// 签发时间
.setIssuedAt(new Date())
// 过期时间
.setExpiration(new Date(System.currentTimeMillis() + EXPIRE))
// 设置Token主体部分
.claim("id", id)
.claim("nickname", nickname)
// 签名算法,秘钥
.signWith(SignatureAlgorithm.HS256, APP_SECRET)
.compact();
}
/**
* 判断token是否存在与有效
* @param jwtToken token
* @return 判断token是否存在与有效
*/
public static boolean checkToken(String jwtToken) {
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 判断token是否存在与有效
* @param request request
* @return 判断token是否存在与有效
*/
public static boolean checkToken(HttpServletRequest request) {
try {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) {
return false;
}
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
} catch (Exception e) {
e.printStackTrace();
return false;
}
return true;
}
/**
* 根据token获取会员id
* @param request request
* @return 会员id
*/
public static String getMemberIdByJwtToken(HttpServletRequest request) {
String jwtToken = request.getHeader("token");
if(StringUtils.isEmpty(jwtToken)) {
return "";
}
Jws<Claims> claimsJws = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(jwtToken);
Claims claims = claimsJws.getBody();
return (String)claims.get("id");
}
}
2.3、登录代码
public String login(UcenterMember member) {
//获取登录手机号和密码
String mobile = member.getMobile();
String password = member.getPassword();
//手机号和密码非空判断
if(StringUtils.isEmpty(mobile) || StringUtils.isEmpty(password)) {
throw new MyException(20001,"登录失败");
}
//判断手机号是否正确
QueryWrapper<UcenterMember> wrapper = new QueryWrapper<>();
wrapper.eq("mobile",mobile);
UcenterMember mobileMember = baseMapper.selectOne(wrapper);
//判断查询对象是否为空
if(mobileMember == null) {//没有这个手机号
throw new MyException(ResultCode.CODE_20001);
}
//判断密码
if(!DigestUtils.md5DigestAsHex(password.getBytes()).equals(mobileMember.getPassword())) {
throw new MyException(ResultCode.CODE_20001);
}
//判断用户是否禁用
if(mobileMember.getIsDisabled()) {
throw new MyException(ResultCode.CODE_20001);
}
//登录成功,生成token字符串
return JwtUtils.getJwtToken(mobileMember.getId(), mobileMember.getNickname());
}
2.4、使用JWT做拦截
/**
* 根据token获取用户信息
*/
@GetMapping("/getMemberInfo")
public R getMemberInfo(HttpServletRequest request) {
//调用jwt工具类的方法。根据request对象获取头信息,返回用户id
String memberId = JwtUtils.getMemberIdByJwtToken(request);
if (memberId.isEmpty()) {
return R.error().data("userInfo", "登陆失效,请重新登录");
}
//查询数据库根据用户id获取用户信息
UcenterMember member = memberService.getById(memberId);
return R.success().data("userInfo",member);
}
三、续签和过期问题
3.1、思路
将生成的JWT字符串不返回给前端,而是自己生成一个UUID,将该UUID返回给前端,再使用该UUID存储