当前位置:Gxlcms > 数据库问题 > Mysql读写分离+防止sql注入攻击

Mysql读写分离+防止sql注入攻击

时间:2021-07-01 10:21:17 帮助过:28人阅读

三、 几种方案对比(why)

1、主从都使用读写账户,sql语句采用字符串拼接方式(废弃)

既不能实现主从读写分离,也不能防止sql注入攻击。

2、主从都使用读写账户,sql采用prepare+execute的方式(存在风险)

可以防止sql注入攻击,但不能实现主从读写分离,当并发量上来后主库压力会很大,存在风险。

3、主采用读写账户,从采用只读账户,sql采用prepare+execute的方式(会报错)

(1)首先抛出报错:

技术图片

(2)报错原因分析:

技术图片图1-2 报错原因分析

错误原因:如果prepare的时候sql发给了从库1,但是execute的时候因为从库1延时较大,sql命令发给了从库2,就会报上面这个错误(除此之外,默认proxy会隔断时间切换备机,保证备机都能用到)。

4、主采用读写账户,从采用只读账户,建立mysql链接时使用参数interpolateParams=true

针对方案4,我们需要来看下底层源码:

这里我们需要关注两部分源码:database/sql和go-sql-driver/mysql,其中database/sql是golang针对数据库抽象出来的一个标准库,go-sql-driver/mysql是实现database/sql驱动接口的mysql驱动。

(1)我们一般写查询语句是这样的(举个栗子)

rows, err := db_slave.Query("SELECT * FROM tbl_user WHERE user_id = ?", 1)
if err != nil { 
    panic(err.Error()) //只是举例,真实使用中需要对错误进行处理和返回
}

我们知道prepared statement可以防止sql注入攻击,上图这样的写法看上去是使用的prepared statement方式,但到底是不是呢?我们继续往底层走。

(2)database/sql中查询接口是下面两个方法(即Query和QueryRow)

// Query executes a query that returns rows, typically a SELECT.
// The args are for any placeholder parameters in the query.
func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
	return db.QueryContext(context.Background(), query, args...)
}

注:Query执行查询并返回多个数据行,这个查询通常是一个select,方法中args参数用于填写查询语句中包含的占位符的实际参数。

// QueryRow executes a query that is expected to return at most one row.
// QueryRow always returns a non-nil value. Errors are deferred until
// Row‘s Scan method is called.
// If the query selects no rows, the *Row‘s Scan will return ErrNoRows.
// Otherwise, the *Row‘s Scan scans the first selected row and discards
// the rest.
func (db *DB) QueryRow(query string, args ...interface{}) *Row {
	return db.QueryRowContext(context.Background(), query, args...)
}

注:QueryRow与Query方法不同点是,执行一条查询最多只会返回一个数据行。

(3)发现宝藏:Query底层执行查询的策略

从queryDC方法来看Query默认是不使用prepared statement方式的,只有在查询时发生driver.ErrSkip错误才会启用prepared statement继续查询。

注:从Query到queryDC方法经过的连接获取和错误处理等逻辑不影响我们分析问题,可以忽略。

// queryDC executes a query on the given connection.
// The connection gets released by the releaseConn function.
// The ctx context is from a query method and the txctx context is from an
// optional transaction context.
func (db *DB) queryDC(ctx, txctx context.Context, dc *driverConn, releaseConn func(error), query string, args []interface{}) (*Rows, error) {
	queryerCtx, ok := dc.ci.(driver.QueryerContext)
	var queryer driver.Queryer
	if !ok {
		queryer, ok = dc.ci.(driver.Queryer)
	}
	if ok {
		var nvdargs []driver.NamedValue
		var rowsi driver.Rows
		var err error
		withLock(dc, func() {
			nvdargs, err = driverArgsConnLocked(dc.ci, nil, args)
			if err != nil {
				return
			}
                        //核心查询 不使用‘prepared Statement‘来执行
			rowsi, err = ctxDriverQuery(ctx, queryerCtx, queryer, query, nvdargs)
		})
		if err != driver.ErrSkip {//发生错误driver.ErrSkip才使用‘prepared Statement‘方式去查询
			if err != nil {//其他错误,关闭链接并报错返回
				releaseConn(err)
				return nil, err
			}
			// Note: ownership of dc passes to the *Rows, to be freed
			// with releaseConn.
			rows := &Rows{
				dc:          dc,
				releaseConn: releaseConn,
				rowsi:       rowsi,
			}
			rows.initContextClose(ctx, txctx)
			return rows, nil
		}
	}
        //发生错误driver.ErrSkip时,才会使用‘Prepared Statement‘方式再次执行查询
	var si driver.Stmt
	var err error
	withLock(dc, func() {
		si, err = ctxDriverPrepare(ctx, dc.ci, query)//prepare
	})
	if err != nil {
		releaseConn(err)
		return nil, err
	}

	ds := &driverStmt{Locker: dc, si: si}
	rowsi, err := rowsiFromStatement(ctx, dc.ci, ds, args...)//Statement
	if err != nil {
		ds.Close()
		releaseConn(err)
		return nil, err
	}

	// Note: ownership of ci passes to the *Rows, to be freed
	// with releaseConn.
	rows := &Rows{
		dc:          dc,
		releaseConn: releaseConn,
		rowsi:       rowsi,
		closeStmt:   ds,
	}
	rows.initContextClose(ctx, txctx)
	return rows, nil
}

