摘要

角色路由访问策略匹配器作为中间件用于golang http路由访问,使得只有符合策略的路由请求才允许被访问。核心在于实现角色、资源路由和访问方式的匹配器。

实现

目录结构

- route
  - method
    - matcher. Go
  - path
    - mathcer.go
  - role
    - mathcer.go
  - router. Go

定义访问方式匹配器

访问方式匹配器支持以下语法:

  • * 匹配任意访问方式;
  • | 例如get|post,匹配get或者post访问;
  • 大小写不敏感;
package method

import (
	"fmt"
	"regexp"
)

type MethodMatcher struct {
	Pattern string
}

func NewMethodMatcher(pattern string) MethodMatcher {
	return MethodMatcher{
		Pattern: pattern,
	}
}

func (matcher MethodMatcher) resolver(pattern string) string {
	pattern = regexp.MustCompile(`\s`).ReplaceAllString(pattern, "")
	pattern = regexp.MustCompile(`(\*)`).ReplaceAllString(pattern, ".*")
	pattern = fmt.Sprintf(`^(?i)(%s)$`, pattern)
	return pattern
}

func (matcher MethodMatcher) Match(method string) bool {
	regStr := matcher.resolver(matcher.Pattern)
	reg, err := regexp.Compile(regStr)
	if err != nil {
		return false
	}
	return reg.MatchString(method)
}

定义路径匹配器

