SpringBoot 使用 JWT
登录方式对比
传统登录方式 Session + Cookie
- 客户端向服务器发送用户名和密码
- 服务器验证通过,并把相关数据保存在 Session 中,例如登录时间之类的
- 服务器返回给用户一个 SessionId ,客户端把这个 SessionId 写入 Cookie
- 用户每次请求都会通过 Cookie 提交 SessionId 到服务器
- 服务器收到 SessionId 后查找数据,就可以知道用户身份
特点:
- 数据存储在服务器,安全性较强,但是占用服务器资源
- 因为使用到了 Cookie ,所以会被伪造
- 如果服务器较多,或者跨域访问之类的操作,就要求共享 Session 资源,否则就需要用户和服务器重复登录验证操作,或者记录用户登录的服务器,对服务器和用户体验都不好。
JWT 方式登录
- 客户端向服务器发送用户名和密码
- 服务器验证通过,对用户数据进行加密,生成 Token 返回给客户端
- 浏览器(客户端)接收到 Token 后,将 Token 存储在 Local Storage,需要使用 JavaScript 代码获取,而 Cookie 是自动携带
- 用户每次请求都把 Token 提交到服务器
- 服务器对传来的 Token 进行解密,再去查询用户数据,一次知道用户身份
特点:
- 存储在客户端,不占用服务器资源,但是同样会被伪造
- 前后端分离,带上 Token 进行请求,不需要考虑用户是在哪个服务器上登录的,多服务器和跨域请求都没有问题
建议:对数据库的增删改,必须加上 Token 验证,查询不加 Token ,这样效率会比较高,同时查询操作也无法获取 Token ,更安全
如何强制token失效?
在数据库里保存一份 Token ,验证时再拿出来校验,重新登录就刷新覆盖这个值
JWT
JSON Web Token(JWT)是目前最流行的跨域身份验证解决方案。
先看概念
官网:https://jwt.io/
这张图来自官网
JWT 结构
JWT 分为三个部分
- Header,算法和令牌类型
{
"alg": "HS256",
"typ": "JWT"
}
- Payload:数据,实际需要传递的 JSON 对象
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
- Verify Signature:签名,用于防伪验证
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
加密之后的结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
终于可以上代码了
代码
新建一个 SpringBoot 项目
application.properties ,我只配置了最基本的
#配置程序端口,默认为8080
server.port= 8080
# 配置默认访问路径,默认为/
server.servlet.context-path=/jwt_demo
# 配置 Tomcat
# 配置Tomcat编码,默认为UTF-8
server.tomcat.uri-encoding=UTF-8
# 配置最大线程数
server.tomcat.max-threads=1000
JWT 依赖库
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
模型类
因为我要用 RESTful 所以可以用一个模型类
public class UserModel
{
private String _username;
private String _password;
public String get_username()
{
return _username;
}
public void set_username(String _username)
{
this._username = _username;
}
public String get_password()
{
return _password;
}
public void set_password(String _password)
{
this._password = _password;
}
}
控制器
DemoController ,用于测试访问数据,Foo_01()不需要Token,Foo_02()需要 Token
@RestController
@RequestMapping("/v1/Demo")
public class DemoController
{
@RequestMapping(value = "/Get1",method = RequestMethod.GET,produces = "application/json")
public List<String> Foo_01()
{
List<String> list=new ArrayList<>();
list.add("Foo_01");
list.add("Test");
return list;
}
@RequestMapping("/Get2")
public List<String> Foo_02()
{
List<String> list=new ArrayList<>();
list.add("Foo_02");
list.add("Test");
return list;
}
}
LoginController ,用于登录并获取 Token
@RestController
@RequestMapping("/v1/Login")
public class LoginController
{
@RequestMapping(value = "/Login", method = RequestMethod.POST, produces = "application/json")
public String Login(@RequestBody UserModel user)
{
if (user.get_username().equals("abc") && user.get_password().equals("123456"))
{
Map<String, String> claimMap = new HashMap<>();
claimMap.put("username", "abc");
return TokenUtli.GenerateToken(claimMap);
}
return "登录失败";
}
}
封装 JWT 工具类
其实我封装的很随便啦,仅用于本案例
public class TokenUtli
{
//Issuer
public static final String ISSUER = "Test.com";
//Audience
public static final String AUDIENCE = "Client";
//密钥
public static final String KEY = "ThisIsMySecretKey";
//算法
public static final Algorithm ALGORITHM = Algorithm.HMAC256(TokenUtli.KEY);
//Header
public static final Map<String, Object> HEADER_MAP = new HashMap<>()
{
{
put("alg", "HS256");
put("typ", "JWT");
}
};
/**
* 生成 Token 字符串
*
* @param claimMap claim 数据
* @return Token 字符串
*/
public static String GenerateToken(Map<String, String> claimMap)
{
Date nowDate = new Date();
//120 分钟过期
Date expireDate = TokenUtli.AddDate(nowDate, 2 * 60);
//Token 建造器
JWTCreator.Builder tokenBuilder = JWT.create();
for (Map.Entry<String, String> entry : claimMap.entrySet())
{
//Payload 部分,根据需求添加
tokenBuilder.withClaim(entry.getKey(), entry.getValue());
}
//token 字符串
String token = tokenBuilder.withHeader(TokenUtli.HEADER_MAP)//Header 部分
.withIssuer(TokenUtli.ISSUER)//issuer
.withAudience(TokenUtli.AUDIENCE)//audience
.withIssuedAt(nowDate)//生效时间
.withExpiresAt(expireDate)//过期时间
.sign(TokenUtli.ALGORITHM);//签名,算法加密
return token;
}
/**
* 时间加法
*
* @param date 当前时间
* @param minute 持续时间(分钟)
* @return 时间加法结果
*/
private static Date AddDate(Date date, Integer minute)
{
if (null == date)
{
date = new Date();
}
Calendar calendar = new GregorianCalendar();
calendar.setTime(date);
calendar.add(Calendar.MINUTE, minute);
return calendar.getTime();
}
}
在此示例中,我们指定了必须考虑哪些参数才能将 JWT 视为有效。根据我们的代码,以下项目认为令牌有效:
- 验证生成令牌的服务器 Issuer
- 验证令牌的接收者被授权接收 Audience
- 检查令牌是否未过期以及颁发者的签名密钥是否有效 Lifetime
- 验证令牌的签名 IssuerSigningKey
- 此外,我们指定Issuer、Audience、SigningKey的值。在本例中,我将这些值存储在常量中。
验证 Token ,Token 错误或者过期就抛出异常,这里我就只判断了一个空字符串,其它验证规则可以自己写啦
/**
* 验证 Token
*
* @param webToken 前端传递的 Token 字符串
* @return Token 字符串是否正确
* @throws Exception 异常信息
*/
public static boolean VerifyJWTToken(String webToken) throws Exception
{
String[] token = webToken.split(" ");
if (token[1].equals(""))
{
throw new Exception("token错误");
}
//JWT验证器
JWTVerifier verifier = JWT.require(TokenUtli.ALGORITHM).withIssuer(TokenUtli.ISSUER).build();
//解码
DecodedJWT jwt = verifier.verify(token[1]);
//Audience
List<String> audienceList = jwt.getAudience();
String audience = audienceList.get(0);
//Payload
Map<String, Claim> claimMap = jwt.getClaims();
for (Map.Entry<String, Claim> entry : claimMap.entrySet())
{
}
//生效时间
Date issueTime = jwt.getIssuedAt();
//过期时间
Date expiresTime = jwt.getExpiresAt();
return true;
}
注册拦截器
JWTInterceptor
public class JWTInterceptor implements HandlerInterceptor
{
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception
{
//从请求头内获取token
String token = request.getHeader("authorization");
//验证令牌,如果令牌不正确会出现异常会被全局异常处理
return TokenUtli.VerifyJWTToken(token);
}
}
注册拦截器
@Configuration
public class InterceptorConfig implements WebMvcConfigurer
{
@Override
public void addInterceptors(InterceptorRegistry registry)
{
registry.addInterceptor(new JWTInterceptor()).addPathPatterns("/**")//全部路径
.excludePathPatterns("/v1/Demo/Get1")//排除不需要Token的路径
.excludePathPatterns("/v1/Login/Login");//开放登录路径
}
}
测试
因为懒得写前端页面,所以使用 postman 调试
首先,不登录去访问 DemoController 里的两个函数
报了 500错误
登录,以及生成的 Token
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiJDbGllbnQiLCJpc3MiOiJUZXN0LmNvbSIsImV4cCI6MTYyNjY4OTczMiwiaWF0IjoxNjI2NjgyNTMyLCJ1c2VybmFtZSI6ImFiYyJ9.ZtSyoqmUVKgGY3_wFQD24_mOot7qnMUKh8xMPKOW2JQ
使用 Token 再去测试需要 Token 的函数
这次就访问到数据了
SpringBoot 使用 JWT 结束
项目结构