什么是jwt
首先jwt其实是三个英语单词JSON Web Token的缩写。通过全名你可能就有一个基本的认知了。token一般都是用来认证的,比如我们系统中常用的用户登录token可以用来认证该用户是否登录。jwt也是经常作为一种安全的token使用。
JWT的定义:
JWT是一种用于双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT作为一个开放的标准(RFC 7519),定义了一种简洁的,自包含的方法用于通信双方之间以Json对象的形式安全的传递信息。因为数字签名的存在,这些信息是可信的,JWT可以使用HMAC算法或者是RSA的公私秘钥对进行签名。
JWT特点:
简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快
自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库
简单来说:
JWT
全称JSON Web Token
,简单的说就是用户登录成功之后,将用户的信息进行加密,然后生成一个token
返回给客户端
JWT可以做什么
授权:这是使用 JWT 最常见的场景。用户登录后,每个后续请求都将包含 JWT,从而允许用户访问该令牌允许的路由、服务和资源。单点登录是当今广泛使用 JWT 的一项功能,因为它的开销很小并且能够在不同的域中轻松使用。
信息交换:JSON Web 令牌是在各方之间安全传输信息的好方法。因为可以对 JWT 进行签名(例如,使用公钥/私钥对),所以可以确定发件人就是他们所说的那个人。此外,由于使用标头和有效负载计算签名,还可以验证内容没有被篡改。
为什么是jwt
基于传统的session
认证方式
我们知道,http协议本身是一种无状态的协议,而这就意味着如果用户向我们的应用提供了
用户名和密码来进行用户认证,那么下一次请求时,用户还要进行一次用户认证才行,因为
根据http协议,我们并不知道是那个用户发出的请求。所以为了让我们的应用能识别是那个
用户发出的请求,我们只能在服务器存储一份用户的登陆信息,这份登陆信息会在响应时传
递给浏览器,告诉其保存为cookie,以便下次请求发送给我们的应用,这样我们的应用就能
识别请求来自那个用户了,这就是传统的基于session认证
认证流程
交互流程
暴露问题
1.每个用户经过我们的应用认证之后,我们的应用都是要在服务器端做一次记录,以方便用户下次请求的鉴别,通常而言session都是保存在内存中的,而随着认证用户的增多,服务器的压力开销就会明显增大
2,用户认证之后,服务端做认证记录,如果认证的记录被保存在内存中的话,这意味着用户下次请求还必须要请求在这台服务器上,这样才能拿到授权的资源,这样在分布式的应用上,相应限制了负载均衡的能力,遮这也意味着限制了应用的扩展。
3,因为是基于cookie来进行用户识别,cokkie如果被拦截,用就很容易受到跨站请求伪造的攻击
4,前后盾分离系统中更痛苦,
- 前后端分离在应用解耦后增加了部署的复杂性。通常用户一次请求就要转发多次,如果用session每次携带sessionid 到服务器,服务器还要查询用户信息。
- 同时如果用户很多,这些信息存储在服务器内存中,给服务器增加负担。
- 容易受到CSRF(跨站伪造请求攻击)攻击,session是基于cookie进行用户识别的,cookie如果被拦截,用户就会很容易受到跨站请求伪造的攻击。
- sessionid就是一个特征值,表达的信息不够准确,不容易扩展。而且如果你后端应用是多节点部署。那么就需要实现session共享机制,不方便集群
认证:
用户认证是判断一个用户的身份是否合法的过程,用户去访问系统资源时系统要求验证用户的身份信 息,身份合法方可继续访问,不合法则拒绝访问。常见的用户身份认证方式有:用户名密码登录,二维码登录,手 机短信登录,指纹认证等方式。
会话:
用户认证通过后,为了避免用户的每次操作都进行认证可将用户的信息保证在会话中。会话就是系统为了保持当前 用户的登录状态所提供的机制,常见的有基于session方式、基于token方式等。
session认证方式‘
它的交互流程是,用户认证成功后,在服务端生成用户相关的数据保存在session(当前会话)中,发给客户端的
sesssion_id 存放到 cookie 中,这样用户客户端请求时带上 session_id 就可以验证服务器端是否存在 session 数
据,以此完成用户的合法校验,当用户退出系统或session过期销毁时,客户端的session_id也就无效了。
授权:
授权是用户认证通过根据用户的权限来控制用户访问资源的过程,拥有资源的访问权限则正常访问,没有 权限则拒绝访问。
JWT的结构是什么
令牌的组成
1. 标头(Header)
2,有效荷载(Payload)
3,签名(Stringture)
因此,JWT通常为 Header.Payload,Signature
Header
- 标头通常由两部分组成:令牌的类型(即JWT)和所使用的签名算法,例如HMAC SHA256 或RSA 。他会使用Base64 编码组成JWT结构的第一部分
- 注意:Base64 是一种编码,也就是说,它是可以被翻译会原来的样子。并不是一种加密过程
{
"alg":"HS256",
"typ":"JWT"
}
Payload(不要把敏感信息放在这里,会被解密)
- 令牌的第二部分是有效负载,其中包含声明。声明是有关实体(通常是用户)和其他数据的
声明。同样的,他会使用Base64编码组成JWT结构的第二部分
{
"sub":"12345",
"name":"John Doe",
"admin":true
}
Signature
- 前两部分都是使用Base64进行编码的,即前端可以解开知道里面的信息。Signature需要使
用编码后的Header和payload以及我们提供的一个秘钥,然后使用haeder中指定的算法(HS256)
进行签名,签名的作用是保证JWT没有被篡改过
签名的目的
-保证内容没有被篡改过
最后一步签名的过程实际上对头部以及负载内容进行签名,防止内容被更改。第三部分的内容是
第一部分加第二部分和加密盐生成的,如果更改了前面的内容生成的签名就会发生改变。
信息安全
- 在这里我们一定会问一个问题:Base64是一种编码,是可逆的,那我们的数据不就被暴露了吗
- 是的,因为我们一般不会将敏感信息放在负载里面。因此JWT适用于向web应用传递非敏感信息。
JWT还用于设计用户的认证和授权系统,甚至实现web应用单点登陆
使用jwt
1.引入依赖
<!-- https://mvnrepository.com/artifact/com.auth0/java-jwt -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.1</version>
</dependency>
2.生成token
public static void main(String[] args) {
HashMap<String, Object> map = new HashMap<>();
Calendar instance = Calendar.getInstance();
instance.add(Calendar.SECOND,98);
//生成令牌
String token=JWT.create()
.withHeader(map)//header
.withClaim("userId",21)//设置自定义用户id
.withClaim("username","张三")//设置自定义用户名
.withExpiresAt(instance.getTime())//设置过期时间
.sign(Algorithm.HMAC256("token!Q2M#E$RW"));//设置签名 保留 复杂
//输出打印
System.out.println(token);
}
根据令牌和签名解析数据
//验证对象
public static void main(String[] args) {
//创建验证对象
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("token!Q2M#E$RW")).build();
DecodedJWT verify = jwtVerifier.verify("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJleHAiOjE2NjY1MDY2NDgsInVzZXJJZCI6MjEsInVzZXJuYW1lIjoi5byg5LiJIn0.erWPVY6un_1a9-l1IYEibQW_XCYYmTIjQE4Iqkw9Iz0");
System.out.println(verify.getClaim("userid").asInt());
System.out.println(verify.getClaim("username").asString());
System.out.println("过期时间"+verify.getExpiresAt());
// System.out.println(verify.getClaim("username").asString());
// System.out.println(verify.getClaims().get("userid").asString());
// System.out.println(verify.getClaims().get("username").asString());
}
生成令牌/解析令牌(签名算法和密钥要相同,否则会抛异常)案例
public static void main(String[] args) {
// 生成令牌
Map<String, Object> map = new HashMap<>();
map.put("id", 100);
map.put("name", "李四");
String token = JWT.create()
.withClaim("userId", 1) // 设置payload
.withClaim("userName", "张三")
.withClaim("user", map) // 自定义Map
.withIssuedAt(new Date()) // 令牌生成时间
.withExpiresAt(new Date(System.currentTimeMillis() + 8 * 60 * 60 * 1000)) // 令牌过期时间(8小时有效)
.sign(Algorithm.HMAC256("Test666"));// 设置签名算法和密钥
System.out.println("token = " + token);
// 解析令牌
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("Test666")).build();
DecodedJWT verify = jwtVerifier.verify(token);
System.out.println("解析结果:" + verify.getClaim("userId").asInt());
System.out.println("解析结果:" + verify.getClaim("userName").asString());
System.out.println("解析结果:" + verify.getClaim("user").asMap());
System.out.println("解析结果:" + verify.getClaim("user").asMap().get("id"));
System.out.println("解析结果:" + verify.getClaim("user").asMap().get("name"));
}
JWT工具类封装
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static String TOKEN="token!QM3#e4r";
/*
* 生成token
* */
public static String getToken(Map<String,String>map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//默认7天过期
instance.add(Calendar.MINUTE,30);// 有效时间:30分钟
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
}
/*
* 生成token写法2
* */
public static String getToken1(Map<String,String>map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//默认7天过期
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token= builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(TOKEN));
return token;
}
/*
* 验证token
* 原本写法
* public static void verity(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
* */
//优化
public static DecodedJWT verity(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
/*
*获取token中payload
* */
/* public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}*/
}
Springboot整合jwt
<!--引入jwt-->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.19.2</version>
</dependency>
<!--使用mybatis-->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.2</version>
</dependency>
<!--使用lombok-->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!--引入druid-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.9</version>
</dependency>
<!--使用sql-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
配置文件
server.port=9000
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/cs?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true
spring.datasource.username=root
spring.datasource.password=root
mybatis.type-aliases-package=com.xcuin.jwt
mybatis.mapper-locations=classpath:com/xcuin/jwt/mapper/*.xml
logging.level.com.xcuin.jwt.dao=debug
实体类
import lombok.Data;
import lombok.experimental.Accessors;
@Data
@Accessors(chain = true)
public class User {
private String id;
private String name;
private String password;
}
Dao+Dao.xml层
UserDao
import com.xcuin.jwt.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserDao {
//直接根据用户名和密码登录
User login(User user);
}
UserDao.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.xcuin.jwt.Dao.UserDao">
<select id="login" parameterType="User" resultType="User">
select * from user where name=#{name} and
password =#{password}
</select>
</mapper>
UserService+UserServiceImpl层
userService
public interface UserService {
User login(User user);//登录接口
}
userServiceImpl
import com.xcuin.jwt.Dao.UserDao;
import com.xcuin.jwt.entity.User;
import com.xcuin.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user){
//根据接收用户名密码查询数据库
User login = userDao.login(user);
if (login!=null){
return login;
}
throw new RuntimeException("登陆失败");
}
}
Controller层
import com.xcuin.jwt.Dao.UserDao;
import com.xcuin.jwt.entity.User;
import com.xcuin.jwt.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@Service
@Transactional
public class UserServiceImpl implements UserService {
@Autowired
private UserDao userDao;
@Override
@Transactional(propagation = Propagation.SUPPORTS)
public User login(User user){
//根据接收用户名密码查询数据库
User login = userDao.login(user);
if (login!=null){
return login;
}
throw new RuntimeException("登陆失败");
}
}
工具类
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.util.Calendar;
import java.util.Map;
public class JWTUtils {
private static String TOKEN="token!QM3#e4r";
/*
* 生成token
* */
public static String getToken(Map<String,String>map){
JWTCreator.Builder builder = JWT.create();
map.forEach((k,v)->{
builder.withClaim(k,v);
});
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//默认7天过期
builder.withExpiresAt(instance.getTime());
return builder.sign(Algorithm.HMAC256(TOKEN)).toString();
}
/*
* 生成token写法2
* */
/* public static String getToken1(Map<String,String>map){
Calendar instance = Calendar.getInstance();
instance.add(Calendar.DATE,7);//默认7天过期
//创建jwt builder
JWTCreator.Builder builder = JWT.create();
//payload
map.forEach((k,v)->{
builder.withClaim(k,v);
});
String token= builder.withExpiresAt(instance.getTime())
.sign(Algorithm.HMAC256(TOKEN));
return token;
}*/
/*
* 验证token
* 原本写法
* public static void verity(String token){
JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
* */
//优化验证token+获取token中payload
public static DecodedJWT verity(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}
/*
*获取token中payload
* */
/* public static DecodedJWT getToken(String token){
return JWT.require(Algorithm.HMAC256(TOKEN)).build().verify(token);
}*/
}
增加回响token
import com.xcuin.jwt.entity.User;
import com.xcuin.jwt.service.UserService;
import com.xcuin.jwt.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/login")
public Map<String,Object>login(User user){
log.info("用户名:{{}}",user.getName());
log.info("用户名:{{}}",user.getPassword());
HashMap<String, Object> map = new HashMap<>();
try {
User login = userService.login(user);
HashMap<String, String> payload = new HashMap<>();
payload.put("id",login.getId());
payload.put("name",login.getName());
//生成jwt的令牌
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);
} catch (Exception e) {
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
}
新增加拦截器
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureVerificationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.xcuin.jwt.utils.JWTUtils;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.HashMap;
/**
* 定义拦截器
* @Date: 2022/04/14 12:26
* @Version 1.0
**/
@Component
public class JWTInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws IOException {
HashMap<String, Object> map = new HashMap<>();
//获取请求头中的令牌
String token = request.getHeader("token");
try {
JWTUtils.verity(token);
return true;//放行请求
/*
常见异常
SignatureVerificationException: 签名不一致异常
TokenExpiredException: 令牌过期异常
AlgorithmMismatchException: 算法不匹配异常
*/
} catch (SignatureVerificationException e) {
e.printStackTrace();
map.put("msg","无效签名");
} catch (TokenExpiredException e) {
e.printStackTrace();
map.put("msg","token过期");
} catch (AlgorithmMismatchException e){
e.printStackTrace();
map.put("msg","token算法不一致");
}catch (Exception e){
e.printStackTrace();
map.put("msg","token无效");
}
map.put("state",false);//设置状态
//将map转为json jackjon
String json = new ObjectMapper().writeValueAsString(map);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
return false;
}
}
配置拦截器:新建InterceptorConfig类
import com.xcuin.jwt.interceptor.JWTInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@Configuration
public class InterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new JWTInterceptor()) // 注册拦截器
.addPathPatterns("/**") // 设置需要拦截的请求(也可设置成list集合)
.excludePathPatterns("/**"); // 设置不需要拦截的请求(也可设置成list集合)
}
}
优化原先的接口
import com.auth0.jwt.interfaces.DecodedJWT;
import com.xcuin.jwt.entity.User;
import com.xcuin.jwt.service.UserService;
import com.xcuin.jwt.utils.JWTUtils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@RestController
@Slf4j
public class UserController {
@Autowired
private UserService userService;
@GetMapping("/user/login")
public Map<String,Object>login(User user){
log.info("用户名:{{}}",user.getName());
log.info("用户名:{{}}",user.getPassword());
HashMap<String, Object> map = new HashMap<>();
try {
User login = userService.login(user);
HashMap<String, String> payload = new HashMap<>();
payload.put("id",login.getId());
payload.put("name",login.getName());
//生成jwt的令牌
String token = JWTUtils.getToken(payload);
map.put("state",true);
map.put("msg","认证成功");
map.put("token",token);
} catch (Exception e) {
map.put("state",false);
map.put("msg",e.getMessage());
}
return map;
}
@PostMapping("/user/test")
Map<String,Object>test(HttpServletRequest request){
HashMap<String, Object> map = new HashMap<>();
//除理自己的业务
String token = request.getHeader("token");
DecodedJWT verity = JWTUtils.verity(token);
log.info("用户id:{{}}",verity.getClaim("id").asString());
log.info("用户name:{{}}",verity.getClaim("name").asString());
map.put("state",true);
map.put("msg","请求成功");
return map;
}
}
扩展:
扩展1:
工具类的编写
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.exceptions.JWTDecodeException;
import com.auth0.jwt.interfaces.Claim;
import com.auth0.jwt.interfaces.DecodedJWT;
import java.io.Serializable;
import java.util.Calendar;
import java.util.Date;
/**
* @author Lehr
* @create: 2020-02-04
*/
public class JwtUtils {
/**
签发对象:这个用户的id
签发时间:现在
有效时间:30分钟
载荷内容:暂时设计为:这个人的名字,这个人的昵称
加密密钥:这个人的id加上一串字符串
*/
public static String createToken(String userId,String realName, String userName) {
Calendar nowTime = Calendar.getInstance();
nowTime.add(Calendar.MINUTE,30);
Date expiresDate = nowTime.getTime();
return JWT.create().withAudience(userId) //签发对象
.withIssuedAt(new Date()) //发行时间
.withExpiresAt(expiresDate) //有效时间
.withClaim("userName", userName) //载荷,随便写几个都可以
.withClaim("realName", realName)
.sign(Algorithm.HMAC256(userId+"HelloLehr")); //加密
}
/**
* 检验合法性,其中secret参数就应该传入的是用户的id
* @param token
* @throws TokenUnavailable
*/
public static void verifyToken(String token, String secret) throws TokenUnavailable {
DecodedJWT jwt = null;
try {
JWTVerifier verifier = JWT.require(Algorithm.HMAC256(secret+"HelloLehr")).build();
jwt = verifier.verify(token);
} catch (Exception e) {
//效验失败
//这里抛出的异常是我自定义的一个异常,你也可以写成别的
throw new TokenUnavailable();
}
}
/**
* 获取签发对象
*/
public static String getAudience(String token) throws TokenUnavailable {
String audience = null;
try {
audience = JWT.decode(token).getAudience().get(0);
} catch (JWTDecodeException j) {
//这里是token解析失败
throw new TokenUnavailable();
}
return audience;
}
/**
* 通过载荷名字获取载荷的值
*/
public static Claim getClaimByName(String token, String name){
return JWT.decode(token).getClaim(name);
}
}
注解类的编写
在controller层上的每个方法上,可以使用这些注解,来决定访问这个方法是否需要携带token,由于默认是全部检查,所以对于某些特殊接口需要有免验证注解
免验证注解
@PassToken
:跳过验证,通常是入口方法上用这个,比如登录接口
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @author Lehr
* @create: 2020-02-03
*/
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface PassToken {
boolean required() default true;
}
拦截器的编写
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author lehr
*/
@Configuration
public class JwtInterceptorConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
//默认拦截所有路径
registry.addInterceptor(authenticationInterceptor())
.addPathPatterns("/**");
}
@Bean
public JwtAuthenticationInterceptor authenticationInterceptor() {
return new JwtAuthenticationInterceptor();
}
}
配置拦截器:新建InterceptorConfig类
import com.auth0.jwt.interfaces.Claim;
import com.imlehr.internship.annotation.PassToken;
import com.imlehr.internship.dto.AccountDTO;
import com.imlehr.internship.exception.NeedToLogin;
import com.imlehr.internship.exception.UserNotExist;
import com.imlehr.internship.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.reflect.Method;
import java.util.Map;
/**
* @author Lehr
* @create: 2020-02-03
*/
public class JwtAuthenticationInterceptor implements HandlerInterceptor {
@Autowired
AccountService accountService;
@Override
public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object object) throws Exception {
// 从请求头中取出 token 这里需要和前端约定好把jwt放到请求头一个叫token的地方
String token = httpServletRequest.getHeader("token");
// 如果不是映射到方法直接通过
if (!(object instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查是否有passtoken注释,有则跳过认证
if (method.isAnnotationPresent(PassToken.class)) {
PassToken passToken = method.getAnnotation(PassToken.class);
if (passToken.required()) {
return true;
}
}
//默认全部检查
else {
System.out.println("被jwt拦截需要验证");
// 执行认证
if (token == null) {
//这里其实是登录失效,没token了 这个错误也是我自定义的,读者需要自己修改
throw new NeedToLogin();
}
// 获取 token 中的 user Name
String userId = JwtUtils.getAudience(token);
//找找看是否有这个user 因为我们需要检查用户是否存在,读者可以自行修改逻辑
AccountDTO user = accountService.getByUserName(userId);
if (user == null) {
//这个错误也是我自定义的
throw new UserNotExist();
}
// 验证 token
JwtUtils.verifyToken(token, userId)
//获取载荷内容
String userName = JwtUtils.getClaimByName(token, "userName").asString();
String realName = JwtUtils.getClaimByName(token, "realName").asString();
//放入attribute以便后面调用
request.setAttribute("userName", userName);
request.setAttribute("realName", realName);
return true;
}
return true;
}
@Override
public void postHandle(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest httpServletRequest,
HttpServletResponse httpServletResponse,
Object o, Exception e) throws Exception {
}
}
这段代码的执行逻辑大概是这样的:
目标方法是否有注解?如果有PassToken的话就不用执行后面的验证直接放行,不然全部需要验证
开始验证:有没有token?没有?那么返回错误
从token的audience中获取签发对象,查看是否有这个用户(有可能客户端造假,有可能这个用户的账户被冻结了),查看用户的逻辑就是调用Service方法直接比对即可
检验Jwt的有效性,如果无效或者过期了就返回错误
Jwt有效性检验成功:把Jwt的载荷内容获取到,可以在接下来的controller层中直接使用了(具体使用方法看后面的代码)
接口的编写
这里设计了两个接口:登录和查询名字,来模拟一个迷你业务,其中后者需要登录之后才能使用,大致流程如下:
登录代码
/**
* 用户登录:获取账号密码并登录,如果不对就报错,对了就返回用户的登录信息
* 同时生成jwt返回给用户
*
* @return
* @throws LoginFailed 这个LoginFailed也是我自定义的
*/
@PassToken
@GetMapping(value = "/login")
public AccountVO login(String userName, String password) throws LoginFailed{
try{
service.login(userName,password);
}
catch (AuthenticationException e)
{
throw new LoginFailed();
}
//如果成功了,聚合需要返回的信息
AccountVO account = accountService.getAccountByUserName(userName);
//给分配一个token 然后返回
String jwtToken = JwtUtils.createToken(account);
//我的处理方式是把token放到accountVO里去了
account.setToken(jwtToken);
return account;
}
业务代码
这里列举一个需要登录,用来测试用户名字的接口(其中用户的名字来源于jwt的载荷部分)
@GetMapping(value = "/username")
public String checkName(HttpServletRequest req) {
//之前在拦截器里设置好的名字现在可以取出来直接用了
String name = (String) req.getAttribute("userName");
return name;
}
扩展2:
引入依赖jar:
<!-- JWT依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
添加请求拦截器WebConfig(实现WebMvcConfigurer重写addInterceptors),对请求url进行token认证
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Resource
private JwtFilter jwtFilter ;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtFilter).addPathPatterns("/**");
}
}
新增拦截器JwtFilter
@Component
public class JwtFilter extends HandlerInterceptorAdapter {
public static final String LOGIN_URL = "/login";
@Resource
private JwtTokenUtil jwtTokenUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws SignatureException {
String uri = request.getRequestURI();
if(uri.contains(LOGIN_URL) || uri.contains("/doc.html") || uri.contains("/swagger-resources") ){
return true;
}
//获取token
String token = request.getHeader(jwtTokenUtil.header);
if(StringUtils.isEmpty(token)){
token = request.getParameter(jwtTokenUtil.header);
}
if(StringUtils.isEmpty(token)){
throw new SignatureException(jwtTokenUtil.header+"不能为空");
}
//判断token是否超时
Claims claims = jwtTokenUtil.getTokenClaim(token);
if(null == claims || jwtTokenUtil.isTokenExpired(claims.getExpiration())){
throw new SignatureException(jwtTokenUtil.header+"失效,请重新登录");
}
return true;
}
}
异常处理类,springAop实现(异常通知)
@RestControllerAdvice
public class SecurityExceptionHandler {
@ExceptionHandler(value = {SignatureException.class})
public Result authorizationException(SignatureException e){
return Result.failedWith(null,CodeEnum.FORBBIDEN.getCode(),"权限不足");
}
}
tokenUtil工具类
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
public String secret;
@Value("${jwt.expire}")
public int expire;
@Value("${jwt.header}")
public String header;
/**
* 生成token
* @param subject
* @return
*/
public String createToken (String subject){
Date nowDate = new Date();
Date expireDate = new Date(nowDate.getTime() + expire * 1000);
return Jwts.builder()
.setHeaderParam("typ", "JWT")
.setSubject(subject)
.setIssuedAt(nowDate)
.setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
/**
* 获取token中注册信息
* @param token
* @return
*/
public Claims getTokenClaim (String token) {
try {
return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
}catch (Exception e){
return null;
}
}
/**
* 验证token是否过期失效
* @param expirationTime
* @return
*/
public boolean isTokenExpired (Date expirationTime) {
return expirationTime.before(new Date());
}
/**
* 获取token失效时间
* @param token
* @return
*/
public Date getExpirationDateFromToken(String token) {
return getTokenClaim(token).getExpiration();
}
/**
* 获取用户名从token中
*/
public String getUsernameFromToken(String token) {
return getTokenClaim(token).getSubject();
}
/**
* 获取jwt发布时间
*/
public Date getIssuedAtDateFromToken(String token) {
return getTokenClaim(token).getIssuedAt();
}
}
类中使用配置添加,application.yml中增加token的有效时间等
jwt:
# 加密密钥
secret: abcdefg1234567
# token有效时长
expire: 3600
# header 名称
header: token
jwt生成token使用
@Resource
JwtTokenUtil jwtTokenUtil;
@Override
public String login(String userName, String password) {
//验证用户名密码
......
//生成token
return jwtTokenUtil.createToken("admin");
}
扩展3:
引入依赖jar:
<!-- JWT依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.1</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
类中使用配置添加,application.yml中增加token的有效时间等
jwt:
# 加密密钥
secret: abcdefg1234567
# token有效时长
expire: 3600
# header 名称
header: token
启动类
这里选择使用@ServletComponentScan,是因为在Filter类用@component和@configuration会导致
@WebFilter(urlPatterns = “/testToken”, filterName = “jwtFilter”) url失效变成拦截所有
package com.example.bootjwt;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.web.servlet.ServletComponentScan;
@SpringBootApplication
@ServletComponentScan //这里是将filter扫描加载进spring容器
public class BootJwtApplication {
public static void main(String[] args) {
SpringApplication.run(BootJwtApplication.class, args);
}
}
实现代码
这里主要是做一个简单的demo验证,有三个类JwtController、CreatToken、JwtFilter。
JwtController:用来接收rest请求。
JwtUtil:用来生成token,解密token,验证token
JwtFilter:用来拦截请求对http请求中携带的token进行验证
JwtController
package com.example.bootjwt.controller;
import com.example.bootjwt.Util.JwtUtil;
import com.example.bootjwt.domain.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@RestController
public class JwtController {
@PostMapping("/get")
public String creatToken2(){
User user = new User();
user.setId("1");
user.setUsername("hand2020");
user.setPassword("123456");
return JwtUtil.createJWT(40000,user);
}
@PostMapping("/test")
public String testToken2(HttpServletRequest request, HttpServletResponse response){
String token= request.getHeader("Authorization");
User user = new User();
user.setId("1");
user.setUsername("hand2020");
user.setPassword("123456");
if (JwtUtil.isVerify(token,user)){
return "success";
}
return "fail";
}
}
JwtUtil
这里我是在配置文件中读需要加密的明文,和过期时间。也可以在controller里处理参数设置。
package com.example.bootjwt.Util;
import com.example.bootjwt.domain.User;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
public class JwtUtil {
// @Value("${jwt.secret}")
// private static String key;
/**
* 用户登录成功后生成Jwt
* 使用Hs256算法 私匙使用用户密码
*
* @param ttlMillis jwt过期时间
* @param user 登录成功的user对象
* @return
*/
public static String createJWT(long ttlMillis, User user) {
//指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//生成JWT的时间
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
//创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
Map<String, Object> claims = new HashMap<String, Object>();
claims.put("id", user.getId());
claims.put("username", user.getUsername());
claims.put("password", user.getPassword());
//生成签名的时候使用的秘钥secret,这个方法本地封装了的,一般可以从本地配置文件中读取,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
String key = user.getPassword();
//生成签发人
String subject = user.getUsername();
//下面就是在为payload添加各种标准声明和私有声明了
//这里其实就是new一个JwtBuilder,设置jwt的body
JwtBuilder builder = Jwts.builder()
//如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
.setClaims(claims)
//设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
.setId(UUID.randomUUID().toString())
//iat: jwt的签发时间
.setIssuedAt(now)
//代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
.setSubject(subject)
//设置签名使用的签名算法和签名使用的秘钥
.signWith(signatureAlgorithm, key);
if (ttlMillis >= 0) {
long expMillis = nowMillis + ttlMillis;
Date exp = new Date(expMillis);
//设置过期时间
builder.setExpiration(exp);
}
return builder.compact();
}
/**
* Token的解密
* @param token 加密后的token
* @param user 用户的对象
* @return
*/
public static Claims parseJWT(String token, User user) {
//签名秘钥,和生成的签名的秘钥一模一样
String key = user.getPassword();
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
return claims;
}
/**
* 校验token
* 在这里可以使用官方的校验,我这里校验的是token中携带的密码于数据库一致的话就校验通过
* @param token
* @param user
* @return
*/
public static Boolean isVerify(String token, User user) {
//签名秘钥,和生成的签名的秘钥一模一样
String key = user.getPassword();
//Jwts.parser在执行parseClaimsJws(token)时如果token时间过期会抛出ExpiredJwtException异常
try {
//得到DefaultJwtParser
Claims claims = Jwts.parser()
//设置签名的秘钥
.setSigningKey(key)
//设置需要解析的jwt
.parseClaimsJws(token).getBody();
if (claims.get("password").equals(user.getPassword())) {
return true;
}
}catch (ExpiredJwtException e){
e.printStackTrace();
}
return false;
}
}
JwtFilter
过滤器是通过实现Filter接口,注意@WebFilter相当于xml配置,但是需要在启动类上注解
@ServletComponentScan,将JwtFilter加入到spring容器中。
在JwtFilter类上注解@component或@configuration会导致@WebFilter失效从而拦截所有请求
目前这个没用到,直接在controller里做了判断,这个是后续业务需求的demo
package com.example.bootjwt;
import com.example.bootjwt.Util.JwtUtil;
import com.example.bootjwt.domain.User;
import org.springframework.beans.factory.annotation.Autowired;
import javax.servlet.*;
import javax.servlet.annotation.WebFilter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@WebFilter(urlPatterns = "/testToken", filterName = "jwtFilter")
public class JwtFilter implements Filter {
@Autowired
private CreatToken creatToken;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest)servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
response.setCharacterEncoding("UTF-8");
response.setContentType("application/json; charset=utf-8");
response.setHeader("Access-Control-Allow-Origin", "*");
String token= request.getHeader("Authorization");
User user = new User();
user.setId("1");
user.setUsername("hand2020");
user.setPassword("123456");
boolean flag = JwtUtil.isVerify(token,user);
if (flag){
filterChain.doFilter(servletRequest,servletResponse);
}else {
System.out.println("失败。。。。。。。。");
response.getWriter().write("失败。。。。。。。。");
}
}
@Override
public void destroy() {
}
}