路径匹配器支持以下语法:

  • /*/** 匹配当前路径下的任意路径及任意子路径,例如account/*/**,则可以访问/account路径下全部路径;
  • /* 匹配当前路径下的任意子路径,例如/account/*,则/account/123被允许访问,/account/123/123不被允许访问;
  • * 匹配任意字符,例如/account-*,则/account-123被允许访问,/account-123/不被允许访问;
package path

import (
	"fmt"
	"regexp"
)

type PathMatcher struct {
	Pattern string
}

func NewPathMatcher(pattern string) PathMatcher {
	return PathMatcher{
		Pattern: pattern,
	}
}

func (matcher PathMatcher) resolver(pattern string) string {
	reg := regexp.MustCompile(`(/\*/\*\*)|(/\*)|(\*)`)
	pattern = reg.ReplaceAllStringFunc(pattern, func(s string) string {
		switch s {
		case "/*/**":
			return "/.*"
		case "/*":
			return "/[^/]*"
		case "*":
			return ".*"
		}
		return s
	})
	return fmt.Sprintf("^%s$", pattern)
}

func (matcher PathMatcher) Match(path string) bool {
	regStr := matcher.resolver(matcher.Pattern)
	reg, err := regexp.Compile(regStr)
	if err != nil {
		return false
	}
	return reg.MatchString(path)
}

定义角色规则逻辑解析器

角色规则逻辑解析器支持以下语法:

  • & 且运算,例如user&admin,则只允许具备user且具备admin角色的用户访问;
  • | 或运算,例如user&admin,则允许具备user或者具备admin角色的用户访问;
  • () 括号运算,例如user&(admin|admin1),则必须具备user角色,且具备admin或者admin1角色的用户访问;
package role

import (
	"fmt"
	"strings"
)

type NodeType string

const (
	NodeTypeAnd NodeType = "AND"
	NodeTypeOr  NodeType = "OR"
	NodeTypeVar NodeType = "VAR"
)

type Node struct {
	Type  NodeType
	Value string
	Left  *Node
	Right *Node
}

func parseExpression(expr string) (*Node, error) {
	stack := []*Node{}
	operators := []rune{}

	precedence := map[rune]int{
		'|': 1,
		'&': 2,
		'(': 0,
	}

	popOperator := func() {
		if len(operators) == 0 {
			return
		}
		op := operators[len(operators)-1]
		operators = operators[:len(operators)-1]

		if len(stack) < 2 {
			return
		}
		right := stack[len(stack)-1]
		left := stack[len(stack)-2]
		stack = stack[:len(stack)-2]

		var nodeType NodeType
		if op == '|' {
			nodeType = NodeTypeOr
		} else if op == '&' {
			nodeType = NodeTypeAnd
		}

		stack = append(stack, &Node{
			Type:  nodeType,
			Left:  left,
			Right: right,
		})
	}

	pushOperator := func(op rune) {
		for len(operators) > 0 && precedence[operators[len(operators)-1]] >= precedence[op] {
			popOperator()
		}
		operators = append(operators, op)
	}

	for i := 0; i < len(expr); {
		ch := expr[i]
		if ch == ' ' {
			i++
			continue
		}
		if ch == '(' {
			operators = append(operators, '(')
			i++
		} else if ch == ')' {
			for len(operators) > 0 && operators[len(operators)-1] != '(' {
				popOperator()
			}
			if len(operators) > 0 && operators[len(operators)-1] == '(' {
				operators = operators[:len(operators)-1]
			}
			i++
		} else if ch == '|' || ch == '&' {
			pushOperator(rune(ch))
			i++
		} else {
			j := i
			for j < len(expr) && !strings.ContainsRune("()|&", rune(expr[j])) {
				j++
			}
			stack = append(stack, &Node{
				Type:  NodeTypeVar,
				Value: expr[i:j],
			})
			i = j
		}
	}

	for len(operators) > 0 {
		popOperator()
	}

	if len(stack) != 1 {
		return nil, fmt.Errorf("invalid expression")
	}

	return stack[0], nil
}

定义角色匹配器

角色匹配器支持以下语法:

  • * 匹配任意角色;
package role

import (
	"fmt"
	"regexp"
)

type RoleMatcher struct {
	Pattern string
}

func NewRoleMatcher(pattern string) RoleMatcher {
	return RoleMatcher{Pattern: pattern}
}

func (matcher RoleMatcher) resolver(pattern string) string {
	pattern = regexp.MustCompile(`\s`).ReplaceAllString(pattern, "")
	return regexp.MustCompile(`(\*)`).ReplaceAllString(pattern, ".*")
}

func (matcher RoleMatcher) match(roles []string, role string) bool {
	for _, s := range roles {
		compile, err := regexp.Compile(fmt.Sprintf("^%s$", role))
		if err != nil {
			return false
		}
		if compile.MatchString(s) {
			return true
		}
	}
	return false
}

func (matcher RoleMatcher) exec(node *Node, roles []string) bool {
	if node == nil {
		return true
	}
	if node.Type == NodeTypeVar {
		return matcher.match(roles, node.Value)
	}
	if node.Type == NodeTypeOr {
		return matcher.exec(node.Left, roles) || matcher.exec(node.Right, roles)
	}
	if node.Type == NodeTypeAnd {
		return matcher.exec(node.Left, roles) && matcher.exec(node.Right, roles)
	}
	return false
}

func (matcher RoleMatcher) Match(roles []string) bool {
	pattern := matcher.resolver(matcher.Pattern)
	node, err := parseExpression(pattern)
	if err != nil {
		return false
	}
	r := matcher.exec(node, roles)
	return r
}

实现策略匹配器

策略匹配器从路由策略文件中读取策略,并在中间件中通过Match函数判断是否具有访问规则。

package routeAccess

import (
	"routeAccess/method"
	"routeAccess/path"
	"routeAccess/role"
	"bytes"
	"encoding/csv"
	"io"
	"log"
	"strings"
)

type Policy struct {
	RoleMatcher   role.RoleMatcher     `json:"role_matcher,omitempty"`
	MethodMatcher method.MethodMatcher `json:"method_matcher,omitempty"`
	PathMatcher   path.PathMatcher     `json:"path_matcher,omitempty"`
}

type Policies []Policy

func NewPolicies(csvBytes []byte) Policies {
	policies := make(Policies, 0)

	reader := csv.NewReader(bytes.NewReader(csvBytes))
	reader.Comment = '#'
	for {
		row, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			}
			log.Println(err)
			continue
		}
		for i := 0; i < len(row); i++ {
			row[i] = strings.Trim(row[i], " \t\r\n")
		}

		p := Policy{}
		p.RoleMatcher = role.NewRoleMatcher(row[0])
		p.MethodMatcher = method.NewMethodMatcher(row[1])
		p.PathMatcher = path.NewPathMatcher(row[2])

		policies = append(policies, p)
	}
	return policies
}

func (policies Policies) Match(roles []string, method, path string) bool {
	for _, row := range policies {
		if row.RoleMatcher.Match(roles) &&
			row.MethodMatcher.Match(method) &&
			row.PathMatcher.Match(path) {
			return true
		}
	}
	return false
}

规则样例

# role, method, path
*, post, /account/login
*, post, /terminal/assert
*, get, /
*, get, /index.html
*, get, /favicon.ico
*, get, /js/*
*, get, /css/*
*, get, /file/*
*, get, /system/config
*, *, /network
user|admin, *, *