admin

springboot+jjwt+jersey编写基于jwt验证的接口(完整版)
又双叒叕因为项目的需要,我去学习了用JAVA去开发一个基于jwt验证的接口,整体来说并不算太难,理清思路还是比较容...
扫描右侧二维码阅读全文
25
2018/11

springboot+jjwt+jersey编写基于jwt验证的接口(完整版)

又双叒叕因为项目的需要,我去学习了用JAVA去开发一个基于jwt验证的接口,整体来说并不算太难,理清思路还是比较容易的,下面我们就开始动手吧。

1.首先在pom.xml中添加依赖,我们用的是0.9.0的jjwt库:

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

2.在我们的数据模型package下创建一个jwt package,创建两个类:

认证信息类AccessPara.java

public class AccessPara {

    public AccessPara(String clientId,String userName,String passWord){
        setClientId(clientId);
        setUserName(userName);
        setPassword(passWord);
    }

    private String clientId;
    private String userName;
    private String password;

    public String getClientId() {
        return clientId;
    }
    public void setClientId(String clientId) {
        this.clientId = clientId;
    }
    public String getUserName() {
        return userName;
    }
    public void setUserName(String userName) {
        this.userName = userName;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
}

token返回结果类AccessToken.java

public class AccessToken {
    private String access_token;
    private String token_type;
    private long expires_in;
    public String getAccess_token() {
        return access_token;
    }
    public void setAccess_token(String access_token) {
        this.access_token = access_token;
    }
    public String getToken_type() {
        return token_type;
    }
    public void setToken_type(String token_type) {
        this.token_type = token_type;
    }
    public long getExpires_in() {
        return expires_in;
    }
    public void setExpires_in(long expires_in) {
        this.expires_in = expires_in;
    }
}

3.在common包中添加JWT常量配置类 Constant.java

public class Constant {
    public static final String JWT_ID = UUID.randomUUID().toString();

    /**
     * 加密密文
     */
    public static final String JWT_SECRET = "jwtSecret";   //可以自己定义
    public static final int JWT_TTL = 60*60*1000;  //millisecond  秘钥过期时间
}

4.添加构造jwt和解析jwt的帮助类 JwtHelper.java

public class JwtHelper {

    /**
     * 由字符串生成加密key
     *
     * @return
     */
    public SecretKey generalKey() {
        String stringKey = Constant.JWT_SECRET;

        // 本地的密码解码
        byte[] encodedKey = Base64.decodeBase64(stringKey);

        // 根据给定的字节数组使用AES加密算法构造一个密钥
        SecretKey key = new SecretKeySpec(encodedKey, 0, encodedKey.length, "AES");

        return key;
    }

    /**
     *
     *     jti(JWT ID):是JWT的唯一标识。
     *     iss(Issuser):代表这个JWT的签发主体;
     *     sub(Subject):代表这个JWT的主体,即它的所有人;
     *      aud(Audience):代表这个JWT的接收对象;
     *      exp(Expiration time):是一个时间戳,代表这个JWT的过期时间;
     *      nbf(Not Before):是一个时间戳,代表这个JWT生效的开始时间,意味着在这个时间之前验证JWT是会失败的;
     *      iat(Issued at):是一个时间戳,代表这个JWT的签发时间;
     * 创建jwt
     * @param id
     * @param issuer
     * @param subject
     * @param ttlMillis
     * @return
     * @throws Exception
     */
    public String createJWT(String id, String issuer, String subject, long ttlMillis) throws Exception {

        // 指定签名的时候使用的签名算法,也就是header那部分,jjwt已经将这部分内容封装好了。
        SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;

        // 生成JWT的时间
        long nowMillis = System.currentTimeMillis();
        Date now = new Date(nowMillis);

        // 创建payload的私有声明(根据特定的业务需要添加,如果要拿这个做验证,一般是需要和jwt的接收方提前沟通好验证方式的)
       // Map<String, Object> claims = new HashMap<>();
        //claims.put("uid", "123456");
        //claims.put("user_name", "admin");
        //claims.put("nick_name", "X-rapido");

        // 生成签名的时候使用的秘钥secret,切记这个秘钥不能外露哦。它就是你服务端的私钥,在任何场景都不应该流露出去。
        // 一旦客户端得知这个secret, 那就意味着客户端是可以自我签发jwt了。
        SecretKey key = generalKey();

        // 下面就是在为payload添加各种标准声明和私有声明了
        JwtBuilder builder = Jwts.builder() // 这里其实就是new一个JwtBuilder,设置jwt的body
           //     .setClaims(claims)          // 如果有私有声明,一定要先设置这个自己创建的私有的声明,这个是给builder的claim赋值,一旦写在标准的声明赋值之后,就是覆盖了那些标准的声明的
                .setId(id)                  // 设置jti(JWT ID):是JWT的唯一标识,根据业务需要,这个可以设置为一个不重复的值,主要用来作为一次性token,从而回避重放攻击。
                .setIssuedAt(now)           // iat: jwt的签发时间
                .setIssuer(issuer)          // issuer:jwt签发人
                .setSubject(subject)        // sub(Subject):代表这个JWT的主体,即它的所有人,这个是一个json格式的字符串,可以存放什么userid,roldid之类的,作为什么用户的唯一标志。
                .signWith(signatureAlgorithm, key); // 设置签名使用的签名算法和签名使用的秘钥

        // 设置过期时间
        if (ttlMillis >= 0) {
            long expMillis = nowMillis + ttlMillis;
            Date exp = new Date(expMillis);
            builder.setExpiration(exp);
        }
        return builder.compact();
    }

