zoukankan      html  css  js  c++  java
  • 微服务迁移记(六):集成jwt保护微服务接口安全

    JWT=json web token,是一种跨语言的身份校验机制,通信双方之间以Json对象的形式安全的传递信息,对数据进行加密传输,保证信息传递过程中的身份可信。

    微服务各模块或不同应用程序、终端之间的RPC调用,也应该保障数据传递的安全和可靠,避免身份伪造、传递数据被拦截获取和篡改等信息安全。

    我们对前面的微服务API实现层进行如下改造:

    第一步:调用接口前,先进行接口用户登录,获取令牌(TOKEN)。

    这个用户登录和后台的用户登录不同,是我们分配给每个需要调用API的终端的用户信息。所以我们要再创建一张表,用来保存API权限,为了便于后期扩展,我们还可以增加一些字段如调用阈值,可以再加一个子表,保存这个用户具备哪些接口的调用权限以及阈值等。(说明:下面的代码示例并未取数据库,只是实现业务逻辑

    1. JwtUtil类,主要用来做token生成和校验。客户端应该与服务端超时时间保持一致或小于服务端超时时间。

    /**
     * @program: zyproject
     * @description: jwt公共类
     * @author: zhouyu(zhouyu629 # qq.com)
     * @create: 2020-03-04
     **/
    public class JwtUtil {
        static final long EXPTIME = 3600_000_000L; //超时时间
        static final String SECRET = "Abc@1234"; //示例代码,默认密钥
    
        /**
         * 生成token
         * @param user_name
         * @return
         */
        public static String generatorToken(String user_name){
            HashMap<String,Object> map = new HashMap<>();
            map.put("user_name",user_name);
            String jwt = Jwts.builder()
                    .setClaims(map)
                    .setExpiration(new Date(System.currentTimeMillis()+EXPTIME))
                    .signWith(SignatureAlgorithm.HS512,SECRET)
                    .compact();
            return "Bearer  "+ jwt;
        }
    
        /**
         * token校验
         * @param token
         */
        public static void validateToken(String token){
            try{
                Map<String,Object> body = Jwts.parser()
                        .setSigningKey(SECRET)
                        .parseClaimsJws(token.replace("Bearer ",""))
                        .getBody();
            }catch (Exception e){
                throw new IllegalStateException("Invalid Token."+e.getMessage());
            }
    
        }
    }

    2. 过滤器:JwtAuthenticationFilter

    该过滤器的主要作用是对受保护的接口进行签名校验,如果客户端没有携带token或token不正确,则返回统一报错信息。

    isProtectedUrl:验证当前请求是否需要保护
    isExceedUrl:例外URL,如登录、统一报错接口,不需要进行token认证

    BusinessException:这个是自定义的异常信息,后面有单独章节说明如何做控制器、过滤器的统一出错处理。
    /**
     * @program: zyproject
     * @description: 接口认证过滤器
     * @author: zhouyu(zhouyu629 # qq.com)
     * @create: 2020-03-04
     **/
    public class JwtAuthenticationFilter extends OncePerRequestFilter {
        private final static AntPathMatcher pathMatcher = new AntPathMatcher();
        @Override
        protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
            try {
                if(isProtectedUrl(httpServletRequest) && !isExceedUrl(httpServletRequest)){
                    String token = httpServletRequest.getHeader("Authorization");
                    JwtUtil.validateToken(token);
                }
            }catch (Exception e){
                //httpServletResponse.sendError(CodeEnum.UNAUTHORIZED.getCode(),e.getMessage());
                throw new BusinessException(CodeEnum.UNAUTHORIZED);
            }
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }
    
        private boolean isProtectedUrl(HttpServletRequest request){
            return pathMatcher.match("/api/**",request.getServletPath());
        }
        private boolean isExceedUrl(HttpServletRequest request){
            return pathMatcher.match("/init/**",request.getServletPath());
        }

    3. 我们在ApiInitController里,做用户登录和统一报错返回。

    /**
     * @program: zyproject
     * @description: API初始化相关
     * @author: zhouyu(zhouyu629 # qq.com)
     * @create: 2020-03-04
     **/
    @RestController
    @RequestMapping("/init")
    public class ApiInitService implements IApiInitService {
    
        //控制器统一出错信息
        @RequestMapping("/api-error")
        public ResponseData apierror(HttpServletRequest request){
            Exception e = (Exception)request.getAttribute("filter.error");
            try{
                //应该对Exception做更细致的划分
                BusinessException be = (BusinessException)e;
                return ResponseData.out(be.getCodeEnum(),null);
            }catch (Exception ex) {
                return ResponseData.out(CodeEnum.FAIL,e.getMessage());
            }
        }
    
        //用户登录,测试阶段写死admin登录
        @GetMapping("/login")
        @Override
        public ResponseData login(String user_name) {
            if("admin".equals(user_name)){
                String jwt = JwtUtil.generatorToken(user_name);
                return ResponseData.out(CodeEnum.SUCCESS,jwt);
            }else {
                return ResponseData.out(CodeEnum.AUTHORIZEDUSERNAMEORPASSWORDINVALID, null);
            }
        }
    }

    通过以上操作,如果直接访问接口,则会提示未登录,如:

    用postman进行登录后,再调用api测试

    拿分配的token,再去访问api相关接口,返回成功:

    第二步:访问具体API,Header中需要携带JWT信息,对令牌(TOKEN)的合法性进行校验

     接下来,改造WEB层访问,首次请求,拿到token缓存起来,以后每次调用,缓存不过期,就直接使用,过期后就重新拿。

    1. 增加一个拦截器,为所有的FeiClient添加头信息,携带token。

    /**
     * @program: zyproject
     * @description: Feign拦截器,用于在所有请求上加上header,适用于jwt token认证
     * @author: zhouyu(zhouyu629 # qq.com)
     * @create: 2020-03-04
     **/
    public class FeignInterceptor implements RequestInterceptor {
        private  final String key = "Authorization";
        private Logger logger = LoggerFactory.getLogger(this.getClass());
        @Autowired
        private ApiInitService apiInitService;
        @Override
        public void apply(RequestTemplate requestTemplate) {
            HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
            logger.info("当前路径:"+ request.getServletPath());
            if(request.getServletPath().indexOf("init") < 0) {
                //后期把token放到redies里保存起来
                if (!requestTemplate.headers().containsKey(key)) {
                    //拿token
                    String token = this.apiInitService.getToken();
                    requestTemplate.header(key, token);
                }
            }
        }
    }

    2. FeignClient接口改造

    为了不让spring创建相同context的bean,为FeiClient注解增加ContextId

    /**
     * @program: zyproject
     * @description: 系统登录方法
     * @author: zhouyu(zhouyu629 # qq.com)
     * @create: 2020-03-04
     **/
    @FeignClient(name = "zyproject-api-service-system",contextId = "apiinit",configuration = {})
    public interface ApiInitFeign extends IApiInitService {
    }

    需要保护的接口,使用拦截器

    /**
     * @program: zyproject
     * @description: RPC调用系统管理相关接口服务
     * @author: zhouyu(zhouyu629 @ qq.com)
     * @create: 2020-02-11
     **/
    @FeignClient(name ="zyproject-api-service-system",contextId = "system",configuration = {FeignInterceptor.class})
    public interface SystemFeign extends ISystemService {
    
    }

    到此,API接口保护完成,客户端测试通过。

  • 相关阅读:
    shell学习(11)- seq
    bash快捷键光标移动到行首行尾等
    shell学习(10)- if的使用
    Python 执行 Shell 命令
    查看 jar 包加载顺序
    Linux 中的 sudoers
    Ubuntu 开机启动程序
    指定 Docker 和 K8S 的命令以及用户
    Spark on K8S(Standalone)
    Spark on K8S (Kubernetes Native)
  • 原文地址:https://www.cnblogs.com/zhouyu629/p/12407883.html
Copyright © 2011-2022 走看看