认证包含的功能范围以及在整个体系中的角色
区分资源,认证服务
说明白claim,scope等
提供哪些功能
申请授权码
http://localhost:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://localhost:9001/callback&response_type=code&scope=read_userinfo
获取授权码
通过用户的授权之后,浏览器重定向到redirect_uri,授权码以code参数的形式在重定向url上面
http://localhost:9001/callback&code=XXXXXXX
获取token
http://localhost:8080/oauth/token?code=XXXXXXX&grant_type=authorization_code&redirect_uri=http://localhost:9001/callback&scope=read_userinfo
简化模式也被称为隐式许可类型,客户端运行在浏览器内部,比如使用javascript,response_type参数为token
申请授权
http://localhost:8080/oauth/authorize?client_id=clientapp&redirect_uri=http://localhost:9001/callback&response_type=token&scope=admin&state=abc
获取token
在用户approve之后浏览器会跳转到重定向地址并携带token相关信息
http://localhost:9001/callback#access_token=xxxx-xxxxx-xxxx&token_type=bearer&state=abc&expries_in=59
获得的access_token会在重定向的url的参数中
密码模式适用于用户对应用程序高度信任的情况。比如是用户操作系统的一部分。认证服务器只有在其他授权模式无法执行的情况下,才能考虑使用这种模式。
获取token
http://localhost:8080/oauth/token?password=123456&grant_type=password&username=lll&scope=admin
客户端模式又称为客户端凭据许可类型
没有明确的资源拥有者,或对于客户端来说资源拥有者不可区分。使用客户端的凭据直接向授权服务器获取token,不涉及用户
申请授权token
http://localhost:8080/oauth/token?grant_type=client_credentials&scope=admin
结果携带OIDC的id_token
{
"access_token": "SampleAccessToken",
"id_token": "SampleIdToken",
"token_type": "bearer",
"expires_in": 3600,
"refresh_token": "SampleRefreshToken",
"scope":"read write"
}
目前授权服务器的认证功能遵守OIDC(openID connect)协议,OIDC协议将会增加response_type,id_token
更多内容参考:
OIDC(OpenId Connect)身份认证(核心部分)
OIDC(OpenId Connect)身份认证(扩展部分)
access_token被称为访问令牌,携带access_token可以对资源服务器进行访问
在本次设计中access_token不会包含用户信息
OAuth2提供了Access Token来解决授权第三方客户端访问受保护资源的问题;OIDC在这个基础上提供了ID Token来解决第三方客户端标识用户身份认证的问题。OIDC的核心在于在OAuth2的授权流程中,一并提供用户的身份认证信息(ID Token)给到第三方客户端,ID Token使用JWT格式来包装,得益于JWT(JSON Web Token)的自包含性,紧凑性以及防篡改机制,使得ID Token可以安全的传递给第三方客户端程序并且容易被验证。此外还提供了UserInfo的接口,用户获取用户的更完整的信息。
EU:End User:一个人类用户
RP:Relying Party ,用来代指OAuth2中的受信任的客户端,身份认证和授权信息的消费方
OP:OpenID Provider,有能力提供EU认证的服务(比如OAuth2中的授权服务),用来为RP提供EU的身份认证信息
ID Token:JWT格式的数据,包含EU身份认证的信息
UserInfo Endpoint:用户信息接口(受OAuth2保护),当RP使用Access Token访问时,返回授权用户的信息,此接口必须使用HTTPS
Id Token是一个签名的JSON Web Token(JWT),内容如下:
{
"iss": "https://server.example.com", #必须
"sub": "24400320", #必须
"aud": "s6BhdRkqt3", # 必须
"nonce": "n-0S6_WzA2Mj",
"exp": 1311281970, #必须
"iat": 1311280970, #必须
"auth_time": 1311280969,
"acr": "urn:mace:incommon:iap:silver"
}
iss = Issuer Identifier:必须。提供认证信息者的唯一标识。一般是一个https的url(不包含querystring和fragment部分)。
sub = Subject Identifier:必须。iss提供的EU的标识,在iss范围内唯一。它会被RP用来标识唯一的用户。最长为255个ASCII个字符。
aud = Audience(s):必须。标识ID Token的受众。必须包含OAuth2的client_id。
exp = Expiration time:必须。过期时间,超过此时间的ID Token会作废不再被验证通过。
iat = Issued At Time:必须。JWT的构建的时间。
auth_time = AuthenticationTime:EU完成认证的时间。如果RP发送AuthN请求的时候携带max_age的参数,则此Claim是必须的。
nonce:RP发送请求的时候提供的随机字符串,用来减缓重放攻击,也可以来关联ID Token和RP本身的Session信息。
acr = Authentication Context Class Reference:可选。表示一个认证上下文引用值,可以用来标识认证上下文类。
amr = Authentication Methods References:可选。表示一组认证方法。
azp = Authorized party:可选。结合aud使用。只有在被认证的一方和受众(aud)不一致时才使用此值,一般情况下很少使用。
id_token中携带者用户的标识信息,客户端解析id_token之后就可以知道是是谁发起的认证请求,并在客户端显示出来
假如我们有系统A,系统B,系统C三个系统,这三个系统都有自己的登录界面但是内部用户数据是打通的。
当用户在系统A登陆的时候,在同一浏览器跳转到系统B或者C之后无需再登录。
这里我们通过oauth2.0进行单点登录的实现,客户端使用认证服务器颁发的access_token访问三个系统,当token过期或者退出登录之后token将无效,三个系统将不能被访问,达到单点登录的效果。
资源服务器上面存放着资源内容,可以提供给用户进行访问
资源服务器在获取token之后首先到认证服务器验证token的有效性
每个客户端在注册的时候都会注册自己的scopes(可能多个),resourceIds(可能多个),当客户端在申请认证的时候会首先验证客户端的scope,如果scope跟注册时候的scope对不上会拒绝认证
scope代表了客户端的访问权限,可以在资源服务器设置不同scope可以访问哪些API
token的过期时间也是客户端的一个属性
id_token会被RSAwithSHA256私钥加密,公钥加密会通过授权服务器以接口的方式暴露给客户端,客户端拿到公钥之后将jwt解密并解析出来
OIDC使用授权码模式
多个scope参数用空格分隔
http://localhost:9110/oauth/authorize?client_id=008fec3d-c125-409e-9f8d-ef7724ec21df&redirect_uri=http://www.baidu.com&response_type=code&scope=write read&state=abc
OIDC协议规范的response_type可能有多中,在我们的授权服务器建设过程中使用code类型即刻
可以加state参数防止跨站请求攻击
http://localhost:9110/oauth/token?client_id=008fec3d-c125-409e-9f8d-ef7724ec21df&grant_type=authorization_code&redirect_uri=http://www.baidu.com&code=8QKsHT
{
"access_token": "SlAV32hkKG",
"token_type": "Bearer",
"refresh_token": "8xLOxBtZp8",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzc
yI6ICJodHRwOi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5
NzYxMDAxIiwKICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZ
fV3pBMk1qIiwKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5Nz
AKfQ.ggW8hZ1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6q
Jp6IcmD3HP99Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJ
NqeGpe-gccMg4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7Tpd
QyHE5lcMiKPXfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoS
K5hoDalrcvRYLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4
XUVrWOLrLl0nx7RkKU8NXNHq-rvKMzqg",
"scope":"admin"
}
注:id_token不能作为访问令牌进行资源访问
客户端获取access_token之后可以对资源服务器进行访问
客户端获取到id_token之后解析id_token可以得知当前授权用户是谁
客户端使用access_token进行资源访问,资源服务器拦截到token之后对token进行检查校验token的过期时间,token的scope,校验通过后允许访问API
注册接口
/client/addClient
注册客户端需要的数据
{
"access_token_validity": 60,#token有效时间
"authorities": [#授权人需要具有的身份
"admin",
"user"
],
"authorized_grant_types": [ #授权类型
"password",
"refresh_token",
"authorization_code"
],
"autoapprove": [#哪些scope可以被授权者直接授权 如果为true则表示全部可以直接授权,false为全部不可以直接同意授权
"read"
],
"client_secret": "123456",#client的密码
"redirect_uri": [#回调地址
"http://www.baidu.com"
],
"refresh_token_validity": 100,#refresh有效时长
"resource_ids": [#资源服务器id
"system"
],
"scope": [#客户端scope
"read",
"wirte"
]
}
在oauth2.0授权完成后,客户端不清楚是谁允许授权,是谁授的权,access_token的受众是谁,这些问题如果不解决,客户端是迷茫的,所以将这些信息都包含在id_token中供客户端获取这些信息。
认证服务器的id_token使用jwt格式,id_token的sign值采用的是RSAwith256秘钥非对称加密,解密需要使用公钥
公钥地址:
/.well-known/jwks.json
响应数据,这是一个经过jose加密的公钥
{
"kty": "RSA",
"e": "AQAB",
"n": "p2naRuozp2VPk2-cysifAtwmCiHI2KaSeYwnN_OPr317cEKgfU1zuEtULeQJ_dvzAyC-w7vseUY7OyD2RGVzy8pJPENidSXvRw2Q-EY7Uvz1y0RTkiyhSVkktD66x6eSuuH5gu5ilBMPx6TXwR8jHM3S3h8ilD5YjdXLaQ732g8"
}
token解析可以使用java jose工具包进行解析,解析token的时候要验证jwt的签名防止伪造jwt。
当应用使用完token之后需要将token注销,注销接口:
/oauth/revoke-token
认证服务器会开放/oauth/check_token
接口作为token验证接口,要求在验证的时候需要输入在认证服务器有效的客户端id(clientId)和客户端密码(clientSecret)
@Primary
@Bean
public RemoteTokenServices tokenServices() {
final RemoteTokenServices tokenService = new RemoteTokenServices();
tokenService.setCheckTokenEndpointUrl(String.format("%s/oauth/check_token",host));
tokenService.setClientId(clientId);
tokenService.setClientSecret(clientSecret);
return tokenService;
}
验证示例url:
http://112:222@localhost:9110/oauth/check_token?token=3b26a2c1-7165-4bc2-9c84-29bdc84681ec
以下是验证结果:
token有效:
{
"aud": [
"travelme",
"system"
],
"user_name": "xiaoliu",
"scope": [
"write"
],
"id_token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiJ4aWFvbGl1IiwiYXVkIjoiMTExIiwic2NvcGUiOlsid3JpdGUiXSwiaXNzIjoiaHR0cDovLzEzOS4yMTkuMTEuMjExOjMwOTIwIiwiZXhwIjoxNTY2MTEwMjE2LCJpYXQiOjE1NjYwMjM4MTYsImF1dGhvcml0aWVzIjpbImFkbWluIl19.IgpEPQaILIqerIJMVthDBy4_UkI_Tixtj9A11hn4YGbNt7b4LvqbcoScG3vHRtAQytQc1tBPKFONnPVIUapsEqrHdA96yVYse2NvcJfa1QJp6brW0I-QjZ_H6bYPAWBhLqkrLBjXp6qMfRbBd2hBBihrU3hGIA8L9bKm5umq6V8",
"active": true,
"exp": 1566023876,
"grantType": "authorization_code",
"authorities": [
"admin"
],
"client_id": "111",
"status": 1
}
token过期:
{
"error": "invalid_token",
"error_description": "Token has expired"
}
资源服务器会判断哪些scope有什么样的权限spring security帮我们实现scope验证
以下java代码表示/hello路径下面的所有访问,客户端scope必须包含write
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.and().csrf().disable()
.authorizeRequests()
.antMatchers("/hello/**").access("#oauth2.hasScope('write')";
}
自定义实现过滤器GlobalFilter和加载顺序Ordered接口,重写filter方法
目前网关拦截逻辑是如果请求头中传输了Bearer token,网关将token拿到认证服务器进行有效性验证验证成功将放行,验证失败将在网关层返回401无访问权限状态码
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String accessToken = extractToken(exchange.getRequest());
if (pathMatcher.match("/travelme/dev/staffLogin", exchange.getRequest().getPath().value())) {
return chain.filter(exchange);
}
//目前token为空时直接放行 之后可以根据接口规则配置路径拦截
if (accessToken == null) {
return chain.filter(exchange);
} else {
//在资源服务器校验token
String result = checkToken(host, accessToken);
try {
if (result.equals(FAILED)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
} else {
return chain.filter(exchange);
}
} catch (Exception e) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
}
}