    /**
     * 解密jwt
     *
     * @param jwt
     * @return
     * @throws Exception
     */
    public Claims parseJWT(String jwt) throws JwtException {
        SecretKey key = generalKey();  //签名秘钥,和生成的签名的秘钥一模一样
        Claims claims = Jwts.parser()  //得到DefaultJwtParser
                .setSigningKey(key)                 //设置签名的秘钥
                .parseClaimsJws(jwt).getBody();     //设置需要解析的jwt
        return claims;
    }


}

都有详细的注释了,我就不一一解释了。

5.接着,我们就可以在控制器中编写获取AccessToken的接口请求代码了,具体代码如下:

    @Path("/getToken")
    @POST
    @Consumes("application/x-www-form-urlencoded")
    @Produces("application/json;chartset=utf-8")
    public Object getToken(@FormParam("client_id") String client_id,@FormParam("client_name") String userName)
    {
        AccessPara user = new AccessPara(client_id, userName, "123456");
        String subject = new Gson().toJson(user);
        AccessToken atEntity = new AccessToken();
        try {
            JwtHelper util = new JwtHelper();
            String accessToken = util.createJWT(Constant.JWT_ID, "hymake", subject, Constant.JWT_TTL);
            System.out.println("accessToken:" + accessToken);

            atEntity.setAccess_token(accessToken);
            atEntity.setExpires_in(Constant.JWT_TTL);
            atEntity.setToken_type("bearer");
            System.out.println("\n解密\n");

            Claims c = util.parseJWT(accessToken);
            System.out.println(c.getId());
            System.out.println("签发时间:"+c.getIssuedAt());
            System.out.println(c.getSubject());
            System.out.println(c.getIssuer());
            System.out.println("过期时间:"+c.getExpiration());


        } catch (Exception e) {
            e.printStackTrace();
        }
        return atEntity;
    }

正常来说,jwt接口需要传入用户的业务ID,用户名和密码,因本接口需求比较简单,所以只需传入业务id和用户名区分一下即可。
上述代码的流程可以概述为以下几点

  1. 创建用户对象,用来作为jwt的主体
  2. 创建返回实体对象,转化用户对象为json
  3. 通过jwtHelper 生成AccessToken字符串,放入返回实体中。
  4. 解析AccessToken,打印出相关的签证信息。
  5. 返回实体。

postman结果如下:
获取accesstoken.png

这样我们的AccessToken就返回成功了。

有了AccessToken,我们怎么样才能在别人请求我们项目的所有接口时,都要验证一下是否携带了正确的AccessToken呢?答案是:配置过滤器


(注:如果使用springboot + jjwt的同学,可以使用interceptor,也就是拦截器进行请求拦截,因interceptor没法拦截jersey配置后的地址,所以本项目改用过滤器Filter进行请求拦截。)


1.先列出我的接口返回实体类ResContainer.java和返回码枚举类ResultEnum.java

public class ResContainer<T>{
    private T data;
    private Integer retCode;
    private String retMsg;

    public String getRetMsg() {
        return retMsg;
    }

    public void setRetMsg(String retMsg) {
        this.retMsg = retMsg;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }

    public Integer getRetCode() {
        return retCode;
    }

    public void setRetCode(Integer retCode) {
        this.retCode = retCode;
    }
}

通过泛型往data里填充接口返回的信息,retCoderetMsg则通过枚举类的各种状况来赋值。

public enum ResultEnum {

    SUCCESS(200,"成功"),
    SUCCESS_201(201,"未查询到业务信息!"),
    UNKNOWN_ERROR(-1, "未知错误"),
    HEADER_ERROR(1, "头部未包含authorization或者没有bearer token信息"),
    LOGIN_ERROR(2, "登录失败,用户名或密码错误"),
    SIGNATURE_ERROR(3, "JWT签名与本地计算的签名不匹配,JWT的有效性不能被验证,也不应该被信任"),
    TOKEN_EXP_ERROR(4,"accessToken已过期,请重新获取!"),
    TOKEN_ERROR(5,"accessToken解析错误!")
    ;


    private Integer code;

    private String message;



    ResultEnum(Integer code, String message) {
        this.code = code;
        this.message = message;
    }

    public Integer getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }
}

