• SpringBoot集成JWT


        JWT(json web tokens)是目前比较流行的跨域认证解决方案;说通俗点就是比较流行的token生成和校验的方案。碰巧公司有个app的项目的token采用了jwt方案,因此记录下后端项目集成jwt的过程,方便后续查阅。

    一、jwt的简单介绍

        jwt生成的token是一种无状态的token,服务端不需要对该token进行保存;它一般由客户端保存。客户端访问请求服务时,服务端对token进行校验,然后进行各种控制。下面直接拿一个生成好的token来讲解
            
        通过上图我们可以发现jwt生成的token是非常长的字符串,并且字符串中有2个小点("."),通过这2个小点我们可以把这token分成3部分。
        • header:头部,是用来描述这个token是什么类型,采用了何种加密算法;token中header是经过base64编码的
        • payload:荷载,用来存放需要传递的数据。官方提供的几个标准字段,同时也可以自己往里面加自定义的字段和内容,用来存放一些不敏感的用户信息。可以简单的把它想像成一个Map集合;token中payload也是经过base64编码的
        • signature:签名,主要是将header和payload的base64编码后内容用点拼接在一起然后进行加密生成签名。服务端需要利用这签名来校验token是否被篡改(验签)
        所以通俗的来讲,token = base64(header) + "." + base64(payload) + "." + 签名
        网上很多博文对jwt的介绍都比较详细,因此本文就不再详细的介绍jwt相关细节,重点放在java代码该怎么写。jwt相关详细介绍可以参考如下链接:
        下面直接上代码

    二、Maven依赖版本说明

        pom中部分重要jar包依赖版本如下:
        <!-- SpringBoot 版本 -->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.1.4.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
            <!--jwt 依赖-->
            <dependency>
                <groupId>io.jsonwebtoken</groupId>
                <artifactId>jjwt</artifactId>
                <version>0.9.1</version>
            </dependency>

    三、token生成和解析工具类及token认证拦截器的编写

    (1)token生成和解析工具类编写

        该工具类需要具有如下功能
        • 生成jwt标准的token;生成token时支持把不敏感的用户信息放在token里面,后续解析token后可以直接使用这些用户信息
        • 解析token,校验token是否过期和篡改
            直接看下面代码
    package com.psx.gqxy.web.jwt;
    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.SignatureAlgorithm;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.lang3.exception.ExceptionUtils;
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.Objects;
    
    /**
     * JwtUtils
     * @author ZENG.XIAO.YAN
     * @blog https://www.cnblogs.com/zeng1994/
     * @version 1.0
     */
    @Slf4j
    public final class JwtUtils {
    
        /** 存放token的请求头对应的key的名字 */
        private static String headerKey = "token";
        /** 加密的secret */
        private static String secret = "zxyTestSecret";
        /** 过期时间,单位为秒 */
        private static long expire = 1800L;
    
        static {
            // TODO 上面变量的值应该从配置文件中读取,方便测试这里就不从配置文件中读取
            // 利用配置文件中的值覆盖静态变量初始化的值
        }
    
    
        /**
         * 生成jwt token
         */
        public static String generateToken(Map<String, Object> userInfoMap) {
            if (Objects.isNull(userInfoMap)) {
                userInfoMap = new HashMap<>();
            }
            //  过期时间
            Date expireDate = new Date(System.currentTimeMillis() + expire * 1000);
            return Jwts.builder()
                    .setHeaderParam("typ", "JWT")   // 设置头部信息
                    .setClaims(userInfoMap)               // 装入自定义的用户信息
                    .setExpiration(expireDate)            // token过期时间
                    .signWith(SignatureAlgorithm.HS512, secret)
                    .compact();
        }
    
        /**
         * 校验token并解析token
         * @param token
         * @return Claims:它继承了Map,而且里面存放了生成token时放入的用户信息
         */
        public static Claims verifyAndGetClaimsByToken(String token) {
            try {
                /* 如果过期或者是被篡改,则会抛异常.
                    注意点:只有在生成token设置了过期时间(setExpiration(expireDate))才会校验是否过期,
                    可以参考源码io.jsonwebtoken.impl.DefaultJwtParser的299行。
                    拓展:利用不设置过期时间就不校验token是否过期的这一特性,我们不设置Expiration;
                          而采用自定义的字段来存放过期时间放在Claims(可以简单的理解为map)中;
                          通过token获取到Claims后自己写代码校验是否过期。
                          通过这思路,可以去实现对过期token的手动刷新
                */
                return Jwts.parser()
                        .setSigningKey(secret)
                        .parseClaimsJws(token)
                        .getBody();
            }catch (Exception e){
                log.debug("verify token error:[{}] ", ExceptionUtils.getStackTrace(e));
                return null;
            }
        }
    
        public static String getHeaderKey() {
            return headerKey;
        }
    
    
    }
    
    

    (2)token身份认证拦截器的编写

        拦截器主要作用如下:
        • 1)拦截器拦截到请求后,拿请求头中的token,如果不存在只直接response输出token不能为空
        • 2)拿到token后,进行token的解析,校验是否篡改或者过期。如果被篡改或者过期只直接response输出token已失效
        • 3)如果校验都通过了,则把token中解析出的用户信息放在request请求域中,方便后续Controller方法取用户信息
            直接看参考下面代码
    package com.psx.gqxy.web.jwt;
    import com.alibaba.fastjson.JSON;
    import com.psx.gqxy.common.base.CommonConstant;
    import com.psx.gqxy.common.base.ModelResult;
    import io.jsonwebtoken.Claims;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import java.io.IOException;
    
    /**
     * jwtToken校验拦截器
     * @author ZENG.XIAO.YAN
     * @blog https://www.cnblogs.com/zeng1994/
     * @version 1.0
     */
    public class JwtInterceptor extends HandlerInterceptorAdapter {
    
        public static final String USER_INFO_KEY = "user_info_key";
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            //  获取用户 token
            String token = request.getHeader(JwtUtils.getHeaderKey());
            if (StringUtils.isBlank(token)) {
                token = request.getParameter(JwtUtils.getHeaderKey());
            }
            //  token为空
            if(StringUtils.isBlank(token)) {
                this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
                        JwtUtils.getHeaderKey() + " can not be blank",
                        response);
                return false;
            }
            //  校验并解析token,如果token过期或者篡改,则会返回null
            Claims claims = JwtUtils.verifyAndGetClaimsByToken(token);
            if(null == claims){
                this.writerErrorMsg(CommonConstant.UNAUTHORIZED,
                        JwtUtils.getHeaderKey() + "失效,请重新登录",
                        response);
                return false;
            }
            //  校验通过后,设置用户信息到request里,在Controller中从Request域中获取用户信息
            request.setAttribute(USER_INFO_KEY, claims);
            return true;
        }
    
        /**
         * 利用response直接输出错误信息
         * @param code
         * @param msg
         * @param response
         * @throws IOException
         */
        private void writerErrorMsg (String code, String msg, HttpServletResponse response) throws IOException {
            ModelResult<Void> result = new ModelResult<>();
            result.setCode(code);
            result.setMsg(msg);
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(JSON.toJSONString(result));
        }
    
    }
    

    四、拦截器的配置和功能测试

    (1)编写一个Controller

        写一个Controller,里面包含一登录方法和一个test方法
        • 登录方法用来实现登录,登录成功后返回token
        • test方法,主要通过拦截器拦截该方法的请求,当用户带有效的token访问时才允许访问该方法
            代码如下:
    package com.psx.gqxy.web.controller;
    import com.psx.gqxy.common.base.CommonConstant;
    import com.psx.gqxy.common.base.ModelResult;
    import com.psx.gqxy.domain.dto.UserLoginDTO;
    import com.psx.gqxy.web.jwt.JwtInterceptor;
    import com.psx.gqxy.web.jwt.JwtUtils;
    import io.jsonwebtoken.Claims;
    import org.springframework.web.bind.annotation.*;
    import javax.servlet.http.HttpServletRequest;
    import java.util.HashMap;
    import java.util.Map;
    
    /**
     * TestJwtController
     * @author ZENG.XIAO.YAN
     * @blog https://www.cnblogs.com/zeng1994/
     * @Date 2019-07-14
     * @version 1.0
     */
    
    @RestController
    @RequestMapping("jwt")
    public class TestJwtController {
    
        @PostMapping("login")
        public ModelResult<String> login(@RequestBody UserLoginDTO dto) {
            ModelResult<String> result = new ModelResult<>();
            // 这里登录就简单的模拟下
            if ("root".equals(dto.getUserName()) && "123456".equals(dto.getPassword())) {
                Map<String, Object> userInfoMap = new HashMap<>();
                userInfoMap.put("userName", "隔壁老王");
                String token = JwtUtils.generateToken(userInfoMap);
                result.setData(token);
            } else {
                result.setCode(CommonConstant.FAIL);
                result.setMsg("用户名或密码错误");
            }
            return result;
        }
    
        @GetMapping("test")
        public String test(HttpServletRequest request) {
            // 登录成功后,从request中获取用户信息
            Claims claims = (Claims) request.getAttribute(JwtInterceptor.USER_INFO_KEY);
            if (null != claims) {
                return (String) claims.get("userName");
            } else {
                return "fail";
            }
        }
    
    }
    

    (2)拦截器的配置

        拦截器拦截需要身份认证的请求,同时放行登录接口
        代码如下:
    /**
     * web相关的定制化配置
     * @author ZENG.XIAO.YAN
     * @version 1.0
     */
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
        // WebMvcConfigurerAdapter 这个类在SpringBoot2.0已过时,官方推荐直接实现WebMvcConfigurer 这个接口
    
        @Bean
        public JwtInterceptor jwtInterceptor() {
            return new JwtInterceptor();
        }
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            InterceptorRegistration jwtInterceptorRegistration = registry.addInterceptor(jwtInterceptor());
            // 配置拦截器的拦截规则和放行规则
            jwtInterceptorRegistration.addPathPatterns("/jwt/**")
                    .excludePathPatterns("/jwt/login");
        }
    }    

    (3)相关测试

      • 不带token访问 /jwt/test接口,被拦截器拦截;返回token不能为空; 效果如下图
                    

      • 访问登录接口,进行登录;登录成功,同时返回生成的token;效果如下图
                    

      • 带上登录成功返回的token访问/jwt/test接口,拦截器放行了请求,成功请求到了test方法;效果如下图
                    

      • 当token被篡改或者已过期时,访问/jwt/test接口,拦截器拦截了该请求,返回token已失效;效果如下图
                    

        进行完上述测试后,说明jwt的集成已经大功告成了。

    五、小结

        jwt集成不麻烦,但是也有很多不完善的地方,后续再想办法把它完善。
        在我的JwtUtils的verifyAndGetClaimsByToken方法里提到了相关扩展的思路,可以通过该思路来实现token的刷新及其他的骚操作。
        当然,jwt也有很多缺点,这里就不在赘述了。


  • 相关阅读:
    审核系统
    ehcache 缓存
    tomcat 内存设置
    html5 开发 跨平台 桌面应用
    service thread 结合使用
    html5桌面应用
    鼠标 事件
    服务器 判断 客户端 文件下载
    使用github管理Eclipse分布式项目开发
    uub代码
  • 原文地址:https://www.cnblogs.com/zeng1994/p/14167f9752483f8461b7a2fef405db69.html
Copyright © 2020-2023  润新知