JWT结合Springboot+shiro,session、token同时存在来应对不同的业务场景(物联网设备管理及开放api)

JWT结合Springboot+shiro,session、token同时存在来应对不同的业务场景(物联网设备管理及开放api)

一、背景说明

需求是这样滴:对物联网终端设备以及网关设备进行统一的管理,这里需要一个设备管理平台,同时呢,计划开放API,以供应用开发者调用API来管理控制设备。设备管理平台本身的用的是传统的session来管理,设备管理者数量并不多,所以不会有超量的session给服务器造成太大的压力。开放API给第三方应用用户是为了应对第三方用户开发的各种移动端app以及需要自身维护的设备管理。所以用session就不是那么合适,计划采取token的方式。

多年以前我用过token这种方式来开发,那时候似乎还没有jwt这个框架,我记得是根据用户名密码生成token后存在数据库中的,每次token进来是需要从数据库中或者提前缓存的token池中来找到匹配的token以确保不是非法请求。

闲话多了,看看正题。

二、JWT以及JJWT介绍

首先呢,我们可以通过这里来看看JWT是个什么样的东西:https://jwt.io/introduction/ 官方说的很清楚了,我就用我蹩脚的英文来给大家解释下:

1、什么是JSON Web Token?

JSON Web Token (JWT)是一个开放的标准(RFC 7519),它定义了一种简洁独立的方式,以JSON对象的形式在各方之间安全地传输信息。

2、什么时候使用JWT呢?

授权和信息交换的时候

3、JWT结构介绍

JWT说白了,就是一串字符串,包含三个部分,三部分之间用“.”来分割。三部分分别是:
  • Header

  • Payload

  • Signature

    最后形成的字符串就像这样:xxxxx.yyyyy.zzzzz

Header大概就是这样的:

{
  "alg": "HS256",
  "typ": "JWT"
}
payload就是放内容的,官方叫做claims,这个是啥玩意呢?这玩意是声明一些实体,包括jwt自己已经定义好的特色的声明,还有一些用户加上的声明(我们这些开发者想加上的)以及一些附加数据

这玩意有三种类型,分别是  *registered* ,  *public* , and  *private*  claims. Registered Claims就是官方已经定义了的,比如: **iss**  (issuer),  **exp**  (expiration time),  **sub**  (subject),  **aud** (audience)  public呢,就是自己可以随意定义了,要注意避免命名空间的冲突,https://www.iana.org/assignments/jwt/jwt.xhtml。 private就是几方之间约定的,没有注册public的claims。感觉说多了自己都晕。

说白了就是一些key value,大概是这个样子的:
{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}
 signature是签名喽,就是你要发这些,你签个字再发,大概就是这个样子滴。
HMACSHA256(
  base64UrlEncode(header) + "." +
  base64UrlEncode(payload),
  secret)
最终形成这么个玩意:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.

SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

4、JJWT是啥?

呃,就是Java JSON Web Token。JWT的一个java实现,如果是做Java开发的直接用JJWT得了。

三、和springboot整合

作为一个正常的开发者,和springboot整合这种事情的第一反应就是添加依赖,先把jar之类的搞起来再说,下面这个不用说了吧,spring的pom文件中添加依赖,如果看这个蒙圈的话,请学习springboot……相关内容。

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

然后我就在想,我要的效果是:

1)我的客户在我的平台上注册一个账户。

2)然后通过这个账户创建一个APP,平台会根据规则(你自己定)生成一个appEUI(app全球唯一编码)和一个appSecret,把appEUI和appSecret在页面上展示给客户。

3)这个时候告诉客户,你要想访问平台各种设备接口,那么首先用appEUI和appSecret生成token吧!然后访问的时候把这个token放在httpheader里,我服务端收到请求的时候会监控的啦。(在拦截器中)。

嗯……应该就是这样了。

得有个Token的生成和解析的TokenService吧,就是我需要生成token的时候,调用一把这个service,然后把结果给请求者。

@Service
public class TokenService {

    /**
     * 有效期7天
     */
    private static final int EXPIRE_TIME = 7;

    /**
     * 盐
     */
    private static final String signingKey = "secret";

    /**
     * 创建token
     * @param appEUI
     * @param appSecret
     * @return
     */
    public String createToken(String appEUI,String appSecret){
        //签发时间
        Date iatTime = new Date();
        //expire time
        Calendar nowTime = Calendar.getInstance();
        nowTime.add(Calendar.DATE,7);
        Date expireTime = nowTime.getTime();

        Claims claims = Jwts.claims();
        claims.put("appEUI",appEUI);
        claims.put("appSecret",appSecret);
        claims.setIssuedAt(iatTime);

        String token = Jwts.builder().setClaims(claims).setExpiration(expireTime)
                .signWith(SignatureAlgorithm.HS256,signingKey).compact();
        return token;
    }

