安全认证--JWT介绍及使用
- 1.无状态登录原理
- 1.1.什么是有状态?
- 1.2.什么是无状态
- 1.3.如何实现无状态
- 1.4.JWT
- 1.4.1.简介
- 1.4.2.数据格式
- 2.编写JWT工具
- 2.1.添加JWT依赖
- 2.2.载荷对象
- 2.3.工具
- 2.4.测试
- 2.4.1.配置秘钥
- 2.4.2.测试类
- 2.5项目源码
1.无状态登录原理
有状态登录和无状态登录详解
1.1.什么是有状态?
有状态服务,即服务端需要记录每次会话的客户端信息,从而识别客户端身份,根据用户身份进行请求的处理,典型的设计如tomcat中的session。
例如登录:用户登录后,我们把登录者的信息保存在服务端session中,并且给用户一个cookie值,记录对应的session。然后下次请求,用户携带cookie值来,我们就能识别到对应session,从而找到用户的信息。
缺点是什么?
- 服务端保存大量数据,增加服务端压力
- 服务端保存用户状态,无法进行水平扩展
- 客户端请求依赖服务端,多次请求必须访问同一台服务器
1.2.什么是无状态
微服务集群中的每个服务,对外提供的都是Rest风格的接口。而Rest风格的一个最重要的规范就是:服务的无状态性,即:
- 服务端不保存任何客户端请求者信息
- 客户端的每次请求必须具备
自描述信息
,通过这些信息识别客户端身份
带来的好处是什么呢?
- 客户端请求不依赖服务端的信息,任何多次请求不需要必须访问到同一台服务
- 服务端的集群和状态对客户端透明
- 服务端可以任意的迁移和伸缩
- 减小服务端存储压力
1.3.如何实现无状态
无状态登录的流程:
- 当客户端第一次请求服务时,服务端对用户进行信息认证(登录)
- 认证通过,将用户信息进行加密形成token,返回给客户端,作为登录凭证
- 以后每次请求,客户端都携带认证的token
- 服务端对token进行解密,判断是否有效。
整个登录过程中,最关键的点是什么?
token的安全性
token是识别客户端身份的唯一标示,如果加密不够严密,被人伪造那就完蛋了。
采用何种方式加密才是安全可靠的呢?
我们将采用JWT
来生成token,保证token的安全性
1.4.JWT
1.4.1.简介
JWT,全称是Json Web Token, 是JSON风格轻量级的授权和身份认证规范,可实现无状态、分布式的Web应用授权;官网:https://jwt.io
GitHub上jwt的java客户端:https://github.com/jwtk/jjwt
1.4.2.数据格式
JWT包含三部分数据:
-
Header:头部,通常头部有两部分信息:
- token类型,这里是JWT
- 签名算法,自定义
我们会对头部进行base64加密(可解密),得到第一部分数据
-
Payload:载荷,就是有效数据,一般包含下面信息:
- 标准载荷:JWT规定的信息,jwt的元数据:
- JTI: JWT的id,当前jwt的唯一标识(像身份证号)
- IAT: issue at 签发时间
- EXP:过期时间
- SUB:签发人
- …
- 自定义载荷:
- 用户身份信息,(注意,这里因为采用base64加密,可解密,因此不要存放敏感信息)
这部分也会采用base64加密,得到第二部分数据
- 标准载荷:JWT规定的信息,jwt的元数据:
-
Signature:签名,是整个数据的认证信息。一般根据前两步的数据,再加上服务的的密钥(secret)(不要泄漏,最好周期性更换),通过加密算法生成。用于验证整个数据完整和可靠性
生成的数据格式:
可以看到分为3段,每段就是上面的一部分数据。
什么是 JWT – JSON WEB TOKEN
傻傻分不清之 Cookie、Session、Token、JWT
JWT详细教程与使用
2.编写JWT工具
我们会用到比较流行的java语言的JWT工具,jjw
2.1.添加JWT依赖
我们需要先在项目中引入JWT依赖:
<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>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--json工具-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--日期时间工具类-->
<dependency>
<groupId>joda-time</groupId>
<artifactId>joda-time</artifactId>
</dependency>
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2.载荷对象
JWT中,会保存载荷数据,我们计划存储2部分:
- jti:jwt的id
- UserDetail:用户数据
为了方便后期获取,我们定义一个类来封装。
添加一个实体类,代表载荷信息
import lombok.Data;
//载荷对象
@Data
public class Payload {
/**
* tocken的id
*/
private String jti;
/**
* 用户数据
*/
private UserDetail userDetail;
}
载荷中的UserDetail信息,也需要一个实体类表示,这里我们定义一个UserDetail类。
这里我们假设用户信息包含2部分:
- id:用户id
- username:用户名
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor(staticName = "of")
@NoArgsConstructor
public class UserDetail {
/**
* 用户id
*/
private Long id;
/**
* 用户名
*/
private String username;
}
2.3.工具
我创建一个JwtUtils 工具类,用来封装几个方法:
- createJwt() :生成JWT
- parseJwt() :验证并解析JWT
import com.example.jwt.constants.RedisConstants;
import com.example.jwt.dto.Payload;
import com.example.jwt.dto.UserDetail;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jws;
import io.jsonwebtoken.JwtParser;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.nio.charset.Charset;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
public class JwtUtils {
/**
* JWT解析器
*/
private final JwtParser jwtParser;
/**
* 秘钥
*/
private final SecretKey secretKey;
// @Autowired
private StringRedisTemplate redisTemplate;
private final static ObjectMapper mapper = new ObjectMapper();
public JwtUtils(String key) {
// 生成秘钥
secretKey = Keys.hmacShaKeyFor(key.getBytes(Charset.forName("UTF-8")));
// JWT解析器
this.jwtParser = Jwts.parserBuilder().setSigningKey(secretKey).build();
}
/**
* 生成jwt,自己指定的JTI
*
* @param userDetails 用户信息
* @return JWT
*/
public String createJwt(UserDetail userDetails) {
try {
// 生成tokenid
String jti=createJti();
//存入redis中
// this.redisTemplate.opsForValue().set(RedisConstants.JTI_KEY_PREFIX+userDetails.getUsername(),jti,30, TimeUnit.MINUTES);
return Jwts.builder().signWith(secretKey)
.setId(jti)
.claim("user", mapper.writeValueAsString(userDetails))
.compact();
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
}
/**
* 解析jwt,并将用户信息转为指定的Clazz类型
*
* @param jwt token
* @return 载荷,包含JTI和用户信息
*/
public Payload parseJwt(String jwt) {
try {
Jws<Claims> claimsJws = jwtParser.parseClaimsJws(jwt);
Claims claims = claimsJws.getBody();
Payload payload = new Payload();
payload.setJti(claims.getId());
payload.setUserDetail(mapper.readValue(claims.get("user", String.class), UserDetail.class));
return payload;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private String createJti() {
return StringUtils.replace(UUID.randomUUID().toString(), "-", "");
}
/**
* 刷新jwt有效期
* @param username
*/
public void refreshJwt(String username) {
String key= RedisConstants.JTI_KEY_PREFIX+username;
//重置key的过期时间
redisTemplate.expire(key,30,TimeUnit.MINUTES);
}
}
2.4.测试
2.4.1.配置秘钥
在application.yml
文件中配置秘钥:
yy:
jwt:
key: helloWorldJavaAuthServiceSecretKe
定义一个配置类,注册JwtUtils
注入到Spring的容器。
import com.example.jwt.utils.JwtUtils;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class JwtConfig {
@Value("${yy.jwt.key}")
private String key;
@Bean
public JwtUtils jwtUtils(){
return new JwtUtils(key);
}
}
2.4.2.测试类
@Autowired
private JwtUtils jwtUtils;
@Test
public void test() {
// 生成jwt
String jwt = jwtUtils.createJwt(UserDetail.of(112L, "lele"));
System.out.println("jwt = " + jwt);
// 解析jwt
Payload payload = jwtUtils.parseJwt(jwt);
System.out.println("payload = " + payload);
}
结果:
jwt = eyJhbGciOiJIUzI1NiJ9.eyJqdGkiOiIyZTRkMGI1NjZiODY0YjUzODAwZTI3NGNhOWE0MTcxYSIsInVzZXIiOiJ7XCJpZFwiOjExMixcInVzZXJuYW1lXCI6XCJsZWxlXCJ9In0.NGa42tISwsLg_hyONasdGPGDigFFxkWbH04wd4ELztY
payload = Payload(jti=2e4d0b566b864b53800e274ca9a4171a, userDetail=UserDetail(id=112, username=lele))
2.5项目源码
源码地址