JWT
1:基本概念
1.1:传统身份验证
http是一种没有状态的协议,也就是它并不知道是谁访问应用,这里我们把用户看成是客户端,客户端使用用户名还有密码通过了身份验证,不过下回这个客户端在发送请求时候,还等在验证一下。
解决的方法就是,当用户登录的时候,如果没有问题,我们在服务端生成一条记录,这个记录里可以说明一下登录的用户是谁,让后把这条记录的id号发送非客户端,客户端收到以后把这个id号存储在cookie里,下次这个用户在像服务端发送请求的时候,可以带着这个cookie,这样服务端会验证一个这个cookie里的消息,看看能不能在服务端这里找到对应的记录,如果可以,说明用户已经通过了身份验证,就把用户请求的数据返回给客户端。
上面说的服务器端存储就是session,我们需要在服务端存储为登录的用户生成的session,这session肯会存储在内存,磁盘,或者数据库里。我们需要在服务端定时的去清理过期的session。
这种认证中出现的问题是:
session:每次认用户发起请求时,服务器需要在创建一个记录去存储信息,当越来越多的用户发请求时,内存的开销也会不断增加。
CORS(跨域资源共享):当我们需要让数据跨多台系统设备上使用时,跨域资源的共享会使一个让人头疼的问题,在使用ajax抓取另一个域的资源,就可以会进制请求的情况。
CSRF(跨站请求伪造):用户在访问银行网站时,他们很容易受到跨站请求的攻击,并且能够在被利用其访问其他的网站。
在这些问题中,可拓展性是最突出的,因此我们需要出追求一种更行之有效的方法。
1.2:Token 身份认证
使用基于Token的身份验证方法,在服务端不需要存储用户的登录记录。大概的流程是这样的:
1:客户端使用用户,密码请求登录
2:服务端收到请求,去验证用户名,密码
3:验证成功后,服务端会签发一个Token,再把这个Token 发送给客户端
4:客户端每次向服务端请求资源的需要带着服务端签发的Token
5:服务端收到请求,让后去验证客户端请求里面带着的Token,如果验证成功,就向客户端返回请求的数据。
使用Token 验证的优势:
无状态,可拓展。
在客户端存储的token是无状态的,并且能够被拓展,基于这种无状态和不存储Session信息,负载均衡器能够将用户信息从一个服务传到其他服务器上。
安全性:
请求中发送token而不是发送cookie更高放置CSRF(跨站请求伪造),及时在客户端使用cookie存储token,
cookie也仅仅是一个存储机制,而不是用户认证,不将信息存储在session中,让我们少了对session的操作。
1.3 JSON web token (jwt)机制
JWT是一种紧凑且字包含的。用于在多方传递json对象的技术,传递的数据可以使用数字签名增加安全性,可以适应HMAC加密算法或者RSA 公钥/私钥加密方式。
紧凑:数据小,可以通过URL,post参数,请求头发送,且数据小代表传输速度快。
自包含:使用payload 数据块记录用户必要且不隐私的数据。可有效的减少数据库访问次数,提高代码性能。
JWT 一般用于处理用户身份验证或者数据信息交换。
1:用户身份验证:一旦用户登录,每个后续请求都将包含JWT,允许用户访问该令牌的路由,服务和资源。单点登录是当今广泛使用的JWT的一项功能,因为他的开销很小,并且能够轻松的跨不通过域使用。
2:数据信息交换:JWT是一种非常方便的使用多方传递数据的载体,因为其可以使用数据加密来保证数据的有效性和安全性。
2:JWT的数据结构
JWT的数据结构是: A.B.C 由字符点 “。” 来分割三部分数据。
A-header 头信息
B-payload (有效荷载)
C-signature 签名
2.1: header
数据结构:{“alg”:“加密算法名称”,”typ“:“JWT”}
alg是加密算法定义内容,如:HMAC SHA256 或RSA
typ是token类型,这里固定为JWT
2.2 payload
在pyload数据块一般使用记录实体(通常为用户信息)或者其他数据。主要分为三个部分,分别是:已注册的信息,公开数据,私用数据。
payload 中常用信息有:iss(发行者),exp(到期时间),sub(主题),aud(受众)等。
前面列举的都是已注册信息。
注意:即使JWT有签名加密机制,但是patload内容都是明文记录,除非记录的是加密数据,否则不排除泄露隐私数据的可能,不推荐在payload中记录任何铭感数据。
2.3 signatrue
签名信息,这是一个由开发者提供的消息。是服务端验证的传递的数据是否有效安全的标准。在生成JWT最终数据的之前。先试用header中定义的加密算法,将header和payload进行加密,并使用点进行连接。如:加密后的head加密后的payload。在使用相同的加密算法。对家后的数据和签名信息进行加密,得到最终结果。
2.4:JWT执行流程
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-52fTBHeP-1576024217901)(img/snipaste_20191114_092625.png)]
1:用户携带用户名称和密码进行的登录操作
2:服务端验证用户名称和密码没问题了之后创建JWT
3:服务器端将JWT传递到客户端。
4:客户端将JWT方法请求头中,并且每次请求都将JWT带上
5:服务器端验证JWT的有效性。
6:如果JWT有效就响应结果。
3:基于JWT实现单点登录
3.1:注意事项
1:时效性:
在使用JWT实现单点登录时,需要注意token时效性,token是保存在客户端的令牌数据。
如果永久有效,则有被挟持的可能,token在设计的时候,可以考虑一次性有效,或者一段时间内有效。
如果设置的有效时长,则需要考虑是否需要刷新token有效期问题。
2:token保存位置
在使用JWT技术生产的token,客户端保存的时候可以考虑cookie或者localStroage。cookie保存方式,可以实现跨域传递数据,localStorage是域私有的本地存储,无法实现跨域。
3.2:编写代码
1:思路
1:服务器端自定义注解和拦截器,如果Conntroller中方法中添加了自定义注解,就表示这个需要token验证,那么在拦截器中拦截所有请求,判断请求映射的方法是否带有自定义注解,如果带就拦截验证JWT,如果不存在就放行。
2:客户端携带用户名和密码进行登录操作。
3:服务器端拦截器拦截请求发现是登录请求,就放行。
4:进行用户名和密码的验证,如果通过验证,就根据用户信息创建JWT,并返回到客户端。客户端将jwt放在localStroage里面。
5:下次客户端在发起请求的时候,就会被拦截器拦截,因为不是登录请求,拦截就会拦截判断是否有token,token是否过期,是否含有user信息等,如果验证不通过就抛出异常,让后异常就会被全局异常处理所捕获。
6:如果验证通过,那么就继续给客户端提供服务。
2:添加依赖
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-BRuZ2nLn-1576024217903)(img/JWT依赖.png)]
3:编写工具类
1:需要一个设置密钥的方法
2:需要一个创建token的方法
3:需要一个验证token的方法
4:需要一个解析token信息的方法
5:需要将用户对象转换为字符串的方法。
public class JwtUtils {
//设定服务器端用于加密的Key
public static final String serverKey = "Mytoken";
private static ObjectMapper objectMapper = new ObjectMapper();
//将加密的KEY进行编码处理
private static SecretKey generalKey(){
try {
byte[] bytes = serverKey.getBytes("UTF-8");
SecretKey key = new SecretKeySpec(bytes,0,bytes.length,"AES");
return key;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
//id表示唯一标识,可以使用用户id,或者uuid
//iss 表示签发者。
//subject 存储的用户信息。
//ttlMillis 表示token的有效期
//生成koten
public static String createJWT(String id,String iss,String subject,long ttlMillis){
//声明使用的加密算法
SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
//获取当前时间
long newMellis = System.currentTimeMillis();
Date newDate = new Date(newMellis);
SecretKey secretKey = generalKey();
// 创建JWT的构建器。 就是使用指定的信息和加密算法,生成Token的工具。
JwtBuilder builder = Jwts.builder()
.setId(id) // 设置身份标志。就是一个客户端的唯一标记。 如:可以使用用户的主键,客户端的IP,服务器生成的随机数据。
.setIssuer(iss)
.setSubject(subject)
.setIssuedAt(newDate) // token生成的时间。
.signWith(signatureAlgorithm, secretKey); // 设定密匙和算法
if (ttlMillis >= 0) {
long expMillis = newMellis + ttlMillis;
Date expDate = new Date(expMillis); // token的失效时间。
builder.setExpiration(expDate);
}
return builder.compact();
}
//解析token的信息
public static Claims parseJWT(String jwt) throws Exception {
SecretKey secretKey = generalKey();
return Jwts.parser()
.setSigningKey(secretKey)
.parseClaimsJws(jwt)
.getBody();// getBody获取的就是token中记录的payload数据。就是payload中保存的所有的claims。
}
//验证token
public static ResponseResult validateJWT(String jwt){
if(jwt == null){
return ResponseResult.error(CodeMsg.SERVEREXCEPTION);
}
Claims claims = null;
try {
claims = parseJWT(jwt);
return ResponseResult.success(claims);
} catch (ExpiredJwtException e) { // token超时
return ResponseResult.error(CodeMsg.JWT_TimeOut);
} catch (SignatureException e) { // 校验失败
return ResponseResult.error(CodeMsg.JWT_verification_failed);
} catch (Exception e) {
return ResponseResult.error(CodeMsg.SERVEREXCEPTION);
}
}
//将用户对象转换为JSON字符串
public static String generalSubject(Object o){
try {
return objectMapper.writeValueAsString(o);
} catch (JsonProcessingException e) {
e.printStackTrace();
return null;
}
}
}
4:自定义注解
package com.maven.jwt01.MyAnnocation;
//注解用来测试是否验证token
@Target({ElementType.METHOD,ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Tokenverify {
public boolean Tokenverify() default true;
}
5:自定义拦截器
public class TokenInterceptor implements HandlerInterceptor {
@Autowired
private UserService userService;
private ObjectMapper objectMapper = new ObjectMapper();
//用来验证token
@Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response,
Object object) throws Exception {
//首先从请求头中获取token
String token = request.getHeader("Authorization");
//如果不是映射到Controller 的方法直接放行
if (!(object instanceof HandlerMethod)){
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) object;
Method method = handlerMethod.getMethod();
//检查方法上是否有token验证注解
if (method.isAnnotationPresent(Tokenverify.class)){
//判断是否需要验证
Tokenverify tokenverify = method.getAnnotation(Tokenverify.class);
if(tokenverify.Tokenverify() == true ){
if(token == null || token.equals("null") || token.trim() == "" ){
throw new RuntimeException(CodeMsg.JWT_ISNULL.getMessage());
}
//判断token的合法性
ResponseResult responseResult = JwtUtils.validateJWT(token);
if(responseResult.getData()!=null){
//获取用户信息
Claims claims = (Claims) responseResult.getData();
String subject = claims.getSubject();
User user = null;
try {
user = objectMapper.readValue(subject, User.class);
} catch (JsonProcessingException e) {
throw new RuntimeException(CodeMsg.JWT_ERRO.getMessage());
}
}
}
}
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
6:全局异常处理
//全局的异常处理
@RestControllerAdvice
public class MyControllerAdvice {
private Logger log = LoggerFactory.getLogger(MyControllerAdvice.class);
@ExceptionHandler(Exception.class)
public ResponseResult handlerException(Exception e){
log.info("erro this {} ",e.getMessage());
e.printStackTrace();
CodeMsg codeMsg = new CodeMsg(500,e.getMessage());
return ResponseResult.error(codeMsg);
}
}
7:Controller
@RestController
@RequestMapping("index")
public class WebController {
private ObjectMapper objectMapper = new ObjectMapper();
private Logger log = LoggerFactory.getLogger(WebController.class);
@Autowired
private UserService userService;
//用户登录的方法
@RequestMapping("login")
public ResponseResult login(String data){
User user =null;
try {
user = objectMapper.readValue(data, User.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
//1:验证登录
User login_user = userService.login(user);
//2:登录成功创建token
if(login_user!=null){
String jwt = JwtUtils.createJWT("" + login_user.getId(), JwtUtils.serverKey,
JwtUtils.generalSubject(login_user), 30 * 60 * 1000);
return ResponseResult.success(jwt);
}else{
return ResponseResult.error(CodeMsg.SERVEREXCEPTION);
}
}
//获取token中存放的用户信息的方法
@Tokenverify
@RequestMapping(value="getUserBytoken")
public ResponseResult getUserBytoken(HttpServletRequest request){
String token = request.getHeader("Authorization");
ResponseResult responseResult = JwtUtils.validateJWT(token);
if(responseResult.getData()!=null){
//获取用户信息
Claims claims = (Claims) responseResult.getData();
String subject = claims.getSubject();
User user =null;
try {
user = objectMapper.readValue(subject, User.class);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return ResponseResult.success(user);
}
return responseResult;
}
}
8:Service
@Component
public class UserService {
//登录验证返回用户对象
public User login(User user){
if (user.getUsername().equals("admin") && user.getPassword().equals("admin")){
user.setId(1);
user.setPassword(null);
return user;
}
return null;
}
}