(4)什么时候才会报driver.ErrSkip错误呢

答案就在go-sql-driver/mysql下的connection.go中真实Query方法中,请看大屏幕:

//mysql query底层实现
func (mc *mysqlConn) query(query string, args []driver.Value) (*textRows, error) {
	if mc.closed.IsSet() {
		errLog.Print(ErrInvalidConn)
		return nil, driver.ErrBadConn
	}
	if len(args) != 0 {
		if !mc.cfg.InterpolateParams {//能不能不使用Prepared Statement,就看interpolateParams是不是true了
			return nil, driver.ErrSkip
		}
		// try client-side prepare to reduce roundtrip
		prepared, err := mc.interpolateParams(query, args)//在mysql客户端侧使用插值法实现防止sql注入攻击
		if err != nil {
			return nil, err
		}
		query = prepared
	}
	// Send command
	err := mc.writeCommandPacketStr(comQuery, query)
	if err == nil {
		// Read Result
		var resLen int
		resLen, err = mc.readResultSetHeaderPacket()
		if err == nil {
			rows := new(textRows)
			rows.mc = mc

			if resLen == 0 {
				rows.rs.done = true

				switch err := rows.NextResultSet(); err {
				case nil, io.EOF:
					return rows, nil
				default:
					return nil, err
				}
			}

			// Columns
			rows.rs.columns, err = mc.readColumns(resLen)
			return rows, err
		}
	}
	return nil, mc.markBadConn(err)
}

(5) 驱动程序是如何使用插值法防止sql注入攻击的呢?

我们到interpolateParams方法中一探究竟(我们只需要关注参数为string的情况):

