摘要
角色路由访问策略匹配器作为中间件用于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, *, *
DISCUSSION
评论
还没有公开评论,欢迎留下想法。