什么是JWT? JWT是Json Web Token的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
JWT的工作流程 下面是一个JWT的工作流程图。模拟一下实际的流程是这样的(假设受保护的API在/protected中)
用户导航到登录页,输入用户名、密码,进行登录
服务器验证登录鉴权,如果改用户合法,根据用户的信息和服务器的规则生成JWT Token
服务器将该token以json形式返回(不一定要json形式,这里说的是一种常见的做法)
用户得到token,存在localStorage、cookie或其它数据存储形式中。
以后用户请求/protected中的API时,在请求的header中加入Authorization: Bearer xxxx(token)。此处注意token之前有一个7字符长度的 Bearer
服务器端对此token进行检验,如果合法就解析其中内容,根据其拥有的权限和自己的业务逻辑给出对应的响应结果。
用户取得结果
JWT的主要应用场景 身份认证在这种场景下,一旦用户完成了登陆,在接下来的每个请求中包含JWT,可以用来验证用户身份以及对路由,服务和资源的访问权限进行验证。由于它的开销非常小,可以轻松的在不同域名的系统中传递,所有目前在单点登录(SSO)中比较广泛的使用了该技术。 信息交换在通信的双方之间使用JWT对数据进行编码是一种非常安全的方式,由于它的信息是经过签名的,可以确保发送者发送的信息是没有经过伪造的。
优点 1.简洁(Compact): 可以通过URL,POST参数或者在HTTP header发送,因为数据量小,传输速度也很快 2.自包含(Self-contained):负载中包含了所有用户所需要的信息,避免了多次查询数据库 3.因为Token是以JSON加密的形式保存在客户端的,所以JWT是跨语言的,原则上任何web形式都支持。 4.不需要在服务端保存会话信息,特别适用于分布式微服务。
JWT的结构 JWT是由三段信息构成的,将这三段信息文本用.链接一起就构成了JWT字符串。 就像这样:
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ
JWT包含了三部分: Header 头部(标题包含了令牌的元数据,并且包含签名和/或加密算法的类型) Payload 负载 (类似于飞机上承载的物品) Signature 签名/签证
Header JWT的头部承载两部分信息:token类型和采用的加密算法。
1 2 3 4 { "alg": "HS256", "typ": "JWT" }
声明类型:这里是jwt 声明加密的算法:通常直接使用 HMAC SHA256
加密算法是单向函数散列算法,常见的有MD5 、SHA 、HAMC 。 MD5(message-digest algorithm 5) (信息-摘要算法)缩写,广泛用于加密和解密技术,常用于文件校验。校验?不管文件多大,经过MD5后都能生成唯一的MD5值 SHA (Secure Hash Algorithm,安全散列算法),数字签名等密码学应用中重要的工具,安全性高于MD5 HMAC (Hash Message Authentication Code),散列消息鉴别码,基于密钥的Hash算法的认证协议。用公开函数和密钥产生一个固定长度的值作为认证标识,用这个标识鉴别消息的完整性。常用于接口签名验证
Payload 载荷就是存放有效信息的地方。 有效信息包含三个部分 1.标准中注册的声明 2.公共的声明 3.私有的声明
标准中注册的声明 (建议但不强制使用) : iss: jwt签发者 sub: 面向的用户(jwt所面向的用户) aud: 接收jwt的一方 exp: 过期时间戳(jwt的过期时间,这个过期时间必须要大于签发时间) nbf: 定义在什么时间之前,该jwt都是不可用的. iat: jwt的签发时间 jti: jwt的唯一身份标识,主要用来作为一次性token,从而回避重放攻击。
公共的声明 : 公共的声明可以添加任何的信息,一般添加用户的相关信息或其他业务需要的必要信息.但不建议添加敏感信息,因为该部分在客户端可解密.
私有的声明 : 私有声明是提供者和消费者所共同定义的声明,一般不建议存放敏感信息,因为base64是对称解密的,意味着该部分信息可以归类为明文信息。
Signature jwt的第三部分是一个签证信息,这个签证信息由三部分组成: header (base64后的) payload (base64后的) secret 这个部分需要base64加密后的header和base64加密后的payload使用.连接组成的字符串,然后通过header中声明的加密方式进行加盐secret组合加密,然后就构成了jwt的第三部分。 密钥secret是保存在服务端的,服务端会根据这个密钥进行生成token和进行验证,所以需要保护好。
SpringBoot+Spring Security和JWT的集成实现token验证 引入JWT依赖 1 2 3 4 5 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.0</version> </dependency>
JWT的生成和解析工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 import io.jsonwebtoken.*;import java.util.Calendar;import java.util.Date;import java.util.HashMap;import java.util.Map;public class JwtUtil { private static final int EXPIRATION_TIME = 60 *60 *24 ; private static final String SECRET = "023bdc63c3c5a4587*9ee6581508b9d03ad39a74fc0c9a9cce604743367c9646b" ; public static final String TOKEN_PREFIX = "Bearer " ; public static final String AUTHORIZATION = "Authorization" ; public static String generateToken (String userName) { Calendar calendar = Calendar.getInstance(); Date now = calendar.getTime(); calendar.setTime(new Date ()); calendar.add(Calendar.SECOND, EXPIRATION_TIME); Date time = calendar.getTime(); HashMap<String, Object> map = new HashMap <>(); map.put("userName" , userName); String jwt = Jwts.builder() .setClaims(map) .setIssuedAt(now) .setExpiration(time) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); return TOKEN_PREFIX + jwt; } public static String validateToken (String token) { try { Map<String, Object> body = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token.replace(TOKEN_PREFIX, "" )) .getBody(); String userName = body.get("userName" ).toString(); return userName; }catch (ExpiredJwtException e) { throw e; } catch (UnsupportedJwtException e) { throw e; } catch (MalformedJwtException e) { throw e; } catch (SignatureException e) { throw e; } catch (IllegalArgumentException e) { throw e; } catch (Exception e){ throw e; } } }
Spring Security配置 Spring Security是一个基于Spring的通用安全框架,里面内容太多了,本文的主要目的也不是展开讲这个框架,而是如何利用Spring Security和JWT一起来完成API保护。所以关于Spring Secruity的基础内容或展开内容,请自行去官网学习(官网 )。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(securedEnabled = true) public class SpringSecurityConfig extends WebSecurityConfigurerAdapter { public static final String[] AUTH_WHITELIST = { "/user/login" }; @Override protected void configure (HttpSecurity http) throws Exception { http.cors().and() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and() .authorizeRequests() .antMatchers(AUTH_WHITELIST).permitAll() .anyRequest().authenticated() .and() .addFilter(new JWTAuthenticationFilter (authenticationManager())); }
这是标准的SpringSecurity配置内容,就不在详细说明。注意其中的
.addFilter(new JwtAuthenticationFilter(authenticationManager()))
这行,将我们定义的JWT方法加入SpringSecurity的处理流程中。
重点就是配置请求拦截,由于我做的是一个接口服务,所以暂时除了登录验证,其他接口链接全部都要拦截,通过JWTAuthenticationFilter 过滤器来实现token的验证
登录生成token 验证用户名密码正确后,生成一个token,并将token返回给客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 @ResponseBody @RequestMapping(value = "/login", method = RequestMethod.POST) public String toLogin (SysUser user) { JSONObject json=new JSONObject (); BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder (); try { SysUser user1 = userControllerClient.getUserInfoByLoginName(user.getUsername()); if (user1!=null ) { String dbPassWord = user1.getPassword(); if (bCryptPasswordEncoder.matches(user.getPassword(),dbPassWord)) { String token = JwtUtil.generateToken(user.getUsername()); json.put("success" , true ); json.put("code" , 1 ); json.put("time" , DateUtil.dateToString(new Date ())); json.put("message" , "登陆成功" ); json.put(JwtUtil.AUTHORIZATION,token); } else { json.put("success" , false ); json.put("code" , -1 ); json.put("message" , "登陆失败,密码错误" ); } }else { json.put("success" , false ); json.put("code" , 0 ); json.put("message" , "无此用户信息" ); } } catch (Exception e) { json.put("code" , -2 ); json.put("success" , false ); json.put("message" , e.getMessage()); } return JSON.toJSONString(json); }
授权验证
用户一旦登录成功后,会拿到token,后续的请求都会带着这个token,服务端会验证token的合法性。
创建JWTAuthenticationFilter
类,我们在这个类中实现token的校验功能。
token的校验 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 public class JWTAuthenticationFilter extends BasicAuthenticationFilter { private final Logger logger = LoggerFactory.getLogger(this .getClass()); public JWTAuthenticationFilter (AuthenticationManager authenticationManager) { super (authenticationManager); } @Override protected void doFilterInternal (HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String url = request.getRequestURI(); String header = request.getHeader(JwtUtil.AUTHORIZATION); JSONObject json=new JSONObject (); if (null != SpringSecurityConfig.AUTH_WHITELIST&&Arrays.asList(SpringSecurityConfig.AUTH_WHITELIST).contains(url)){ chain.doFilter(request, response); return ; } if (StringUtils.isBlank(header) || !header.startsWith(JwtUtil.TOKEN_PREFIX)) { json.put("codeCheck" , false ); json.put("msg" , "Token为空" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); return ; } try { UsernamePasswordAuthenticationToken authentication = getAuthentication(request,response); SecurityContextHolder.getContext().setAuthentication(authentication); chain.doFilter(request, response); }catch (ExpiredJwtException e) { json.put("codeCheck" , false ); json.put("msg" , "Token已过期" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("Token已过期: {} " + e); } catch (UnsupportedJwtException e) { json.put("codeCheck" , false ); json.put("msg" , "Token格式错误" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("Token格式错误: {} " + e); } catch (MalformedJwtException e) { json.put("codeCheck" , false ); json.put("msg" , "Token没有被正确构造" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("Token没有被正确构造: {} " + e); } catch (SignatureException e) { json.put("codeCheck" , false ); json.put("msg" , "Token签名失败" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("签名失败: {} " + e); } catch (IllegalArgumentException e) { json.put("codeCheck" , false ); json.put("msg" , "Token非法参数异常" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("非法参数异常: {} " + e); }catch (Exception e){ json.put("codeCheck" , false ); json.put("msg" , "Invalid Token" ); response.setCharacterEncoding("UTF-8" ); response.getWriter().write(JSON.toJSONString(json)); logger.error("Invalid Token " + e.getMessage()); } } private UsernamePasswordAuthenticationToken getAuthentication (HttpServletRequest request,HttpServletResponse response) { String token = request.getHeader(JwtUtil.AUTHORIZATION); if (token != null ) { String userName="" ; try { userName = JwtUtil.validateToken(token); if (StringUtils.isNotBlank(userName)) { return new UsernamePasswordAuthenticationToken (userName, null , new ArrayList <>()); } }catch (ExpiredJwtException e) { throw e; } catch (UnsupportedJwtException e) { throw e; } catch (MalformedJwtException e) { throw e; } catch (SignatureException e) { throw e; } catch (IllegalArgumentException e) { throw e; }catch (Exception e){ throw e; } return null ; } return null ; } }
该类继承自BasicAuthenticationFilter,在doFilterInternal方法中,从http头的Authorization 项读取token数据,然后用Jwts包提供的方法校验token的合法性。如果校验通过,就认为这是一个取得授权的合法请求。 这其中也包括了,token验证异常处理的返回信息