JWT是 Json Web Token 的缩写。它是基于 RFC 7519 标准定义的一种可以安全传输的 小巧 和 自包含 的JSON对象。由于数据是使用数字签名的,所以是可信任的和安全的。JWT可以使用HMAC算法对secret进行加密或者使用RSA的公钥私钥对来进行签名。
我理解的,就是用户登录成功后,服务端会给客户端发放凭证token,并且凭证token不再由服务端保存,而是由客户端自己保存。即用户登陆后,将加密登陆凭证交于客户端,客户端并不明白凭证有何意义,只知道登陆需要使用。在访问时服务端获取到登陆凭证token进行解密,获取到当前用户信息。同时用户凭证有一定的实效,当超过一定时效后,将会失效
JWT包含了使用 . 分隔的三部分:
1.Header 头部,包含了两部分:token类型和采用的加密算法。
2.Payload 负载,Token的第二部分是负载,它包含了claim, Claim是一些实体(通常指的用户)的状态和额外的元数据。
3.Signature 签名,创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。
JWT 生成的token如下
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHBpcmVzSW4iOjE2Mjc3MzY1MzkzNTQsImF1dGhvcml0aWVzIjpbeyJhdXRob3JpdHkiOiJST0xFX3VzZXIifV0sImVuYWJsZWQiOnRydWUsInVzZXJuYW1lIjoibWlrZSJ9.DCoCxd8lIRtzntLdjLPHB-utL8xZttnFQkTFny8MKZI
虽然JWT包含了几个部分,但是我们也不需要关心太多,毕竟,现在的JWT比较成熟,工具类很友好,只需处理我们关系的部分就可以了,也就是claim,而工具类已经封装好了,我们直接取出来解析好就可以直接用
1.用户在浏览器访问资源
2.服务端取出token进行校验
3.如果token有效,取出用户信息(就是将用户信息保存到SecurityContextHolder.getContext()上下文中)
4.如果token失效或者没有携带token,则走登录逻辑
5.登录成功后,服务端创建登录凭证token,返回给客户端(服务端不保存token)
6.客户端浏览其他资源,携带token,继续走1,2逻辑
OK,大致了解了JWT,我们用demo来介绍
只需要在项目中添加依赖即可:
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> <scope>compile</scope> </dependency>
其实,很多工作JwtHelper类已经帮我们封装好了,我们可以直接拿来用
Map<String, Object> claums=new HashMap<>(); claums.put("username","mike"); claums.put("mobile","15994643438"); claums.put("expires_in",14400000); MacSigner rsaSigner=new MacSigner(secret); Jwt encode = JwtHelper.encode(JSON.toJSONString(claums), rsaSigner String token = encode.getEncoded(); System.out.println(token);
这里,secret 是加密串,可以自由定义
Jwt decode = JwtHelper.decode(token); System.out.println(decode.getClaims());
这样,解析出来的就是一个Jwt对象,可以直接获取到Claims,Claims就是我们登录成功后存储在token中的信息
运行结果:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJtb2JpbGUiOiIxNTk5NDY0MzQzOCIsImV4cGlyZXNfaW4iOjE0NDAwMDAwLCJ1c2VybmFtZSI6Im1pa2UifQ.DOgTvXN1FdJ12OWmezzCJ9vkjU0Flh6CsCenpqb7mXE {"mobile":"15994643438","expires_in":14400000,"username":"mike"}
是不是很简单?
我们结合spring security 来搞个demo,按需求来做吧,简单的一个需求:
某企业要做前后端分离的项目,决定要用spring boot + spring security+JWT 框架实现登录认证授权功能,用户登录成功后,服务端利用JWT生成token,之后客户端每次访问接口,都需要在请求头上添加Authorization:Bearer token 的方式传值到服务器端,服务器端再从token中解析和校验token的合法性,如果合法,则取出用户数据,保存用户信息,不需要在校验登录,否则就需要重新登录
好了,基于上述需求,我们来新建项目
我们基于上一篇文章《循序渐进学spring security 第七篇,如何基于用户表和权限表配置权限?越学越简单了》的项目copy一个新的项目,改名为:security-mybatis-jwt
<dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-jwt</artifactId> <version>1.0.9.RELEASE</version> <scope>compile</scope> </dependency>
也可以直接用JwtHelper, 我这里为了方便管理,还是稍微封装了一下
public class JWTUtils { /** * 创建JWT * @param secret * @param claims 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的) * @return */ public static String getAccessToken(String secret, Map<String, Object> claims){ // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。 MacSigner rsaSigner=new MacSigner(secret); Jwt jwt = JwtHelper.encode(JSON.toJSONString(claims), rsaSigner); return jwt.getEncoded(); } public static Map<String,Object> parseToken(String token){ Jwt jwt = JwtHelper.decode(token); return JSON.parseObject(jwt.getClaims()); } /** * 根据传入的token过期时间判断token是否已过期 * @param expiresIn * @return true-已过期,false-没有过期 */ public static boolean isExpiresIn(long expiresIn){ long now=System.currentTimeMillis(); return now>expiresIn; } }
这里,封装了三个方法
登录成功后,会走UsernamePasswordAuthenticationSuccessHandler 的onAuthenticationSuccess 方法,如果不熟悉的可以翻看我之前的文章《循序渐进学习spring security 第五篇,如何处理重定向和服务器跳转?登录如何返回JSON串》,我们在这方法里面,生成token,并以JSON串形式返回给前端
@Component public class UsernamePasswordAuthenticationSuccessHandler implements AuthenticationSuccessHandler { @Value("${harry.jwt.secret: huangxuanheng@163.com}") private String secret; @Value("${harry.jwt.expMillis: 7200000}") private long expMillis; @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { Object principal = authentication.getPrincipal(); response.setContentType("application/json;charset=utf-8"); PrintWriter out = response.getWriter(); User user= (User) principal; //生成token Map<String, Object> claims=new HashMap<>(); claims.put("username",user.getUsername()); claims.put("authorities",user.getAuthorities()); claims.put("enabled",user.isEnabled()); claims.put("expiresIn",(System.currentTimeMillis()+expMillis)); String token = JWTUtils.getAccessToken(secret, claims); Map<String,Object>result=new HashMap<>(); result.put("accessToken",token); out.write(JSON.toJSONString(result)); out.flush(); out.close(); } }
harry: jwt: secret: e9948PG02lURjvhjotDGQ6ksRdz3920MEfdy0q6HIszaxNNXw5D1yGq7l3zVWVfUbPBSA56JMqawy7Mt2vPDx5AveuOHHpT0uZB #随机生成,可在百度上自行搜索,或者自己取随机字符串 # expMillis: 14400000 #4个小时候过期,可根据实际情况自行修改 expMillis: 20000 #4个小时候过期,可根据实际情况自行修改
这一步,我们需要写一个过滤器:JwtAuthenticationTokenFilter,继承OncePerRequestFilter,在这个过滤器里面实现doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) 方法,取出客户端在请求头上携带的token数据,进行解析校验
public class JwtAuthenticationTokenFilter extends OncePerRequestFilter { private String tokenHead = "Bearer "; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String tokenValue = request.getHeader(HttpHeaders.AUTHORIZATION); if(!StringUtils.hasText(tokenValue)){ filterChain.doFilter(request,response); return; } String token = tokenValue.substring(tokenHead.length()); Map<String, Object> parseJWT = JWTUtils.parseToken(token); if(JWTUtils.isExpiresIn((long)parseJWT.get("expiresIn"))){ //token 已经过期 SecurityContextHolder.getContext().setAuthentication(null); filterChain.doFilter(request,response); return; } String username = (String) parseJWT.get("username"); if(StringUtils.hasText(username)&& SecurityContextHolder.getContext().getAuthentication() == null){ //正常用户 UserDetails userDetails = userDetailsService.loadUserByUsername(username); if(userDetails!=null&&userDetails.isEnabled()){ UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities()); authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); //设置用户登录状态 log.info("authenticated user {}, setting security context",username); SecurityContextHolder.getContext().setAuthentication(authentication); } } filterChain.doFilter(request,response); } }
我们这里说下实现的方法
public class UsernamePasswordAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=utf-8"); response.setHeader("WWW-Authenticate", "Bearer"); response.addHeader("Access-Control-Allow-Origin", "*"); response.setStatus(HttpStatus.UNAUTHORIZED.value()); PrintWriter out = response.getWriter(); Map<String,Object> data = new HashMap<>(); data.put("path", request.getRequestURI()); data.put("time", LocalDateTime.now().toString()); data.put("errCode", HttpStatus.UNAUTHORIZED.value()); data.put("errMsg", HttpStatus.UNAUTHORIZED.getReasonPhrase()); out.write(JSON.toJSONString(data)); out.flush(); out.close(); } }
如果验证失败,统一返回JSON串,并将状态码设置为401,表示未授权
启动项目,登录测试
登录成功后返回token
然后拿token放到请求头,访问其他接口
20秒后,token会过期,过期后访问是这样
和我们预期的一样,这样,我们就完成了JWT生成用户凭证token的介绍了
源码下载 项目security-mybatis-jwt