jwt 基本介绍 jwt 全称是jsonWebToken, 简单的说就是一种能够携带信息的token。 在传统的web环境中,浏览器和后端通过记录在浏览器的cookie 和存储在服务端的session 来实现登录状态,而cookie session的方式在多分布式环境下可能带来session复制,跨域访问,单点登录等问题; 直接使用后端生成token的方式,服务端也需要存储生成的token信息,因为token是无意义的。而使用jwt ,能够携带一些必要得信息比如用户id 和用户名称等; 后端就不需要对生成的token做存储,同时jwt也有时间的有效期。能够做到请求接口无状态;
缺点:
安全性,payload是使用base64编码的,并没有加密,因此jwt中不能存储敏感数据。而session的信息是存在服务端的,相对来说更安全。 无法废弃,只能等待过期失效,或增加其他的黑名单类似的逻辑处理失效。 jwt 官网: https://jwt.io/
格式 在使用过程中是一个base64编码的字符串
1 eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
此字符串通过逗号分割是由3部分组成
第一部分是 header 区域,只要表示当前签名的加密方式; 第二部分是 plaoyload 区域,存储了当前的token携带的信息,包含颁发给谁,有效期等 第三部分是 将前2部分通过加密生成的,主要用于服务端校验token的合法性;
使用 基本依赖 引用对应的依赖,关于jwt的工具类有很多,这里使用 https://github.com/jwtk/jjwt
引入maven依赖
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <!--api-> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <!---实现--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency>
创建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 //设置自定义header 信息 JwtBuilder jwtBuilder = Jwts.builder().setHeaderParam("a", "b"); Calendar instance = Calendar.getInstance(); instance.add(Calendar.MINUTE,30); Date expire = instance.getTime(); //设置playload信息 jwtBuilder = jwtBuilder.setIssuer("me") //谁颁发的 .setSubject("Bob") // token的主体是什么 ,是关于什么的 .setAudience("you") // 给谁的 .setExpiration(expire) //失效时间 .setNotBefore(new Date()) //不能在此时间之前获取 .setIssuedAt(new Date()) //签发时间 .setId(UUID.randomUUID().toString());//id //设置自定义的playload信息 jwtBuilder.claim("key","value"); //构建签名算法,更多签名算法查看 SignatureAlgorithm Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); jwtBuilder = jwtBuilder.signWith(key); //执行压缩 使生成的字符串变小 jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE); System.out.println(jwtBuilder.compact());
解析读取token 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); //设置解析的签名算法 jwtParserBuilder = jwtParserBuilder.setSigningKey(key); Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr); String signature = claimsJws.getSignature(); System.out.println("<========>"); System.out.println(signature); JwsHeader header = claimsJws.getHeader(); System.out.println(header); Claims body = claimsJws.getBody(); System.out.println(body);
jackjson 的支持 引入依赖
1 2 3 4 5 6 7 8 9 10 <!--jackson 支持--> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <!-- or jjwt-gson if Gson is preferred --> <version>0.11.2</version> <scope>runtime</scope>
1 2 3 4 5 6 //设置序列化方式 jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer()); //设置反序列化方式 jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer());
springsecurity 整合jwt 首先回忆一下springsecurity 的默认登录和鉴权流程;
springsecurity 中主要由一整套过滤器链来处理,不同的过滤器处理不同的功能;
ChannelProcessingFilter,因为它可能需要重定向到不同的协议SecurityContextPersistenceFilter,因此可以在web请求开头的SecurityContextHolder中设置SecurityContext,并且SecurityContext的任何更改都可以复制到HttpSession当web请求结束时(准备好与下一个web请求一起使用)ConcurrentSessionFilter,因为它使用SecurityContextHolder功能并需要更新SessionRegistry以反映来自校长的持续请求身份验证处理机制 - UsernamePasswordAuthenticationFilter,CasAuthenticationFilter,BasicAuthenticationFilter等 - 以便SecurityContextHolder可以修改为包含有效的Authentication请求令牌 SecurityContextHolderAwareRequestFilter,如果您使用它将Spring Security识别HttpServletRequestWrapper安装到您的servlet容器中JaasApiIntegrationFilter,如果SecurityContextHolder位于SecurityContextHolder,则会将FilterChain视为JaasAuthenticationToken中的SubjectRememberMeAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,并且请求提供了一个启用记住我服务的cookie,则会在那里放置一个合适的记忆Authentication对象AnonymousAuthenticationFilter,如果没有早期的身份验证处理机制更新SecurityContextHolder,那么匿名Authentication对象将被放置在那里ExceptionTranslationFilter,捕获任何Spring Security异常,以便可以返回HTTP错误响应或启动适当的AuthenticationEntryPointFilterSecurityInterceptor,用于保护web URI并在访问被拒绝时引发异常对于一个具有session登录的流程的过滤器链执行顺序是;
当发起登录请求的时候:
UsernamePasswordAuthenticationFilter 处理登录请求的参数和处理 将登录的信息放入session中;同时将Authentication 放入SecurityContext 中;
再次发起请求时 由 SecurityContextPersistenceFilter 从请求中获取请求的session信息获取Authentication 用于后续的流程校验;
整合基本要做的有
禁用session,不需要做任何session相关的处理。 未登录的时候能返回一个json提示,而不是登录页面。 登录成功后以json的返回成功和失败信息和jwt字符串 后续请求在header中卸载jwt 需要通过一个自定义的filter 从header中解析出来任何设置到 SecurityContext 中; jwt 工具类 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.2</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.2</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.2</version> <scope>compile</scope> </dependency>
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 //提供了2个方法都是从UserDetails 中生成信息和获取信息 public class JwtUtil { private static Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256); private static String ISS_USER = "web"; private static String SUBJECT = "auth"; /** * 创建jwt * @param userDetails * @return */ public static String createJwt(UserDetails userDetails){ JwtBuilder jwtBuilder = Jwts.builder(); Calendar instance = Calendar.getInstance(); instance.add(Calendar.MINUTE,30); Date expire = instance.getTime(); jwtBuilder = jwtBuilder.setIssuer(ISS_USER) .setSubject(SUBJECT) .setAudience(userDetails.getUsername()) .setExpiration(expire) .setNotBefore(new Date()) .setIssuedAt(new Date()) .setId(userDetails.getUsername()); JSONObject jsonObject = new JSONObject(); jsonObject.putOpt("roleCodes",((MyUserDetails)userDetails).getRoleCodes()); jsonObject.putOpt("permissionCodes",((MyUserDetails)userDetails).getPermissionCodes()); jwtBuilder.addClaims(jsonObject); jwtBuilder = jwtBuilder.signWith(key); //执行压缩 使生成的字符串变小 jwtBuilder = jwtBuilder.compressWith(CompressionCodecs.DEFLATE); jwtBuilder = jwtBuilder.serializeToJsonWith(new JacksonSerializer()); return jwtBuilder.compact(); } /** * 解析jwt * @param jwtStr * @return */ public static UserDetails parseJwt(String jwtStr){ JwtParserBuilder jwtParserBuilder = Jwts.parserBuilder(); jwtParserBuilder = jwtParserBuilder.setSigningKey(key); jwtParserBuilder = jwtParserBuilder.deserializeJsonWith(new JacksonDeserializer()); Jws<Claims> claimsJws = jwtParserBuilder.build().parseClaimsJws(jwtStr); MyUserDetails userDetails = new MyUserDetails(); Claims claims = claimsJws.getBody(); userDetails.setUserName(claims.getId()); userDetails.setRoleCodes((List)claims.get("roleCodes")); userDetails.setPermissionCodes((List)claims.get("permissionCodes")); return userDetails; } }
配置无权限的json返回 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 @Bean public AuthenticationEntryPoint authenticationEntryPoint(){ return new AuthenticationEntryPoint() { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt("code","4001"); jsonObject.putOpt("message","未登录"); writeJson(response,jsonObject); } }; } private void writeJson(HttpServletResponse response, JSONObject jsonObject){ try { response.setCharacterEncoding("utf-8"); response.setContentType("application/json; charset=utf-8"); PrintWriter writer = response.getWriter(); writer.write(jsonObject.toString()); } catch (IOException e) { e.printStackTrace(); } } @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and(). //配置exceptionHandling exceptionHandling().authenticationEntryPoint(authenticationEntryPoint())
处理登录成功或失败的处理 在failureHandler 中处理登录错误的信息;
successHandler 中返回登录成功和jwt字符串信息;
sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) 禁用session
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 @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().anyRequest().authenticated().and(). exceptionHandling().authenticationEntryPoint(authenticationEntryPoint()) .and() .formLogin().loginProcessingUrl("/loginDo").failureHandler(new AuthenticationFailureHandler(){ @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt("code","4002"); jsonObject.putOpt("message","登录错误"); writeJson(response,jsonObject); } }).successHandler(new AuthenticationSuccessHandler() { @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { JSONObject jsonObject = new JSONObject(); jsonObject.putOpt("code","2000"); jsonObject.putOpt("message","登录成功"); //生成token Object getPrincipalObj = authentication.getPrincipal(); if(getPrincipalObj instanceof MyUserDetails){ MyUserDetails details = (MyUserDetails)getPrincipalObj; String jwt = JwtUtil.createJwt(details); jsonObject.putOpt("token",jwt); } writeJson(response,jsonObject); } }).permitAll().and() .csrf().disable() .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and().addFilterBefore(new TokenAuthFilter(authenticationManager(),authenticationEntryPoint()),UsernamePasswordAuthenticationFilter.class).httpBasic(); }
添加 自定义解析jwtFilter 配置添加自定义的filter ,添加到 UsernamePasswordAuthenticationFilter 之前;
此filter主要逻辑是从header中解析 jwt 信息,如果能获取到就封装成UsernamePasswordAuthenticationToken 并设置到SecurityContext 中去.
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 public class TokenAuthFilter extends BasicAuthenticationFilter { private AuthenticationEntryPoint authenticationEntryPoint; public TokenAuthFilter(AuthenticationManager authenticationManager) { super(authenticationManager); } public TokenAuthFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) { super(authenticationManager, authenticationEntryPoint); } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { try { UsernamePasswordAuthenticationToken authRequest = getUsernamePasswordAuthenticationToken(request); if (authRequest == null) { this.logger.trace("Did not process authentication request since failed to find " + "username and password in Basic Authorization header"); chain.doFilter(request, response); return; } String username = authRequest.getName(); this.logger.trace(LogMessage.format("Found username '%s' in Basic Authorization header", username)); if (authenticationIsRequired(username)) { SecurityContextHolder.getContext().setAuthentication(authRequest); } } catch (AuthenticationException ex) { SecurityContextHolder.clearContext(); this.logger.debug("Failed to process authentication request", ex); onUnsuccessfulAuthentication(request, response, ex); this.authenticationEntryPoint.commence(request, response, ex); return; } chain.doFilter(request, response); } private boolean authenticationIsRequired(String username) { Authentication existingAuth = SecurityContextHolder.getContext().getAuthentication(); if (existingAuth == null || !existingAuth.isAuthenticated()) { return true; } if (existingAuth instanceof UsernamePasswordAuthenticationToken && !existingAuth.getName().equals(username)) { return true; } return (existingAuth instanceof AnonymousAuthenticationToken); } private UsernamePasswordAuthenticationToken getUsernamePasswordAuthenticationToken(HttpServletRequest request) { String header = request.getHeader(HttpHeaders.AUTHORIZATION); if (header == null) { return null; } header = header.trim(); try{ UserDetails userDetails = JwtUtil.parseJwt(header); UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userDetails.getUsername(), userDetails.getPassword(),userDetails.getAuthorities()); return token; }catch (Exception e){ e.printStackTrace(); } return null; }
测试 未携带jwt 或jwt错误返回
1 2 3 4 { "code": "4001", "message": "未登录" }
执行登录返回jwt信息;