JWT是一种用户双方之间传递安全信息的简洁的、URL安全的表述性声明规范。JWT(Json Web Token)作为一个开放的标准(RFC 7519),定义了一种简洁的、自包含的方法用于通信双方之间以Json对象的形式进行安全性信息传递,传递时有数字签名所以信息时安全的,JWT使用RSA公钥密钥的形式进行签名。

JWT组成
JWT格式的输出是以.分隔的三段Base64编码,与SAML等基于XML的标准相比,JWT在HTTP和HTML环境中更容易传递。(形式:xxxxx.yyy.zzz):

1、Header:头部
2、Payload:负载
3、Signature:签名

Header
在header中通常包含了两部分,Token类型以及采用加密的算法

Payload
Token的第二部分是负载,它包含了Claim,Claim是一些实体(一般都是用户)的状态和额外的数据组成。

Signature
创建签名需要使用编码后的header和payload以及一个秘钥,使用header中指定签名算法进行签名。

JWT工作流程图
JWT客户端发送请求到服务器端整体流程:

jwt

引入依赖

<!--jwt依赖-->
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.7.0</version>
</dependency>

初始化信息

DROP TABLE IF EXISTS `api_token_infos`;
CREATE TABLE `api_token_infos` (
  `ati_id` int(255) NOT NULL AUTO_INCREMENT,
  `ati_app_id` varchar(100) DEFAULT NULL,
  `ati_token` blob,
  `ati_build_time` varchar(20) DEFAULT NULL COMMENT '生成token时间(秒单位)',
  PRIMARY KEY (`ati_id`),
  KEY `ati_app_id` (`ati_app_id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=30 DEFAULT CHARSET=utf8 COMMENT='api请求token信息表,根据appId保存';

-- ----------------------------
-- Records of api_token_infos
-- ----------------------------
INSERT INTO `api_token_infos` VALUES ('29', 'c2R4bWtqX21vYmlsZTQ4QTkxMzAwMTkyQzJFMTgzODc0N0NFMzk4MTREM0ZG', 0x65794A68624763694F694A49557A49314E694A392E65794A7A645749694F694A6A4D6C4930596C6430635667794D585A5A6257787A576C52524E4646556133684E656B46335456527265564636536B5A4E564764365430526A4D453477546B5A4E656D73305456525352553077576B63694C434A70595851694F6A45304F5449344E7A51784F545573496D6C7A63794936496B397562476C755A53425A515856306143424364576C735A475679496977695A586877496A6F784E446B794F4467784D7A6B3166512E30464251735536635A53796F46695371316F6F775737634D345358756A3944644439795544324956736B34, '1492874196023');

-- ----------------------------
-- Table structure for api_user_infos
-- ----------------------------
DROP TABLE IF EXISTS `api_user_infos`;
CREATE TABLE `api_user_infos` (
  `aui_app_id` varchar(100) NOT NULL COMMENT '授权唯一标识',
  `aui_app_secret` blob NOT NULL COMMENT '授权密钥',
  `aui_status` char(1) NOT NULL DEFAULT '1' COMMENT '用户状态,1:正常,0:无效',
  `aui_day_request_count` int(11) NOT NULL COMMENT '日请求量',
  `aui_ajax_bind_ip` varchar(100) DEFAULT NULL COMMENT '绑定IP地址多个使用“,”隔开',
  `aui_mark` varchar(255) DEFAULT NULL COMMENT '备注',
  PRIMARY KEY (`aui_app_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='api平台用户信息表';

-- ----------------------------
-- Records of api_user_infos
-- ----------------------------
INSERT INTO `api_user_infos` VALUES ('c2R4bWtqX21vYmlsZTQ4QTkxMzAwMTkyQzJFMTgzODc0N0NFMzk4MTREM0ZG', 0x6D4B6B377237494A6B632B52566A765068334D34504736564947556C6744376A4F6F3356776B484A4B534F4C395179643742573159496E764A582F4E324D4B52584A412F626B33524A3532444E376E4B41376464393251642B2B75712B33775342355359303871492F4F787038524979445635513679614B7541786E304767374566792F4374587562537A4A496D394748675878676D3079523135615573386358434B414C62544D34734E5632694F73544E616C3138395843395363457A5A323042576C4D4E4742637A676D4C7A3368464B71624F6E2F46384465535A3043395A43664137322B4A6B6A5A72723168775765537868465A473071706D6666355A4631324C736843383639682B374F707A6238466359655469716B5A7245596E71666663367A557659303533505368584C37644D38466E4546414F4749393457644A5041547936786C456C62664D492F6954412B4371513D3D, '1', '0', '*', '测试用户');

Model

model、JpaRepository、Controller省略。。。。

public class TokenResult implements Serializable{
    //状态
    private boolean flag = true;
    //返回消息内容
    private String msg = "";
    //返回token值
    private String token ="";

    public boolean isFlag() {
        return flag;
    }

    public void setFlag(boolean flag) {
        this.flag = flag;
    }

    public String getMsg() {
        return msg;
    }

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

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }
}

生成Token

生成Token方法的内容大致是,检查appId以及appSecret-->检查是否存在该appId的对应Token-->根据存在与否、过期与否执行更新或者写入操作-->返回用户请求。

在createNewToken方法中是JWT生成Token的方法,我们默认了过期时间为7200秒,上面是毫秒单位,我们生成token需要指定subject也就是我们的用户对象,设置过期时间、生成时间、还有签名生成规则等。token生成方法已经编写完成,下面我们需要在除了获取token的路径排除在外拦截所有的路径,验证路径是否存在header包含token,并且验证token是否正确,jwt会自动给我们验证过期,如果过期会抛出对应的异常。

@RestController
@RequestMapping(value = "/jwt")
public class TokenController
{
    @Autowired
    private TokenJPA tokenJPA;

    @Autowired
    private UserInfoJPA userInfoJPA;

    /**
     * 获取token,更新token
     * @param appId 用户编号
     * @param appSecret 用户密码
     * @return
     */
    @RequestMapping(value = "/token", method = {RequestMethod.POST,RequestMethod.GET})
    public TokenResult token
            (
                    @RequestParam String appId,
                    @RequestParam String appSecret
            )
    {
        TokenResult token = new TokenResult();
        //appId is null
        if(appId == null || appId.trim() == "")
        {
            token.setFlag(false);
            token.setMsg("appId is not found!");
        }
        //appSecret is null
        else if(appSecret == null || appSecret.trim() == "")
        {
            token.setFlag(false);
            token.setMsg("appSecret is not found!");
        }
        else
        {
            //根据appId查询用户实体
            UserInfoEntity userDbInfo = userInfoJPA.findOne(new Specification<UserInfoEntity>() {
                @Override
                public Predicate toPredicate(Root<UserInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                    criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), appId));
                    return null;
                }
            });
            //如果不存在
            if (userDbInfo == null)
            {
                token.setFlag(false);
                token.setMsg("appId : " + appId + ", is not found!");
            }
            //验证appSecret是否存在
            else if (!new String(userDbInfo.getAppSecret()).equals(appSecret.replace(" ","+")))
            {
                token.setFlag(false);
                token.setMsg("appSecret is not effective!");
            }
            else
            {
                //检测数据库是否存在该appId的token值
                TokenInfoEntity tokenDBEntity = tokenJPA.findOne(new Specification<TokenInfoEntity>() {
                    @Override
                    public Predicate toPredicate(Root<TokenInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                        criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), appId));
                        return null;
                    }
                });
                //返回token值
                String tokenStr = null;
                //tokenDBEntity == null -> 生成newToken -> 保存数据库 -> 写入内存 -> 返回newToken
                if(tokenDBEntity == null)
                {
                    //生成jwt,Token
                    tokenStr = createNewToken(appId);
                    //将token保持到数据库
                    tokenDBEntity = new TokenInfoEntity();
                    tokenDBEntity.setAppId(userDbInfo.getAppId());
                    tokenDBEntity.setBuildTime(String.valueOf(System.currentTimeMillis()));
                    tokenDBEntity.setToken(tokenStr.getBytes());
                    tokenJPA.save(tokenDBEntity);
                }
                //tokenDBEntity != null -> 验证是否超时 ->
                //不超时 -> 直接返回dbToken
                //超时 -> 生成newToken -> 更新dbToken -> 更新内存Token -> 返回newToken
                else
                {
                    //判断数据库中token是否过期,如果没有过期不需要更新直接返回数据库中的token即可
                    //数据库中生成时间
                    long dbBuildTime = Long.valueOf(tokenDBEntity.getBuildTime());
                    //当前时间
                    long currentTime = System.currentTimeMillis();
                    //如果当前时间 - 数据库中生成时间 < 7200 证明可以正常使用
                    long second = TimeUnit.MILLISECONDS.toSeconds(currentTime - dbBuildTime);
                    if (second > 0 && second < 7200) {
                        tokenStr = new String(tokenDBEntity.getToken());
                    }
                    //超时
                    else{
                        //生成newToken
                        tokenStr = createNewToken(appId);
                        //更新token
                        tokenDBEntity.setToken(tokenStr.getBytes());
                        //更新生成时间
                        tokenDBEntity.setBuildTime(String.valueOf(System.currentTimeMillis()));
                        //执行更新
                        tokenJPA.save(tokenDBEntity);
                    }
                }
                //设置返回token
                token.setToken(tokenStr);
            }
        }
        return token;
    }
    /**
     * 创建新token
     * @param appId
     * @return
     */
    private String createNewToken(String appId){
        //获取当前时间
        Date now = new Date(System.currentTimeMillis());
        //过期时间
        Date expiration = new Date(now.getTime() + 7200000);
        return Jwts
                .builder()
                .setSubject(appId)
                //.claim(YAuthConstants.Y_AUTH_ROLES, userDbInfo.getRoles())
                .setIssuedAt(now)
                .setIssuer("Online YAuth Builder")
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, "HengYuAuthv1.0.0")
                .compact();
    }
}