    /**
     * 解析token
     * @param token
     */
    public void parseToken(String token){
        Jws<Claims> jws = Jwts.parser().setSigningKey(signingKey).parseClaimsJws(token);
        Claims claims = jws.getBody();
        Map<String,String> header = jws.getHeader();
        System.out.println("parse");
    }
}

请求进来后我首先要看看是管理端的还是第三方客户的,如果是第三方客户的,还有看有没有token,如果有token,还要看对不对,如果对,还要看在不在有效期……好烦。好吧,首先得从拦截器入手分析,这又涉及到一个知识点:拦截器,它的作用呢,就是当有个请求来的时候,来判断这个请求是不是合法,比如你想验证session是不是过期,就可以在拦截器中做,如果过期就跳转到登陆页面。在这个项目里呢,我设置了两个拦截器,分别是:

ApiInterceptor:用来拦截所有的第三方用户请求。

UserActionInterceptor:用来拦截所有的管理平台用户请求。

拦截器建立好了后,如果要启用哪个拦截器,就需要在继承了WebMvcConfigurer接口的类中来启用它,就像你买了两个摄像头,需要通电来启用一样。

@Configuration
public class WebAppConfigurer implements WebMvcConfigurer {

    /**
     * 保障在spring加载的时候注入拦截器,可以在拦截器中使用业务service。
     * @return
     */
    @Bean
    UserActionInterceptor userActionInterceptor(){
        return new UserActionInterceptor();
    }
    @Bean
    ApiInterceptor apiInterceptor(){return new ApiInterceptor();}
    @Override
    public void addInterceptors(InterceptorRegistry interceptorRegistry) {
        // 可添加多个
        interceptorRegistry.addInterceptor(userActionInterceptor())
                .addPathPatterns("/**")
                .excludePathPatterns("/login/**")
                .excludePathPatterns("/user/login")
                .excludePathPatterns("/api/**");
        interceptorRegistry.addInterceptor(apiInterceptor())
                .addPathPatterns("/api/**")
                .excludePathPatterns("/api/getToken");
    }
}

可以看到,在userActionInterceptor拦截器中,拦截所有路径,排除以api开头的路径;在apiInterceptor中拦截所有api开头的,但是需要排除生成token的路径。这样通过拦截器把内容用户和外部api接口请求分割开来。

ApiInterceptor核心代码:

public class ApiInterceptor implements HandlerInterceptor {
    //可以在这里设置各种规则,取到token后解析,来验证token有效性,有效期等等。这里仅仅验证了是不是token为空。
    @Override
    public boolean preHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o) throws Exception {
        String token = httpServletRequest.getHeader("v-token");//这个就是从http头中取约定好的token的key。
        try{
            if(token==null||token.trim().equals("")){
                throw new SignatureException("token is null");
            }
        }catch (SignatureException e){
            JSONObject jsonObject = new JSONObject();
            jsonObject.put("msg","请求参数中找不到Token");
            jsonObject.put("code", Code.NO_TOKEN);
            createSuccessResponse(jsonObject,httpServletResponse);
            return false;
        }
      
        return true;
    }

ApiController:用来生成token以及得到token之后通过token来请求其他接口。

@Controller
@RequestMapping("/api")
public class ApiController {

    @Autowired
    TokenService tokenService;

    @RequestMapping(value = "/getToken",method = RequestMethod.POST)
    @ResponseBody
    public ApiResult getToken(String appEUI,String appSecret){
        String token =  tokenService.createToken(appEUI,appSecret);
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("token",token);
        jsonObject.put("expireTime", Calendar.getInstance().getTime());
        ApiResult result = new ApiResult();
        result.setCode(Code.SUCCESS);
        result.setMsg("操作成功");
        result.setData(jsonObject.toJSONString());
        return result;
    }
    @RequestMapping(value = "/addNode",method = RequestMethod.POST)
    @ResponseBody
    public ApiResult addNode(){
        ApiResult result = new ApiResult();
        //TODO 各种API接口就在这个类里搞了。
        return result;
    }
}

Api请求的结果就是通过这个bean ApiResult来返回给接口请求者:

public class ApiResult implements Serializable {

    /**
     * 状态码
     */
    private int code;
    /**
     * 结果 success,error
     */
    private String msg;
    /**
     * 数据
     */
    private String data;

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public String getData() {
        return data;
    }

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

四、排除shiro控制

因为我不需要用shiro来控制第三方用户的授权,所以我在shiro配置中进行排除

filterChainDefinitionMap.put("/api/**","anon");

原文:https://my.oschina.net/u/3658506/blog/2985967

1赞