2.再接着,在common下创建一个结果工具类ResultUtil.java,用来结合我们上面的返回结果和返回码:

@Component
public class ResultUtil {
    /**
     * 请求成功返回
     * @param object
     * @return
     */
    public static ResContainer success(Object object){
        ResContainer ResContainer=new ResContainer();
        ResContainer.setRetCode(ResultEnum.SUCCESS.getCode());
        ResContainer.setRetMsg(ResultEnum.SUCCESS.getMessage());
        ResContainer.setData(object);
        return ResContainer;
    }

    public static ResContainer success_201(Object object){
        ResContainer ResContainer=new ResContainer();
        ResContainer.setRetCode(ResultEnum.SUCCESS_201.getCode());
        ResContainer.setRetMsg(ResultEnum.SUCCESS_201.getMessage());
        ResContainer.setData(object);
        return ResContainer;
    }

    public static ResContainer success(){
        return success(null);
    }

    public static ResContainer error(Integer code,String resultmsg){
        ResContainer ResContainer=new ResContainer();
        ResContainer.setRetCode(code);
        ResContainer.setRetMsg(resultmsg);
        return ResContainer;
    }
    
}

3.最后,我们来看看过滤器怎么写:

@WebFilter(filterName = "JwtFilter",urlPatterns = {"/TESTAPI/*"})
public class JwtFilter implements Filter {
    private static Logger logger = LoggerFactory.getLogger(ResourceApi.class);

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {

    }

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        final HttpServletRequest request = (HttpServletRequest) req;
        final HttpServletResponse response = (HttpServletResponse) res;

        //等到请求头信息authorization信息
        final String authHeader = request.getHeader("authorization");

        if ("OPTIONS".equals(request.getMethod())) {
            response.setStatus(HttpServletResponse.SC_OK);
            chain.doFilter(req, res);
        } else {

            if (authHeader == null || !authHeader.startsWith("Bearer")) {

                toResponse(response,request,ResultEnum.HEADER_ERROR);
                return;
            }
            String token = authHeader.substring(7);

            try {
                JwtHelper jwtHelper = new JwtHelper();
                Claims claims =  jwtHelper.parseJWT(token);
                if(claims == null){
                    toResponse(response,request,ResultEnum.LOGIN_ERROR);
                    return;
                }
            } catch (final JwtException e) {
                if(e instanceof SignatureException){
                    toResponse(response,request,ResultEnum.SIGNATURE_ERROR);
                }else if(e instanceof ExpiredJwtException){
                    toResponse(response,request,ResultEnum.TOKEN_EXP_ERROR);
                }else{
                    toResponse(response,request,ResultEnum.TOKEN_ERROR);
                }
                //throw new NoteException(ResultEnum.TOKEN_ERROR);
                return;

            }

            chain.doFilter(req, res);
        }
    }

    /**
     * 响应
     * @param response
     * @param i 类型
     * @throws IOException
     */
    private void toResponse(HttpServletResponse response,HttpServletRequest request,ResultEnum re) throws IOException {
        HttpServletResponse httpResponse = response;
        httpResponse.setCharacterEncoding("UTF-8");
        httpResponse.setContentType("application/json; charset=utf-8");
        httpResponse.setHeader("Access-Control-Allow-Origin","*");
        httpResponse.setHeader("Access-Control-Allow-Credentials", "true");
        httpResponse.setHeader("Access-Control-Allow-Methods", "POST,GET,OPTIONS,DELETE,PATCH,PUT");
        httpResponse.setHeader("Access-Control-Max-Age", "3600");
        httpResponse.setHeader("Access-Control-Allow-Headers", "Origin,X-Requested-With,x-requested-with,X-Custom-Header," +
                "Content-Type,Accept,Authorization");
        String method = request.getMethod();
        if ("OPTIONS".equalsIgnoreCase(method)){
            logger.info("OPTIONS请求");
            httpResponse.setStatus(HttpServletResponse.SC_ACCEPTED);
        }

        ObjectMapper mapper = new ObjectMapper();
        PrintWriter writer = httpResponse.getWriter();
        ResultUtil resultUtil = new ResultUtil();

            ResContainer resContainer = resultUtil.error(re.getCode(),re.getMessage());
            writer.write(mapper.writeValueAsString(resContainer));


        writer.close();

        if (writer!=null)
            writer = null;
    }

    @Override
    public void destroy() {

    }

主要代码在doFilter里,我们根据if判断以及Exception的类型判断,去执行toResponse函数返回各项jwt的异常错误码和信息。
我们来测试一下:

  • 执行一个普通的接口,不包含auth头部

没头部.png

  • 执行接口,包含一个过期的auth

1.png

  • 赋予正常的token,返回正确的接口信息

成功.png

参考文章:Json Web Token JWT的Java使用 (JJWT)

Last modification:November 26th, 2018 at 09:44 am
If you think my article is useful to you, please feel free to appreciate

Leave a Comment