configuration

@Configuration
public class JWTConfiguration extends WebMvcConfigurerAdapter
{
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new JwtTokenInterceptor()).addPathPatterns("/api/**");
    }
}

interceptor

Claims就是我们生成Token是的对象,我们把传递的头信息token通过JWT可以逆转成Claims对象,并且通过getSubject可以获取到我们用户的appId。

public class JwtTokenInterceptor implements HandlerInterceptor
{
    /**
     * 请求之前
     * @param request 请求对象
     * @param response 返回对象
     * @param o
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object o) throws Exception {

        //自动排除生成token的路径,并且如果是options请求是cors跨域预请求,设置allow对应头信息
        if(request.getRequestURI().equals("/token") || RequestMethod.OPTIONS.toString().equals(request.getMethod()))
        {
            return true;
        }

        //其他请求获取头信息
        final String authHeader = request.getHeader("X-YAuth-Token");
        try {
            //如果没有header信息
            if (authHeader == null || authHeader.trim() == "") {
                throw new SignatureException("not found X-YAuth-Token.");
            }

            //获取jwt实体对象接口实例
            final Claims claims = Jwts.parser().setSigningKey("HengYuAuthv1.0.0")
                    .parseClaimsJws(authHeader).getBody();
            //从数据库中获取token
            TokenInfoEntity token = getDAO(TokenJPA.class,request).findOne(new Specification<TokenInfoEntity>() {
                @Override
                public Predicate toPredicate(Root<TokenInfoEntity> root, CriteriaQuery<?> criteriaQuery, CriteriaBuilder criteriaBuilder) {
                    criteriaQuery.where(criteriaBuilder.equal(root.get("appId"), claims.getSubject()));
                    return null;
                }
            });
            //数据库中的token值
            String tokenval = new String(token.getToken());
            //如果内存中不存在,提示客户端获取token
            if(tokenval == null || tokenval.trim() == "") {
                throw new SignatureException("not found token info, please get token agin.");
            }
            //判断内存中的token是否与客户端传来的一致
            if(!tokenval.equals(authHeader))
            {
                throw new SignatureException("not found token info, please get token agin.");
            }
        }
        //验证异常处理
        catch (SignatureException | ExpiredJwtException e)
        {
            //输出对象
            PrintWriter writer = response.getWriter();

            //输出error消息
            writer.write("need refresh token");
            writer.close();
            return false;
        }
        //出现异常时
        catch (final Exception e)
        {
            //输出对象
            PrintWriter writer = response.getWriter();
            //输出error消息
            writer.write(e.getMessage());
            writer.close();
            return false;
        }
        return true;
    }
    /**
     * 根据传入的类型获取spring管理的对应dao
     * @param clazz 类型
     * @param request 请求对象
     * @param <T>
     * @return
     */
    private <T> T getDAO(Class<T> clazz,HttpServletRequest request)
    {
        BeanFactory factory = WebApplicationContextUtils.getRequiredWebApplicationContext(request.getServletContext());
        return factory.getBean(clazz);
    }

    @Override
    public void postHandle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, ModelAndView modelAndView) throws Exception {

    }

    @Override
    public void afterCompletion(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Object o, Exception e) throws Exception {

    }
}

验证

我们在拦截器中配置的无论是不存在token还是token需要刷新都是返回"need refresh token"错误信息

可以看到我们将之前获取的token作为请求header(X-YAuth-Token)的值进行传递,再次访问127.0.0.1:8080/api/index,就可以成功的获取接口返回的数据。

注意:如果Token过期,再次访问/jwt/token地址传入对应的appId以及appSecret就可以获取一条新的token,也会对应的更新数据库token信息表的内容。

Copyright © wswzms.top 2019 all right reserved,powered by Gitbook该文件修订时间: 2019-04-17 18:26:31

results matching ""

    No results matching ""