随便写点技术性的文章
从单体应用到微服务架构,优势很多,但是并不是代表着就没有一点缺点了。
微服务架构,意味着每个服务都是松散耦合的。因此,作为软件工程师和架构师,我们在分布式架构中面临着安全挑战。微服务对外开放的端点,我们称之为:API。
因此,根据上述的安全挑战,我们可以得出一个结论:微服务与单体应用有着不同的安全处理方式。
我们在谈论应用程序安全的时候,总是会提到:认证 (Authentication)和 鉴权 (Authorization)这两个术语。但是,总有人很容易混淆概念。
在 身份验证(Authentication) 的过程当中,我们需要检查用户的身份以提供对系统的访问。在这个过程当中验证的是 “你是谁?” 。故而,用户需要提供登陆所需的详细信息以供身份的验证。
授权(Authorization) ,是通过了身份验证之后的用户,系统是否授权给他访问特定信息(读)或者执行特定的操作(写)的过程。此过程确定了 用户拥有哪些权限。
我可以想到的解决方案有以下这么几种:
我比较推崇 2.3 这种策略,为什么呢?
当一个设备(客户端)向一个设备(服务端)发送请求的时候,服务端如何判断这个客户端是谁?传统意义上的认证方式又两种:有状态认证、无状态认证。有状态认证和无状态认证最大的区别就是服务器会不会保存客户端的信息。
有状态认证,以cookie-session模型为例,当客户端第一次请求服务端的时候,服务端会返回客户端一个唯一的标识(默认在cookie中),并保存对应的客户端信息,客户端接受到唯一标识之后,将标识保存到本地cookie中,以后的每次请求都携带此cookie,服务器根据此cookie标识就可以判断请求的用户是谁,然后查到对应用户的信息。
无状态的认证,客户端在提交身份信息,服务端验证身份后,根据一定的算法生成一个token令牌返回给客户端,之后每次请求服务端,客户端都需要携带此令牌,服务器接受到令牌之后进行校验,校验通过后,提取令牌的信息用来区别用户。
在具体的技术选择上:
在微服务架构下,无状态的身份验证是更为合适的方式,其中以 JWT 为代表,最为流行。
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准 RFC 7519 该token被设计为紧凑且安全的,特别适用于分布式站点的 单点登录(SSO) 场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
Casbin 根本上是依托规则引擎做的软件设计,抽取出来的模型可以做到通用化,能够轻松的使用多种不同的控制模型:ACL, RBAC, ABAC等。
Casbin是一个强大的、高效的开源访问控制框架,其权限管理机制支持多种访问控制模型。目前这个框架的生态已经发展的越来越好了。提供了各种语言的类库,自定义的权限模型语言,以及模型编辑器。
{subject, object, action}
。root
或 administrator
。超级用户可以执行任何操作而无需显式的权限声明。keyMatch
,方便对路径式的资源进行管理,如 /foo/bar
可以映射到 /foo*
Kratos是B站开源出来的一个微服务框架,我在做技术选型的时候,横向的对比了市面上的主流几款微服务架构,总结下来,还是Kratos更加适合我使用,于是就选择了它。
Kratos的认证和权鉴都是依托中间件来实现的。认证方面,Kratos官方已经支持了Jwt中间件 。鉴权方面,Kratos官方还没有对此的支持,于是我就自己简单的实现了一个Casbin中间件 ,简单封装,足够使用就是了。
SecurityUser
用于创建Jwt的令牌,以及后面Casbin解析和存取权鉴相关的数据,需要实现它。
并且实现一个SecurityUserCreator
注册进中间件。
const (
ClaimAuthorityId = "authorityId"
)
type SecurityUser struct {
Path string
Method string
AuthorityId string
}
func NewSecurityUser() authzM.SecurityUser {
return &SecurityUser{}
}
func (su *SecurityUser) ParseFromContext(ctx context.Context) error {
if claims, ok := jwt.FromContext(ctx); ok {
su.AuthorityId = claims.(jwtV4.MapClaims)[ClaimAuthorityId].(string)
} else {
return errors.New("jwt claim missing")
}
if header, ok := transport.FromServerContext(ctx); ok {
su.Path = header.Operation()
su.Method = "*"
} else {
return errors.New("jwt claim missing")
}
return nil
}
func (su *SecurityUser) GetSubject() string {
return su.AuthorityId
}
func (su *SecurityUser) GetObject() string {
return su.Path
}
func (su *SecurityUser) GetAction() string {
return su.Method
}
func (su *SecurityUser) CreateAccessJwtToken(secretKey []byte) string {
claims := jwtV4.NewWithClaims(jwtV4.SigningMethodHS256,
jwtV4.MapClaims{
ClaimAuthorityId: su.AuthorityId,
})
signedToken, err := claims.SignedString(secretKey)
if err != nil {
return ""
}
return signedToken
}
func (su *SecurityUser) ParseAccessJwtTokenFromContext(ctx context.Context) error {
claims, ok := jwt.FromContext(ctx)
if !ok {
return errors.New("no jwt token in context")
}
if err := su.ParseAccessJwtToken(claims); err != nil {
return err
}
return nil
}
func (su *SecurityUser) ParseAccessJwtTokenFromString(token string, secretKey []byte) error {
parseAuth, err := jwtV4.Parse(token, func(*jwtV4.Token) (interface{}, error) {
return secretKey, nil
})
if err != nil {
return err
}
claims, ok := parseAuth.Claims.(jwtV4.MapClaims)
if !ok {
return errors.New("no jwt token in context")
}
if err := su.ParseAccessJwtToken(claims); err != nil {
return err
}
return nil
}
func (su *SecurityUser) ParseAccessJwtToken(claims jwtV4.Claims) error {
if claims == nil {
return errors.New("claims is nil")
}
mc, ok := claims.(jwtV4.MapClaims)
if !ok {
return errors.New("claims is not map claims")
}
strAuthorityId, ok := mc[ClaimAuthorityId]
if ok {
su.AuthorityId = strAuthorityId.(string)
}
return nil
}
在白名单下的API将会被忽略认证和权限验证
需要注意的是:这里面注册的是 操作名(operation),而非是API的Path。具体的操作名是什么,可以在Protoc生成的 *_grpc.pb.go
和 *_http.pb.go
找到。
// NewWhiteListMatcher 创建白名单
func NewWhiteListMatcher() selector.MatchFunc {
whiteList := make(map[string]bool)
whiteList["/admin.v1.AdminService/Login"] = true
return func(ctx context.Context, operation string) bool {
if _, ok := whiteList[operation]; ok {
return false
}
return true
}
}
// NewMiddleware 创建中间件
func NewMiddleware(logger log.Logger) http.ServerOption {
return http.Middleware(
recovery.Recovery(),
tracing.Server(),
logging.Server(logger),
selector.Server(
jwt.Server(
func(token *jwtV4.Token) (interface{}, error) {
return []byte(ac.ApiKey), nil
},
jwt.WithSigningMethod(jwtV4.SigningMethodHS256),
),
).
Match(NewWhiteListMatcher()).Build(),
)
}
var opts = []http.ServerOption{
NewMiddleware(logger),
http.Filter(handlers.CORS(
handlers.AllowedHeaders([]string{"X-Requested-With", "Content-Type", "Authorization"}),
handlers.AllowedMethods([]string{"GET", "POST", "PUT", "DELETE", "HEAD", "OPTIONS"}),
handlers.AllowedOrigins([]string{"*"}),
)),
}
前端只需要在HTTP的Header里面加入以下数据即可:
键 | 值 |
---|---|
Authorization | Bearer {JWT Token} |
export default function authHeader() {
const userStr = localStorage.getItem("user");
let user = null;
if (userStr)
user = JSON.parse(userStr);
if (user && user.token) {
return { Authorization: 'Bearer ' + user.token };
} else {
return {};
}
}
Casbin的模型和策略配置读取,我简化的使用了读取本地配置文件。
通常来说,模型文件变化不大,放本地配置文件或者直接硬代码都没问题。变化的通常都是策略配置,通常做法都是放置在数据库里面,方便通过后台去进行编辑改变。
// NewMiddleware 创建中间件
func NewMiddleware(ac *conf.Auth, logger log.Logger) http.ServerOption {
m, _ := model.NewModelFromFile("../../configs/authz/authz_model.conf")
a := fileAdapter.NewAdapter("../../configs/authz/authz_policy.csv")
return http.Middleware(
recovery.Recovery(),
tracing.Server(),
logging.Server(logger),
selector.Server(
casbinM.Server(
casbinM.WithCasbinModel(m),
casbinM.WithCasbinPolicy(a),
casbinM.WithSecurityUserCreator(myAuthz.NewSecurityUser),
),
).
Match(NewWhiteListMatcher()).Build(),
)
}
func (s *AdminService) Login(_ context.Context, req *v1.LoginReq) (*v1.User, error) {
fmt.Println("Login", req.UserName, req.Password)
var id uint64 = 10
var email = "hello@kratos.com"
var roles []string
switch req.UserName {
case "admin":
roles = append(roles, "ROLE_ADMIN")
case "moderator":
roles = append(roles, "ROLE_MODERATOR")
}
var securityUser myAuthz.SecurityUser
securityUser.AuthorityId = req.GetUserName()
token := securityUser.CreateAccessJwtToken([]byte(s.auth.GetApiKey()))
return &v1.User{
Id: &id,
UserName: &req.UserName,
Token: &token,
Email: &email,
Roles: roles,
}, nil
}
securityUser.CreateAccessJwtToken
生成Jwt的Token
2.3 返回token给前端