func (mc *mysqlConn) interpolateParams(query string, args []driver.Value) (string, error) {
	// Number of ? should be same to len(args)
	if strings.Count(query, "?") != len(args) {
		return "", driver.ErrSkip
	}

	buf, err := mc.buf.takeCompleteBuffer()
	if err != nil {
		// can not take the buffer. Something must be wrong with the connection
		errLog.Print(err)
		return "", ErrInvalidConn
	}
	buf = buf[:0]
	argPos := 0

	for i := 0; i < len(query); i++ {
		q := strings.IndexByte(query[i:], ‘?‘)
		if q == -1 {
			buf = append(buf, query[i:]...)
			break
		}
		buf = append(buf, query[i:i+q]...)
		i += q

		arg := args[argPos]
		argPos++

		if arg == nil {
			buf = append(buf, "NULL"...)
			continue
		}

		switch v := arg.(type) {
		case int64:
			buf = strconv.AppendInt(buf, v, 10)
		case uint64:
			// Handle uint64 explicitly because our custom ConvertValue emits unsigned values
			buf = strconv.AppendUint(buf, v, 10)
		case float64:
			buf = strconv.AppendFloat(buf, v, ‘g‘, -1, 64)
		case bool:
			if v {
				buf = append(buf, ‘1‘)
			} else {
				buf = append(buf, ‘0‘)
			}
		case time.Time:
			if v.IsZero() {
				buf = append(buf, "‘0000-00-00‘"...)
			} else {
				v := v.In(mc.cfg.Loc)
				v = v.Add(time.Nanosecond * 500) // To round under microsecond
				year := v.Year()
				year100 := year / 100
				year1 := year % 100
				month := v.Month()
				day := v.Day()
				hour := v.Hour()
				minute := v.Minute()
				second := v.Second()
				micro := v.Nanosecond() / 1000

				buf = append(buf, []byte{
					‘\‘‘,
					digits10[year100], digits01[year100],
					digits10[year1], digits01[year1],
					‘-‘,
					digits10[month], digits01[month],
					‘-‘,
					digits10[day], digits01[day],
					‘ ‘,
					digits10[hour], digits01[hour],
					‘:‘,
					digits10[minute], digits01[minute],
					‘:‘,
					digits10[second], digits01[second],
				}...)

				if micro != 0 {
					micro10000 := micro / 10000
					micro100 := micro / 100 % 100
					micro1 := micro % 100
					buf = append(buf, []byte{
						‘.‘,
						digits10[micro10000], digits01[micro10000],
						digits10[micro100], digits01[micro100],
						digits10[micro1], digits01[micro1],
					}...)
				}
				buf = append(buf, ‘\‘‘)
			}
		case json.RawMessage:
			buf = append(buf, ‘\‘‘)
			if mc.status&statusNoBackslashEscapes == 0 {
				buf = escapeBytesBackslash(buf, v)
			} else {
				buf = escapeBytesQuotes(buf, v)
			}
			buf = append(buf, ‘\‘‘)
		case []byte:
			if v == nil {
				buf = append(buf, "NULL"...)
			} else {
				buf = append(buf, "_binary‘"...)
				if mc.status&statusNoBackslashEscapes == 0 {
					buf = escapeBytesBackslash(buf, v)
				} else {
					buf = escapeBytesQuotes(buf, v)
				}
				buf = append(buf, ‘\‘‘)
			}
		case string:
                        //我们只需要关注参数为string的情况
buf = append(buf, ‘\‘‘)
			if mc.status&statusNoBackslashEscapes == 0 {
				buf = escapeStringBackslash(buf, v)
			} else {
				buf = escapeStringQuotes(buf, v)
			}
			buf = append(buf, ‘\‘‘)
		default:
			return "", driver.ErrSkip
		}

		if len(buf)+4 > mc.maxAllowedPacket {
			return "", driver.ErrSkip
		}
	}
	if argPos != len(args) {
		return "", driver.ErrSkip
	}
	return string(buf), nil
}

源码中escapeStringBackslash方法的字面意思是对字符串参数转义,我们知道转义特殊字符就是防止sql注入攻击的有效方法之一,本着刨根问底的科学探索精神让我们再进到该方法中一探究竟~

//非常清晰,这里就是在做字符串中特殊字符的转义编码
// escapeStringBackslash is similar to escapeBytesBackslash but for string.
func escapeStringBackslash(buf []byte, v string) []byte {
	pos := len(buf)
	buf = reserveBuffer(buf, len(v)*2)

	for i := 0; i < len(v); i++ {
		c := v[i]
		switch c {
		case ‘\x00‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘0‘
			pos += 2
		case ‘\n‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘n‘
			pos += 2
		case ‘\r‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘r‘
			pos += 2
		case ‘\x1a‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘Z‘
			pos += 2
		case ‘\‘‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘\‘‘
			pos += 2
		case ‘"‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘"‘
			pos += 2
		case ‘\\‘:
			buf[pos] = ‘\\‘
			buf[pos+1] = ‘\\‘
			pos += 2
		default:
			buf[pos] = c
			pos++
		}
	}

	return buf[:pos]
}

这里就是在做字符串中特殊字符的转义编码,因此使用参数interpolateParams=true可以防止sql注入攻击,并且因为没有使用prepared statement方式,当采用主从读写分离方式时,也不会再报出方案3中的错误,既实现了主从读写分离也可以防止sql注入攻击,是最佳解决方案。

(6)interpolateParams=true使用注意事项

技术图片

可以看到官方文档最后给出一个补充说明,interpolateParams=true不可以与以下多字节编码共同使用(multibyte encodings BIG5, CP932, GB2312, GBK or SJIS),因为他们会引起sql注入脆弱性。

总结就是interpolateParams=true会在client端绑定参数并防止sql注入,减少prepared+exec方式的多次网络往返开销和可能的读写分离错误.

Mysql读写分离+防止sql注入攻击

标签:rri   延时   因此   sam   led   mysql   官方文档   column   程序   

人气教程排行