微服务迁移记(六):集成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接口保护完成,客户端测试通过。

 

posted on 2020-03-05 16:23  zhouyu  阅读(607)  评论(0编辑  收藏  举报

导航