SpringBoot 使用JWT

SpringBoot 使用JWT

当下微服务架构在互联网企业中得到了广泛的应用,其便捷的开发方式也得到了大多数Java工程师的喜爱。但是当一个大系统拆分成各个小系统后其系统间的会话管理便成了一个令人头疼的问题,今天要说的JWT(Json Web Token)就是一种解决这种问题的方式,而且该技术也在业界得到了广泛的应用。

JWT是什么?

我的理解就是一个base64编码后的字符串。
例如:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

可以看到,该字符串被"."符号分割成了三个部分,每个部分所代表的意义不一样。
第一部分我们称为头部,也就是“eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9”,这一串base64解码后得到一个json

{
  "alg": "HS256",
  "typ": "JWT"
}

声明一种加密方式,一般来说我们在使用的时候头部是固定的,无需修改任何内容。
第二部分称作负载,也就是eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ。这串字符串解析出来也是一个json

{
  "sub": "1234567890",
  "name": "John Doe",
  "iat": 1516239022
}

这里面包含了各种信息,一般来说将业务相关的信息放在这一部分,但是敏感数据还是不要放在这里了。
第三部分为签名,是为了保证jwt的数据不被篡改。具体算法是一部分的字符串和第二部分的字符串用"."连接组成一个字符串,然后通过第一部分中声明的加密方式进行加盐secret组合加密。

如何在springboot中使用

首先了解下JWT工作原理

这是JWT官网的一张图,理解起来就是客户端向服务端发起登录登录请求,服务器处理登录请求,成功则生成JWT并返回给客户端,客户端再次向服务端发起请求,此时将服务端返回的JWT带在请求header中一并返回,服务端接收到请求后先认证JWT是否合法,合法则处理后续业务逻辑,否则认定为请求非法。
原理就这些了,现在上实践:
1.搭建一个springboot web项目。
2.maven中添加依赖

<dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt</artifactId>
            <version>0.9.0</version>
</dependency>

3.编写jwt工具类该类主要用于生成jwt和解码jwt

/**
     *生成jwt字符串,参数列表可以自定义
     * @param name 用户名(自定义字段)
     * @param userId 用户id(自定义字段)
     * @param role 用户角色(自定义字段)
     * @param TTLSeconds 有效时长
     * @return jwt字符串
     */
    public static String createJWT(String name, String userId, String role,long TTLSeconds) {
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);
        byte[] keyBytes = DatatypeConverter.parseBase64Binary(Constants.JWT_SECRET);
        Key signKey = new SecretKeySpec(keyBytes, signatureAlgorithm.getJcaName());
//此处JwtBuilder是jwt包提供的用于生成jwt字符串的构造类,“setHeaderParam”表示设置jwt预定义字段的值,“claim”表示设置自定义字段及值
        JwtBuilder builder = Jwts.builder().setHeaderParam("typ", "JWT")
                .claim("name", name)
                .claim("id", userId)
                .claim("role", role)
                .signWith(signatureAlgorithm, signKey);
        if (TTLSeconds > 0) {
            Date expTime = new Date(nowMillis + TTLSeconds * 1000);
            builder.setExpiration(expTime).setNotBefore(now);
        }
        return builder.compact();
    }

解析jwt字符串

/**
     * 解析jwt字符串
     * @param token
     * @return
     */
    public static Claims parseJWT(String token) {
        try {
            return Jwts.parser().setSigningKey(DatatypeConverter.parseBase64Binary(Constants.JWT_SECRET)).parseClaimsJws(token).getBody();
        } catch (ExpiredJwtException e) {
            log.error("jwt 过期!", e);
            return null;
        } catch (SignatureException e) {
            log.error("jwt 解码失败!", e);
            return null;
        } catch (Exception e) {
            log.error("jwt 未知异常!",e);
            return null;
        }
    }

返回的Claims本质是一个map,通过它何以获取JWT中包含的字段。
4.编写JWT过滤器。回顾其工作原理,在客户端请求到达服务端时,需要先验证JWT是否合法,因此将这个过程放在过滤器中是个不错的办法。

public class JwtFilter extends GenericFilterBean {




    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        final HttpServletRequest req = (HttpServletRequest) servletRequest;
        final HttpServletResponse resp = (HttpServletResponse) servletResponse;
        final String authHeader = req.getHeader("authorization");
        if (authHeader != null && authHeader.startsWith("bearer;")) {
            final String token = authHeader.substring(7);
              //解析jwt字符串
            final Claims claims = JwtHelper.parseJWT(token);
            if (claims != null) {
              //获取jwt中的参数并放入request中以便后面的业务逻辑使用
                req.setAttribute(Constants.KEY_USER_ID,claims.get("id"));
                filterChain.doFilter(req,resp);
                return;
            }
        }
                //验证未通过直接返回
        resp.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
        resp.setCharacterEncoding("UTF-8");
        resp.setContentType("application/json;charset=utf-8");
        ObjectMapper mapper = new ObjectMapper();
        ResponseVO vo = ResponseVO.buildResponse(ReturnCode.AUTH_FAIL);
        resp.getWriter().write(mapper.writeValueAsString(vo));
    }
}

5.springboot中注册过滤器和过滤规则

@SpringBootApplication
public class CmsApplication {

    public static void main(String[] args) {
        SpringApplication.run(CmsApplication.class, args);
    }


    /**
     * 注册jwt过滤器
     * @return
     */
    @Bean
    public FilterRegistrationBean jwtFilter() {
        final FilterRegistrationBean<JwtFilter> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new JwtFilter());
        List<String> urlPatterns = new ArrayList<>();
        urlPatterns.add("/index/hello");
        registrationBean.addUrlPatterns(urlPatterns.toArray(new String[urlPatterns.size()]));
        return registrationBean;

    }


}

6.编写Controller。

@RestController
@RequestMapping("/index")
public class IndexController {
    @Autowired
    private CommonConfig config;
//用于验证jwt
    @RequestMapping("/hello")
    public ResponseVO hello(HttpServletRequest request) {
        String userId = (String) request.getAttribute(Constants.KEY_USER_ID);
        Map<String, Object> map = new HashMap<>();
        map.put("index", "hello");
        map.put("user_id",userId);
        return ResponseVO.buildResponse(ReturnCode.SUCCESS,map);
    }
//用于登录生成jwt
    @RequestMapping("/login")
    public ResponseVO login(String userName,String pass) {
        //省略了登录
        Map<String, String> map = new HashMap<>();
        String token = JwtHelper.createJWT(userName,"1","admin",config.getJwtExpTime());
        map.put("token", token);
        map.put("userName", userName);
        return ResponseVO.buildResponse(ReturnCode.SUCCESS, map);
    }
}

7.测试,测试工具我是用的是postman
生成

验证

将登录返回的JWT放入head中向服务器发起请求,服务器验证并返回成功。
如果在生成JWT时添加了过期时间,也就是在JWT工具类中的createJWT()方法中builder.setExpiration()方法设置了时间,则如果客户端发送的jwt超时后服务端将会抛出ExpiredJwtException,如果图省事则直接捕获该异常后返回客户端重新登录,如果想要更好的用户体验则需要自行进行过期时间验证并生成新的JWT。


作者:大大大浣熊
原文:https://www.jianshu.com/p/e6421dbaac4a