赞
踩
本篇博客是SpringBoot集成token认证的案例,若文章中出现相关问题,请指出!
所有博客文件目录索引:博客目录索引(持续更新)
session问题
session出现的问题:
token鉴权机制
token的鉴权机制:http协议也是无状态的,不需要在服务端去保留用户的认证信息或者会话信息。这也就意味着基于token认证机制的应用不需要去考虑用户在哪一台服务器登录,为应用的扩展提供了遍历。
鉴权流程:简单来说就是服务器根据前端传来的用户名与密码生成token并返回前端,前端之后的请求都会携带该cookie来进行执行操作认证。
注意:这个token必须要在每次请求时传递给服务端,它应该保存在请求头里, 另外,服务端要支持CORS(跨来源资源共享)
策略,一般我们在服务端这么做就可以了Access-Control-Allow-Origin: *
。
JWT
(JSON WEB TOKEN)是由三段信息构成的,将这三段信息文本用.
链接一起就构成了Jwt字符串.
JWT字符串示例:JWT解析网站
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
第一部分:header
包含两部分信息:①声明类型,这里是jwt。②声明加密的算法 通常直接使用 HMAC SHA256。
完整的头部:
{
'typ': 'JWT',
'alg': 'HS256'
}
对头部进行加密:该加密是可以对称解密的
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9
第二部分:playload
载荷就是存放有效信息的地方。携带的信息,这些有效信息包含三个部分:
标准中注册的声明
iss: jwt签发者
sub: jwt所面向的用户
aud: 接收jwt的一方
exp: jwt的过期时间,这个过期时间必须要大于签发时间
nbf: 定义在什么时间之前,该jwt都是不可用的.
iat: jwt的签发时间
jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明:公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密。
私有的声明:私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
定义一个playload载荷:
{
"sub": "1234567890",
"name": "John Doe",
"admin": true
}
进行base64加密,得到Jwt的第二部分:
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9
第三部分:signature(签证)
签证信息由三部分组成:
第三部分组成:需要base64加密后的header和base64加密后的payload使用.
连接组成的字符串,然后通过header中声明的加密方式进行加盐secret
组合加密,然后就构成了jwt的第三部分
js加密示例:
// 将header与payload各自使用base64加密并用.来连接组合
var encodedString = base64UrlEncode(header) + '.' + base64UrlEncode(payload);
// 接着将组合好之后的字符串进行加盐加密,使用HMACSHA256来进行加密
var signature = HMACSHA256(encodedString, 'secret'); // TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
最终我们将header、playload以及signature各自的加密字符串使用.连接即可组成最终的JWT
。
注意:secret是保存在服务器端的,jwt的签发生成也是在服务器端的,secret就是用来进行jwt的签发和jwt的验证,所以,它就是你服务端的私钥,在任何场景都不应该流露出去。一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
客户端在接收到token之后再次发送请求时,一般是在请求头里加入Authorization
,并加上Bearer
标注:
fetch('api/user/1', {
headers: {
'Authorization': 'Bearer ' + token
}
})
服务端会验证token,如果验证通过就会返回相应的资源。
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.10</version>
</dependency>
思路:头部、playload部分进行Base64编码,对于密钥部分是将playload与盐结合并使用MD5加密
import cn.hutool.crypto.digest.MD5;
import com.alibaba.fastjson.JSONObject;
import java.util.Base64;
/**
* @ClassName JWTUtils
* @Author ChangLu
* @Date 2021/9/20 13:58
* @Description TODO
*/
public class JWTUtils {
private static String secret = "jidjiof";
public static void main(String[] args) {
JSONObject header = new JSONObject();
header.put("alg", "HS256");
header.put("typ", "jwt");
JSONObject playLoad = new JSONObject();
playLoad.put("id", "123456");
//头部与主体部分进行base64编码
String base64Header = Base64.getEncoder().encodeToString(header.toJSONString().getBytes());
String base64PlayLoad = Base64.getEncoder().encodeToString(playLoad.toJSONString().getBytes());
//组合:其中第三部分是拿playLoad的json字符串与盐合并进行MD5加密
String token = base64Header + "." + base64PlayLoad + "." + MD5.create().digestHex(playLoad.toJSONString() + secret);
System.out.println("token:" + token);
//解密
String base64PlayLoadStr = new String(Base64.getDecoder().decode(token.split("\\.")[1]));//拿到playload编码前字符串
String jwtSecret = token.split("\\.")[2];//拿取到MD5加密后密钥
//判断是否有没有中途修改
System.out.println(MD5.create().digestHex(base64PlayLoadStr + secret).equals(jwtSecret));
}
}
对于我们自己实现的话还是有许多的不足之处,例如设置有效时间、相同的内容会产生相同的token等等。
引入依赖:
<!-- 引入jwt依赖 -->
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.4.0</version>
</dependency>
application.yml
:主要配置一些token的header类型,token密钥定义
#token配置
token:
header: token # header类型
secret: 789 #token的秘钥
expireTime: 1 #token的有效时间,以天为单位,默认为1天
JwtUtil
:工具类,对第三方jwt工具类进行封装,主要用于创建、注册JWT以及获取到JWT中的键值对
package com.changlu.springbootdemo.utils;
import com.auth0.jwt.JWT;
import com.auth0.jwt.JWTCreator;
import com.auth0.jwt.JWTVerifier;
import com.auth0.jwt.algorithms.Algorithm;
import com.auth0.jwt.interfaces.DecodedJWT;
import com.changlu.springbootdemo.pojo.User;
import com.changlu.springbootdemo.pojo.response.LoginUser;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import javax.servlet.http.HttpServletRequest;
import java.util.Calendar;
import java.util.Collections;
import java.util.Date;
import java.util.Map;
/**
* JWT工具类
*
* @author changlu
* @date 2021/08/09 16:27
**/
@Component
@Data
@Slf4j
public class JwtUtil {
// header头部声明类型
@Value("${token.header}")
private String header;
// signature中的秘钥
@Value("${token.secret}")
private String secret;
// 过期时间
@Value("${token.expireTime}")
private static Integer expireTime;
/**
* 默认过期时间为1天
*/
private static final Integer DEFAULT_EXPIRETIME = 1;
/**
* 生成JWT token
*
* @param playLoadMap 封装包含用户信息的map
* @return
*/
public String createToken(Map<String, String> playLoadMap) {
// playload主体信息为空则不生成token
if (CollectionUtils.isEmpty(playLoadMap)) {
return null;
}
// 过期时间:若是配置文件不配置就使用默认过期时间(1天)
Calendar ca = Calendar.getInstance();
if (expireTime == null || expireTime <= 0) {
expireTime = DEFAULT_EXPIRETIME;
}
ca.add(Calendar.DATE, expireTime);
// 创建JWT的token对象
JWTCreator.Builder builder = JWT.create();
playLoadMap.forEach((k, v) -> {
builder.withClaim(k, v);
});
// 设置发布事件
builder.withIssuedAt(new Date());
// 过期时间
builder.withExpiresAt(ca.getTime());
// 签名加密
String token = builder.sign(Algorithm.HMAC256(secret));
return token;
}
/**
* 从token中获取到指定指定keyName的value值
* @param keyName 指定的keyname
* @param token token字符串
* @return 对应keyName的value值
*/
public String getTokenClaimByName(String keyName,String token){
DecodedJWT decode = JWT.decode(token);
return decode.getClaim(keyName).asString();
}
/**
* 验证JwtToken 不抛出异常说明验证通过
* @param token JwtToken数据
*/
public void verifyToken(String token)throws Exception{
JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256(secret)).build();
jwtVerifier.verify(token);
}
/**
* 生成响应对象返回给前端
* @param user pojo对象
* @param token token
* @param request 请求对象
* @return
*/
public LoginUser buildLoginUser(User user, String token, HttpServletRequest request){
// 过期的毫秒数
Long expireTimeMillis = expireTime * 24 * 60 * 60 *1000L;
// LoginUser作为登陆用户信息实体类(用于返回给前台的相关信息)
LoginUser loginUser = new LoginUser();
user.setPassword(null);
loginUser.setToken(token);// 登陆凭证
loginUser.setUser(user);// 用户信息
loginUser.setLoginTime(System.currentTimeMillis());
loginUser.setExpireTime(expireTimeMillis);
loginUser.setIpAddr(request.getRemoteAddr());
return loginUser;
}
}
pojo
User.java
:用于描述数据库中的实体类,ORM映射模型
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
/**
* @ClassName User
* @Author ChangLu
* @Date 2021/7/28 23:15
* @Description TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User implements Serializable {
private Integer id;
private String username;
private String password;
}
vo
UserRequest
以及LoginUser
:前者是用于接收请求体参数的,后者则是用于返回给前端的响应实体类(描述用户的登录状态)。
UserRequest.java
:
import com.changlu.springbootdemo.pojo.User;
import java.io.Serializable;
/**
* @ClassName UserReuqest
* @Author ChangLu
* @Date 2021/8/15 18:29
* @Description TODO
*/
//直接继承User实体类得到其属性,之后也可以继续进行扩展
public class UserRequest extends User implements Serializable {
private static final long serialVersionUID = -7849794470884667710L;
}
LoginUser.java
:
import com.changlu.springbootdemo.pojo.User;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @ClassName LoginUser
* @Author ChangLu
* @Date 2021/8/15 17:53
* @Description TODO
*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class LoginUser {
// 用户唯一标识
private String token;
// 登陆时间
private Long loginTime;
// 过期时间
private Long expireTime;
// 登陆IP地址
private String ipAddr;
// 登录地点
private String loginLocation;
// 登陆的用户
private User user;
}
JwtInterceptor.java
首先需要实现一个拦截器,其主要目的是对所有的请求进行拦截校验,当一个用户登录好之后得到token,之后的请求都会携带这个token,这个token就是其自身的凭证,在拦截器中对token进行校验:
import com.auth0.jwt.exceptions.AlgorithmMismatchException;
import com.auth0.jwt.exceptions.SignatureGenerationException;
import com.auth0.jwt.exceptions.TokenExpiredException;
import com.changlu.springbootdemo.enums.CommonExceptionEnum;
import com.changlu.springbootdemo.exception.OwnException;
import com.changlu.springbootdemo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
/**
* @ClassName JwtInterceptor
* @Author ChangLu
* @Date 2021/8/15 18:05
* @Description TODO
*/
@Component
@Slf4j
public class JwtInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 获取token值
String token = request.getHeader("Authorization");
try {
//验证token是否有误,若是验证失败会相应抛出指定的异常
jwtUtil.verifyToken(token);
return true;
}catch (SignatureGenerationException signatureGenerationException){
throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_GENERATED);
}catch (TokenExpiredException tokenExpiredException){
throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_EXPIRED);
}catch (AlgorithmMismatchException algorithmMismatchException){
throw new OwnException(CommonExceptionEnum.TOKEN_VERITY_ALGORITHM_NOT_MATCH);
}
}
}
//token验证异常枚举实体类
TOKEN_VERITY_GENERATED(3001,"token使用算法签名时无法生成令牌的签名"),
TOKEN_VERITY_EXPIRED(3002,"token校验已过期"),
TOKEN_VERITY_ALGORITHM_NOT_MATCH(3003,"token校验加密方法无效");
拦截器注册
当我们定义好拦截器以后,就需要将其进行注册到webmvc中,只要注册了之后才会使用该拦截器进行拦截校验。
/**
* @ClassName WebSecurityConfig
* @Author ChangLu
* @Date 2021/8/15 18:24
* @Description TODO
*/
@Configuration
public class WebSecurityConfig implements WebMvcConfigurer {
@Autowired
private JwtInterceptor jwtInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
//拦截所有请求,除了登陆请求(登陆请求我们不需要进行拦截,其是产生token的源头)
registry.addInterceptor(jwtInterceptor).addPathPatterns("/**").excludePathPatterns("/user/login");
}
}
我们来模拟三次请求来测试该token工具类是否有效:
import com.changlu.springbootdemo.common.ResultBody;
import com.changlu.springbootdemo.enums.CommonExceptionEnum;
import com.changlu.springbootdemo.exception.OwnException;
import com.changlu.springbootdemo.pojo.User;
import com.changlu.springbootdemo.pojo.response.LoginUser;
import com.changlu.springbootdemo.pojo.request.UserRequest;
import com.changlu.springbootdemo.utils.JwtUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
/**
* @ClassName UserController
* @Author ChangLu
* @Date 2021/7/28 23:14
* @Description TODO
*/
@RestController
@Slf4j
@RequestMapping("/user")
public class UserController {
@Autowired
private JwtUtil jwtUtil;
/**
* 登陆
*
* @param request
* @param userRequest
* @return
*/
@PostMapping("/login")
public ResultBody userLogin(HttpServletRequest request, @RequestBody UserRequest userRequest) {
User user = new User(1111, "changlu", "123456");
if (!("changlu".equals(userRequest.getUsername()) && "123456".equals(userRequest.getPassword()))) {
// 登陆不成功
throw new OwnException(CommonExceptionEnum.LOGIN_ERROR);
} else {
// 登陆成功
Map<String, String> playLoadMap = new HashMap<>(1);
playLoadMap.put("id", user.getId().toString());
String token = jwtUtil.createToken(playLoadMap);
LoginUser loginUser = jwtUtil.buildLoginUser(user, token, request);
return ResultBody.success(loginUser);
}
}
/**
* 删除指定id用户
*
* @param id 指定用户的id
* @return
*/
@DeleteMapping("/{id}")
public ResultBody queryList(@PathVariable("id") Integer id) {
return ResultBody.success("删除id为" + id + "的用户成功!");
}
/**
* 查询token中的id
* @param request
* @return
*/
@GetMapping("/")
public ResultBody queryLoginId(HttpServletRequest request) {
String token = request.getHeader("Authorization");
String id = jwtUtil.getTokenClaimByName("id", token);
return ResultBody.success("取到token中存储的id值为:" + id);
}
}
说明:对于之后的两次请求都要携带指定的header键值对,也就是对应的token,可从第一次请求返回值中获取!
浏览器cookie清除(但是服务器还是存在)
建议将时间设置稍微短一点
使用jwt就不需要使用到redis缓存,对于的值需要进行缓存。
JWT最好设置时间为1天或者30分钟。
[1]. 什么是 JWT – JSON WEB TOKEN:JWT的基础知识概念理解,强推。本笔记大部分内容摘自该文章
[2]. json web token(JWT)基本原理及Springboot集成:程序参考(主)
[3]. JWT的Java使用 (JJWT):(辅)
安全:
我是长路,感谢你的耐心阅读。如有问题请指出,我会积极采纳!
欢迎关注我的公众号【长路Java】,分享Java学习文章及相关资料
Q群:851968786 我们可以一起探讨学习
注明:转载可,需要附带上文章链接
赞
踩
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。