2021SC@SDUSC
目录
一.引言
Oauth认证授权主要是为了解决安全性的问题,gateway给我们四种策略来验证安全性,我们验证合法的客户端和合法的授权用户,然后用Token来获取资源,accessToken 和freshToken都是可以验证合法性,accessToken有效时间一般比较短,而freshToken一般较长,客户端用账户名,密码经过一定方式(比如先请求code),获得ACCESS_TOKEN,expire_in与refresh_token。
然后在expire_in到期的时候,通过refresh_token获得新的access_token,expire_in,与refresh_token。refresh_token也有过期时间,当refresh_token过期的时候,则需要用户重新授权登录。
二.gRPC的简介
gRPC是一个现代的、高性能RPC框架,可以运行在任何环境下。它可以有效在数据中心之间连接服务。gRPC基于可以定义远程调用的函数的概念。针对每个方法,定义一个参数并返回类型。服务、参数和返回类型在.proto
文件中定义,使用谷歌的开源语言——中性协议缓存机制。
三.代码分析
Oauth proto
授权服务包括创建授权CreateAuthorization;通过授权码获取会话信息FindAuthorizationByCode ; 签发 accessToken, SignToken ;刷新 accessToken, RefreshAccessToken ;获取令牌载体数据 GetAccess TokenPayload。
message AuthorizationPayload {
string userId = 1;
string clientId = 2;
repeated string scopes = 3;
}
message Authorization {
string code = 1; // 授权码
AuthorizationPayload payload = 2;
}
message AuthorizationCode {
string code = 1;
}
message SignTokenRes {
// 一天就过期呢
string accessToken = 1;
// 过期时间有点长喔
string refreshToken = 2;
// 暂时不实现
string idToken = 3;
// 用户学号
string userId = 4;
}
message RefreshAccessTokenReq {
string refreshToken = 1;
}
message RefreshAccessTokenRes {
string accessToken = 1;
// 原样传回
optional string refreshToken = 2;
}
message AccessToken {
string accessToken = 1;
}
// 开放授权端服务
service OAuthService {
// 创建授权会话,默认三分钟过期
rpc CreateAuthorization(AuthorizationPayload) returns (AuthorizationCode);
// 通过授权码获取会话信息
// 授权码过期将抛出错误
rpc FindAuthorizationByCode (AuthorizationCode) returns (Authorization);
// 签发 accessToken 和 refreshToken
rpc SignToken (AuthorizationCode) returns (SignTokenRes);
// 刷新 accessToken
rpc RefreshAccessToken (RefreshAccessTokenReq) returns (RefreshAccessTokenRes);
// 获取令牌载体数据
rpc GetAccessTokenPayload (AccessToken) returns (AuthorizationPayload);
}
UserService接口包括登入,登出,根据Id寻找用户信息的方法。
export interface UserService {
signIn(req: SignInReq): Observable<SignInRes>;
signOut(req: SignOutReq): Observable<SignOutRes>;
findById(req: UserById): Observable<UserData>;
}
ClientService接口包括创建客户端,根据Id寻找客户端信息,验证客户端合法性的方法。
export interface ClientService {
createClient(req: CreateClientReq): Observable<CreateClientRes>;
findById(req: ClientById): Observable<ClientData>;
validateClient(req: ValidateClientReq): Observable<ValidateClientRes>;
}
签发Token,验证用户合法性,验证客户端合法性
异步方法validateUser功能是实现用户合法性检查,接受参数casId和password,然后调用this.userService.signIn()方法检查casId和password的正确性;异步方法signToken则是为了请求授权即签发Token,首先根据输入的用户Id,调用createAuthorization()方法创建授权,然后根据授权码来调用openAuthService.signToken()方法,得到令牌然后返回。异步方法validateClient功能是验证客户端合法性,根据客户端传入的Id和密钥调用clientService.validateClient()方法得到res,验证res.valid,正确则返回client.Id,错误则返回客户端无效。
async validateUser(casId: string, password: string): Promise<User> {
await this.userService
.signIn({
casId,
password,
})
.toPromise();
return {
userId: casId,
clientId: GatewayClientId,
scopes: [AuthorizationScope.OpenId, AuthorizationScope.UserInfo],
};
}
async signToken(userId: string) {
// 请求授权
const authCode = await this.openAuthService
.createAuthorization({
userId,
clientId: GatewayClientId,
scopes: [AuthorizationScope.OpenId, AuthorizationScope.UserInfo],
})
.toPromise();
// 根据授权码换取令牌
const res = await this.openAuthService.signToken(authCode).toPromise();
return res;
}
async validateClient(clientId: string, secret: string) {
const res = await this.clientService
.validateClient({
id: clientId,
secret,
})
.toPromise();
if (res.valid) {
return clientId;
}
throw new UnauthorizedException(res.error || '客户端无效');
}
持有人的Token合法性验证策略
本策略通过异步函数validate()传入访问令牌accessToken,然后再调用openAuthService
.getAccessTokenPayload()来验证accessToken的正确性,返回结果User。
@Injectable()
export class BearerStrategy
extends PassportStrategy(Strategy)
implements OnModuleInit
{
private openAuthService: OpenAuthService;
constructor(
@Inject(getGrpcPackageToken('oauth'))
private readonly oAuthClient: ClientGrpc,
) {
super();
}
async validate(accessToken: string): Promise<User> {
const res = await this.openAuthService
.getAccessTokenPayload({
accessToken,
})
.toPromise();
return res;
}
onModuleInit() {
this.openAuthService = this.oAuthClient.getService('OAuthService');
}
}
客户端的合法验证策略
现在构造函数中注入私有类型 AuthenticationService变量authService,然后异步函数validate调用authService.validateClient(),将接收到的clientId和secret作为函数validateClient参数。
import { BasicStrategy } from 'passport-http';
import { PassportStrategy } from '@nestjs/passport';
import { Injectable } from '@nestjs/common';
import { AuthenticationService } from './authentication.service';
@Injectable()
export class ClientStrategy extends PassportStrategy(BasicStrategy, 'client') {
constructor(private authService: AuthenticationService) {
super();
}
async validate(clientId: string, secret: string): Promise<string> {
return this.authService.validateClient(clientId, secret);
}
}
本地图灵门户验证合法性
类TuringBearerStrategy继承自PassportStrategy类,构造器中注入了oAuthClient变量, ClientGrpc类型,在异步函数validate中传入accessToken,然后再调用openAuthService.getAccessTokenPayload()来验证accessToken的正确性,返回结果User。
如果返回的res中clientId不等于GatewayClientId,说明客户端不是我们的图灵互联网站,抛出异常'仅允许图灵互联访问本系统'。
@Injectable()
export class TuringBearerStrategy
extends PassportStrategy(Strategy, 'turing')
implements OnModuleInit
{
private openAuthService: OpenAuthService;
constructor(
@Inject(getGrpcPackageToken('oauth'))
private readonly oAuthClient: ClientGrpc,
) {
super();
}
async validate(accessToken: string): Promise<User> {
const res = await this.openAuthService
.getAccessTokenPayload({
accessToken,
})
.toPromise();
if (res.clientId !== GatewayClientId) {
throw new UnauthorizedException('仅允许图灵互联访问本系统');
}
return res;
}
onModuleInit() {
this.openAuthService = this.oAuthClient.getService('OAuthService');
}
}
取得Token令牌的流程
1.先接受dto: GetTokenDto,clientId: string两个参数;
2.根据dto中grant_type与GrantType.AuthCode 比对,
(1)如果正确,查找dto里的code是不是存在,如不存在,报错'授权码无效',如果存在就通过openAuthService.findAuthorizationByCode()方法来得到授权,将授权authorization.payload.clientId客户端Id与系统的clientId比对,不相同则报错,抛出'客户端非法'异常,如果相同的话就调用 this.openAuthService.signToken()签发Token并返回res;
(2)如果dto.grant_type等于GrantType.RefreshToken,找dto里的refresh_token是不是存在,如不存在,报错'刷新令牌无效',如果存在就通过openAuthService.refreshAccessToken()方法来得到刷新访问令牌,然后刷新的accessToken通过openAuthService .getAccessTokenPayload()得到授权载体,将授权载体的clientId客户端Id与系统的clientId比对,不相同则报错,抛出'客户端非法'异常,如果相同的话刷新令牌res;
3.若都不匹配则直接抛出未实现异常。
async getToken(
@Body() dto: GetTokenDto,
@CurUser() clientId: string,
): Promise<GetTokenResDto> {
if (dto.grant_type === GrantType.AuthCode) {
if (!dto.code) {
throw new BadRequestException('授权码无效');
}
const authorization = await this.openAuthService
.findAuthorizationByCode({
code: dto.code,
})
.toPromise();
if (authorization.payload.clientId !== clientId) {
throw new ForbiddenException('客户端非法');
}
const res = await this.openAuthService
.signToken({
code: dto.code,
})
.toPromise();
return {
access_token: res.accessToken,
refresh_token: res.refreshToken,
id_token: res.idToken,
user_id: res.userId,
scope: authorization.payload.scopes.join(' '),
};
} else if (dto.grant_type === GrantType.RefreshToken) {
if (!dto.refresh_token) {
throw new BadRequestException('刷新令牌无效');
}
const res = await this.openAuthService
.refreshAccessToken({
refreshToken: dto.refresh_token,
})
.toPromise();
const authPayload = await this.openAuthService
.getAccessTokenPayload({
accessToken: res.accessToken,
})
.toPromise();
if (authPayload.clientId !== clientId) {
throw new ForbiddenException('客户端非法');
}
return {
access_token: res.accessToken,
refresh_token: res.refreshToken,
id_token: res.idToken,
user_id: res.userId,
scope: authPayload.scopes.join(' '),
};
}
throw new NotImplementedException();
}
三.总结
说到Oauth认证授权,现在已经有知道了流程与详细的细节:用户想操作存放在服务提供方的资源。用户登录客户端向服务提供方请求一个临时令牌;服务提供方验证客户端的身份后,授予一个临时令牌;客户端获得临时令牌后,将用户引导至服务提供方的授权页面请求用户授权;在这个过程中将临时令牌和客户端的回调连接发送给服务提供方;用户在服务提供方的网页上输入用户名和密码,然后授权该客户端访问所请求的资源;授权成功后,服务提供方引导用户返回客户端的网页;客户端根据临时令牌从服务提供方那里获取访问令牌;服务提供方根据临时令牌和用户的授权情况授予客户端访问令牌;客户端使用获取的访问令牌访问存放在服务提供方上的受保护的资源。Refresh_token的作用是刷新AccessToken。认证服务器会提供一个刷新接口,我们传入Refresh_token和client_id,认证服务器通过后会返回一个新的AccessToken。但是为了安全,Oauth2.0引入了两个措施:要求refresh_token必须保存在客户端的服务器上,调用refresh_token的时候一定是从服务器到服务器的访问;OAuth2引入了Client_Secret机制。每一个Client_id,都对应一个Client_Secret。这个Client_Secret会在客户端申请Client_id时,随Client_id一起分配给客户端。客户端把他们都保存在服务器上,刷新Refresh_token时,需要验证这个Client_Secret。这就是Oauth协议基本原理