传统的token认证
用户登录,服务端给前端返回token,并将token保存在服务端。
以后用户再来访问时,需要携带token,服务端获取token后再去数据库获取token做校验。
JWT的token认证
用户登录,服务端给用户返回一个token(服务端不保存)
以后用户再来访问时,需要携带token,服务端获取token做校验
两种认证方式对比:
jwt相对于传统的token认证,无需将token保存在服务端。
因为HTTP request 本身是stateless的,所以要不在server端使用session来判断,要不就用JWT,也就是bearer token,包含的有效期信息,以及user 信息来进行状态判断是否接受HTTP的request(比如用户是否已经登录),来避免存储session,以及服务器集群之间还要实现session同步的麻烦,现在只要定义一个secret_key就行。
每当用户想要访问受保护的路由或资源时,用户代理应该发送 JWT,通常在Authorization标头中使用Bearer模式。标头的内容应如下所示:
Authorization: Bearer
JOSN Web Token(jwt)包含头部(header),载荷(claim set), 和签名(signature)。可以在载荷中存放预定义的元数据,只要是JOSON格式就可以了。
2.1 生成token
用户登录成功后,使用jwt创建一个token,并返回给用户,token格式如下
Base64URL(header)//第一段header
.base64UrlEncode(payload)//第二段payload
.HMACSHA256(Base64URL(header).base64UrlEncode(payload),secret)//第三段verify signature
例子:
eyJhbGciOiJIUzI1NiJ9//第一段
.eyJvcGVuSWQiOiJvam5NVjVKQ3htdTI1Zjl6ai1SYU5xN0JiZTJvIiwianRpIjoidG9rZW5JZCIsImlhdCI6MTY0NDEzNDI5MywiZXhwIjoxNjQ0MTM2OTIwfQ//第二段
.MRx_xPGNa9lzDGj4nrcdENCA2OgIp4En0TL_GH-_0BI//第三段
注意:jwt生成的token是由三段字符串拼接而成,使用 . 连接起来
1.token的第一段字符串:由下面的json数据通过base64(可逆)加密算法得到。
{
"alg": "HS256", //第三段字符串的不可逆加密类型HS256
"typ": "JWT" //token类型JWT
}
2.token的第二段字符串:是由下面的payload信息通过base64(可逆)加密算法得到
// payload信息 为自定义值,一般不放敏感信息
{
"sub": "1234567890", //用户id
"name": "John Doe", //用户名
"exp": 1516239022, //token过期时间
"openId": "fasdkhgflksdhfgsdkjlf"
}
3.token的第三段字符串构成:
1)先将第一段和第二段的密文拼接起来
2)对拼接起来的密文字符串和自定义的盐进行 上边指定的HS256加密
3)对HS256加密后的密文再做base64加密
注意:第一、二部分可以通过Base64
解密得到,但第三部分不可以!
生成token代码如下
/**
* 创建JWT
*/
public static String createJWT(Map<String, Object> claims, Long time) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Date now = new Date(System.currentTimeMillis());
SecretKey secretKey = generalKey();
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(jwtId)
//iat: jwt的签发时间
.setIssuedAt(now)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey);
if (time >= 0) {
long expMillis = nowMillis + time;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
//然后返回token
}
2.2 验证(解密)token
当用户再来访问时,需要携带token,后端需要对token进行校验
/**
* 验证jwt
*/
public static Claims verifyJwt(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
SecretKey key = generalKey();
Claims claims;
try {
claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}//设置需要解析的jwt
return claims;
}
ps : token一旦生成,在过期时间内永久有效,即使项目重启!想要失效token必须等待过期,或者重置盐值!
使用jwt时,一般修改密码或退出登录时,需要把正在使用的token做失效处理,防止别的客户端使用失效token访问信息。
token
,修改了自定义盐值,自然访问不通过。token
时,标注上创建时间。如果这个创建时间小于修改或登出的时间,就表示它是修改或者登出之前的token
,为过期token
(有点不是很懂,好像就是判断token是否过期) /**
* 根据userId和openid生成token
*/
public static String generateToken(String openId) {
Map<String, Object> map = new HashMap<>();
map.put("openId", openId);
return createJWT(map, tokenExpiredTime);//直接调用上面的createJWT方法
}
/**
* token是否过期,就是拿到
* @return true:过期
* lastLoginDate 数据库记录的最后一次登出时间
* issueDate token 创建时间
*/
public boolean isTokenExpired(Date expiration,Date lastLoginDate,Date issueDate) {
//token创建时间小于数据库记录的最后一次登出时间 过期
if(lastLoginDate == null){
return expiration.before(new Date());
}else{
return issueDate.before(lastLoginDate);
}
}
拦截器的判断:
if(jwtUtils.isTokenExpired(claims.getExpiration(),user.getLoginDate(),claims.getIssuedAt())){
Result result = ResultGenerator.genFailResult(ResultCode.UNAUTHORIZED,"token失效,请重新登录");
SendMsgUtil.sendJsonMessage(response,result);
return false;
}
场景:用户登陆后,token的过期时间为30分钟,如果在这30分钟内没有操作,则重新登录,如果30分钟内有操作,就给token自动续一个新的时间。避免用户正在操作时掉线重登!
实现①:在jwt生成token时先不设置过期时间,过期时间的操作放在redis中。()这一次官网项目好像就有这个)
实现②:使用access_token、refresh_token 解决
登录获取token(包括访问令牌access_token
,刷新令牌refresh_token
),其中access_token设置过期时间为5分钟,refresh_token设置过期时间为30分钟。不能同时过期
前端保存access_token
和refresh_token
,每次请求带着access_token
去访问服务器资源
服务器校验access_token
有效性,通过解析access_token
看是否能解析出用户信息。如果用户信息为null
,说明token
无效,返回401
,让用户重新登录
服务器端校验access_token
是否过期
如果access_token
没有过期,则token正常,继续执行业务逻辑
如果access_token
过期,计算 过期后到当前的时间大小 是否在refresh_token
过期时间之内(是否大于30 - 5 - 5 = 20
分钟,为什么不是30 - 5 = 25
分钟呢?主要是想对正在请求的用户token做一个缓存,保证在最后五分钟内,新、老token都有效!防止正在进行的请求token突然失效!),
refresh_token
的过期时间,则表示用户长时间无操作,token真正过期了,返回401,让用户重新登录如果小于refresh_token
的过期时间,则继续让该access_token
访问业务,但返回给前端标识,提示token已过期,让前端带着refresh_token
去服务器获取新的access_token
,并保存在前端,后续使用新的access_token
去访问!
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import com.google.common.io.BaseEncoding;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtBuilder;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@Component
public class JwtUtils {
private static long tokenExpiredTime;
private static String jwtId;
private static String jwtSecret;
/**
* 创建JWT
*/
public static String createJWT(Map<String, Object> claims, Long time) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
Date now = new Date(System.currentTimeMillis());
SecretKey secretKey = generalKey();
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(jwtId)
//iat: jwt的签发时间
.setIssuedAt(now)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, secretKey);
if (time >= 0) {
long expMillis = nowMillis + time;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* 验证jwt
*/
public static Claims verifyJwt(String token) {
//签名秘钥,和生成的签名的秘钥一模一样
SecretKey key = generalKey();
Claims claims;
try {
claims = Jwts.parser() //得到DefaultJwtParser
.setSigningKey(key) //设置签名的秘钥
.parseClaimsJws(token).getBody();
} catch (Exception e) {
claims = null;
}//设置需要解析的jwt
return claims;
}
/**
* 由字符串生成加密key
*
* @return SecretKey
*/
public static SecretKey generalKey() {
String stringKey = jwtSecret;
byte[] encodedKey = BaseEncoding.base64().decode(stringKey);
SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "HmacSHA256");
return key;
}
/**
* 根据userId和openid生成token
*/
public static String generateToken(String openId) {
Map<String, Object> map = new HashMap<>();
map.put("openId", openId);
return createJWT(map, tokenExpiredTime);
}
@Value("${jwt.token-expired-time}")
public void setTokenExpiredTime(long tokenExpiredTime) {
JwtUtils.tokenExpiredTime = tokenExpiredTime;
}
@Value("${jwt.id}")
public void setJwtId(String jwtId) {
JwtUtils.jwtId = jwtId;
}
@Value("${jwt.secret}")
public void setJwtSecret(String jwtSecret) {
JwtUtils.jwtSecret = jwtSecret;
}
public static long getTokenExpiredTime() {
return tokenExpiredTime;
}
public static String getJwtId() {
return jwtId;
}
public static String getJwtSecret() {
return jwtSecret;
}
}
搭建springboot+mybatis-plus+jwt环境
引入依赖:
<dependencies>
<dependency>
<groupId>com.google.guavagroupId>
<artifactId>guavaartifactId>
<version>31.0.1-jreversion>
dependency>
<dependency>
<groupId>com.auth0groupId>
<artifactId>java-jwtartifactId>
<version>3.4.0version>
dependency>
<dependency>
<groupId>org.projectlombokgroupId>
<artifactId>lombokartifactId>
<version>1.18.22version>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.6version>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
dependency>
<dependency>
<groupId>io.jsonwebtokengroupId>
<artifactId>jjwt-apiartifactId>
<version>0.10.7version>
<scope>compilescope>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-testartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<scope>testscope>
dependency>
<dependency>
<groupId>com.fenggroupId>
<artifactId>springbootmybatisplusartifactId>
<version>0.0.1-SNAPSHOTversion>
<scope>compilescope>
dependency>
dependencies>
yaml中的配置信息:
server:
port: 8989
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/mybatis?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: nihao123
druid:
initialSize: 10 #初始化连接个数
minIdle: 10 #最小空闲连接个数
maxActive: 100 #最大连接个数
maxWait: 60000 #获取连接时最大等待时间,单位毫秒。
timeBetweenEvictionRunsMillis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
minEvictableIdleTimeMillis: 30000 #配置一个连接在池中最小生存的时间,单位是毫秒
validationQuery: select 'x' #用来检测连接是否有效的sql,要求是一个查询语句。
testWhileIdle: true #建议配置为true,不影响性能,并且保证安全性。如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testOnBorrow: true #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
testOnReturn: false #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
poolPreparedStatements: false #是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
maxPoolPreparedStatementPerConnectionSize: -1 #要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
filters: stat,wall #通过别名的方式配置扩展插件,常用的插件有:监控统计用的filter:stat,日志用的filter:log4j,防御sql注入的filter:wall
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=5000 # 通过connectProperties属性来打开mergeSql功能;慢SQL记录
useGlobalDataSourceStat: false # 合并多个DruidDataSource的监控数据
jwt:
#设置token的过期时间,单位为秒
token-expired-time: 36000 #10小时
#设置token的id
id: tokenId
#设置密钥
secret: aPbOBbnH4gnZBzIYEY7mxWNu49kYljNPMeva9Fjrwwqzw0bFlO0kPXZTCGaVcw0j
#
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
mapper-locations: classpath:com/feng/mapper/xml/*.xml
global-config:
db-config:
logic-delete-value: 1
logic-not-delete-value: 0
id-type: auto
创建一个简单的数据表
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`name` varchar(80) DEFAULT NULL COMMENT '用户名',
`password` varchar(40) DEFAULT NULL COMMENT '用户密码',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
记得插入账号和密码信息
entity类
import lombok.Data;
import lombok.experimental.Accessors;
/**
* @Description:
* @Author Ladidol
* @Date: 2022/3/21 11:01
* @Version 1.0
*/
@Data
@Accessors(chain=true)//这是干啥用的哦
public class User {
private String id;
private String name;
private String password;
}
service里面的
public interface UserService extends IService<User> {
User login(User user);//登录接口
}
@Service
@Transactional
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
@Resource
UserMapper userMapper;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user) {
User userDB = userMapper.login(user);
if(userDB!=null){
return userDB;
}
throw new RuntimeException("登录失败~~");
}
}
mapper里面:
@Mapper
public interface UserMapper extends BaseMapper<User> {
User login(User user);
}
xml文件中
DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.feng.mapper.UserMapper">
<select id="login" parameterType="com.feng.entity.User" resultType="com.feng.entity.User">
select *
from user
where name = #{name}
and password = #{password}
select>
mapper>
controller类
@RestController
@RequestMapping
@Slf4j
public class UserController {
@Autowired
private UserService userService;
/**
* @Description: 登录判断,得到一个token
* @Author: ladidol
* @Date: 2022/3/21 13:40
* @Param: [user]
* @Return: java.util.Map
*/
@GetMapping("/user/login")
public Map<String,Object> login(@RequestBody User user) {
Map<String,Object> result = new HashMap<>();
log.info("用户名: [{}]", user.getName());
log.info("密码: [{}]", user.getPassword());
try {
User userDB = userService.login(user);
Map<String, Object> map = new HashMap<>();//用来存放payload
map.put("id",userDB.getId());
map.put("username", userDB.getName());
String token = JwtUtils.createJWT(map,10000000L);
result.put("state",true);
result.put("msg","登录成功!!!");
result.put("token",token); //成功返回token信息
} catch (Exception e) {
e.printStackTrace();
result.put("state","false");
result.put("msg",e.getMessage());
}
return result;
}
/**
* @Description: 再次判断token
* @Author: ladidol
* @Date: 2022/3/21 13:40
* @Param: [token]
* @Return: java.util.Map
*/
@GetMapping("/test/test")
public Map<String, Object> test(String token) {
Map<String, Object> map = new HashMap<>();
try {
JwtUtils.verifyJwt(token);
map.put("msg", "验证通过~~~");
map.put("state", 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~~");
}
return map;
}
}
添加拦截器:
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("token");
Map<String,Object> map = new HashMap<>();
try {
JWTUtils.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;
}
@Component
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JwtTokenInterceptor()).
excludePathPatterns("/user/**")
.addPathPatterns("/**");
}
}
可以在postman里面测试:
登录接口:
测试接口: