在一个单位中,可能是存在多个不同的应用,比如学校会有财务的系统会有学生工作的系统,还有图书馆的系统等等,如果每个系统都用独立的账号认证体系,会给用户带来很大困扰,也给管理带来很大不便。所以需要设计一种统一登录的解决方案。比如我登陆了百度账号,进贴吧时发现已经登录了,进糯米发现也自动登录了。常见的有两种情况,一种是SSO(单点登录)效果是一次输入密码多个网站可以识别在线状态;还有一种是多平台登录,效果是可以用一个账号(比如QQ账号)登录多个不同的网站。
SSO一般用于同一单位的多个站点的登陆状态保持,技术上一般参考CAS协议;多平台登录一般是Oauth体系的协议,有多种认证模式但是不具备会话管理和状态保持。
不过从本质上讲,我觉得两者都是通过可信的第三方进行身份验证,如果说同一单位的多个子系统共同只围绕一个第三方账户(可以称为认证中心)进行多平台登录验证,那么在第三方平台登录后再访问其他网站,效果和统一登录是差不多的。此外,Oauth2还有个好处就是可以实现跨平台的登录管理,因为他的认证过程不依赖于session和cookie,比如对于移动端设备,以及在前后端分离后这种登录认证方式也可以起到很大作用。
这篇文章里我就着结合之前项目中整合过的OAUTH2来讲一讲这种登录认证的过程。项目是基于Shiro+ALTU实现,参考方案 mkk/oauth2-shiro - 码云 - 开源中国 。
在Oauth中至少是有用户,应用服务器,认证服务器这几个角色在交互。OAuth的作用就是让”客户端”安全可控地获取”用户”的授权,与”应用服务器”进行互动。
用户通过浏览器访问一个应用,比如我要上慕课网学习。
重点在于 几次跳转的过程 中,慕课网和QQ登录的服务之间还有过几次交互。
这一部分的操作是 应用服务器和验证服务器之间 的交互,这个过程对用户是透明的。这个过程中慕课网是不需要知道用户的账号密码也可以完成对用户身份的认证,这个token就可以用来标识用户资源。
官方的运行流程图是这样的:
上述讲的是OAuth2中支持的授权码(CODE)方式的认证流程,也是其支持的四种认证方式里最复杂的,其他的三种种包括:
假设现在有一个应用服务器跑在我本机8000端口,认证服务器在8090端口。在需要用户登录时候把用户带到以下的一个URL.
http://localhost:8090/oauth/authorize?response_type=code&scope=read write&client_id=test&redirect_uri=http://localhost:8000/login&state=09876999
我们注意到几个重要的参数:
下面是验证服务器接受这个请求的控制器关键代码:
@RequestMapping("authorize")
public void authorize(HttpServletRequest request, HttpServletResponse response) throws Exception {
try {
OAuthAuthxRequest oauthRequest = new OAuthAuthxRequest(request);
if (oauthRequest.isCode()) {
CodeAuthorizeHandler codeAuthorizeHandler = new CodeAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'code' handler: {}", codeAuthorizeHandler);
codeAuthorizeHandler.handle();
} else if (oauthRequest.isToken()) {
TokenAuthorizeHandler tokenAuthorizeHandler = new TokenAuthorizeHandler(oauthRequest, response);
LOG.debug("Go to response_type = 'token' handler: {}", tokenAuthorizeHandler);
tokenAuthorizeHandler.handle();
} else {
unsupportResponseType(oauthRequest, response);
}
}
}
首先拿到这个请求以后根据请求的参数将其封装成一个 OAuthAuthxRequest
,基本就是把请求过来的参数,方法绑定便于使用。这是由oltu提供的 OAuthRequest
的进一步封装。
然后判断这个请求的授权的类型是否是code,也就是判断下请求参数的 response_type
是否为code,可以看到目前制作了两种类型的授权。
然后根据对应的授权类型,构造对应的方法处理器。下面是handle的实现接口:
public void handle() throws OAuthSystemException, ServletException, IOException {
//验证请求是否合法,主要是针对参数做基本的校验,重定向链接,客户端ID授权范围等这些信息与注册的是否相同。
if (validateFailed()) {
return;
}
//判断用户是否登录过,根据session判断。因此多个应用使用同一个授权服务的话,是可以直接跳过登录步骤的也就实现了单点登录的效果。如果没有登录的话,这一步的请求会被重定向至登录页面。(登录也得隐藏域会带上这些参数)
if (goLogin()) {
return;
}
//这个请求如果是从登录页面提交过来的,那么就提交用户的登录,这个框架中交给shiro去做登录相关的操作。
if (submitLogin()) {
return;
}
// 本系统中把登录和授权放在两个步骤中完成,有点像新浪微博的方式,QQ是一步完成授权。用户未授权则跳转授权页面
if (goApproval()) {
return;
}
//与登录类似,也是提交用户批准或拒绝了权限请求
if (submitApproval()) {
return;
}
//以上任意一步没有通过都是授权失败会进行相应处理,如果都通过了就发放Code码。
handleResponse();
}
如果以上步骤都通过的话,认证服务器会转向这个会调地址,带上发放的Code码,类似如下:
http://localhost:8000/login?code=bca654ab6133ab3cbc55bb751da93b1c&state=09876999
可以看到带回了返回的参数,以及原样返回的状态码。
应用服务器这时候拿到返回的code去换token,发起如下的一个请求:
localhost:8090/oauth/token?client_id=test&client_secret=test&grant_type=authorization_code&code=bca654ab6133ab3cbc55bb751da93b1c&redirect_uri=http://localhost:8000/login&scope=read%20write&state=09876999
与之前请求类似只是多了一个code字段,去验证客户端的合法性。
验证服务器会在收到code以后去查找是否有支持这种code的处理器,如果有则发放token。
for (OAuthTokenHandler handler : handlers) {
if (handler.support(tokenRequest)) {
LOG.debug("Found '{}' handle OAuthTokenxRequest: {}", handler, tokenRequest);
handler.handle(tokenRequest, response);
return;
}
}
初始化支持的handler
private void initialHandlers() {
handlers.add(new AuthorizationCodeTokenHandler());
handlers.add(new PasswordTokenHandler());
handlers.add(new RefreshTokenHandler());
handlers.add(new ClientCredentialsTokenHandler());
}
验证通过后应用服务器会接受到包含token的一个json数据:
{
"access_token": "23e003b5e4b9b7eda228b845532d8336",
"refresh_token": "d6b49710f398c405a62f31a6676c5830",
"token_type": "Bearer",
"expires_in": 43199
}
这个token是有一定的有效期的,在服务端会缓存这个token以便下一次查询,应用客户端也应该保留这个token,访问受限资源时候需要带上这个token去验证身份。
比如请求一个API如下:
curl -i -X GET /
-H "Authorization:Bearer 33dbfc80f5659c6fdec73a044ff724c3" /
'http://localhost:8090/api/test'
资源服务器上使用shiro做安全验证,配置OAuth2对应的realms即可:
<property name="realms">
<list>
<bean id="systemAuthorizingRealm" class="me.kbiao.example.modules.sys.security.SystemAuthorizingRealm"/>
<bean id="oAuth2Realm" class="me.kbiao.example..modules.sys.security.OAuth2Realm"/>
</list>
</property>
在这个reamls中根据token去查到用户信息,再去分发对应的资源。
自此便完成了整个oauth2的流程。
这个流程中认证服务系统需要配置三张数据表:
项目是基于Shiro+ALTU实现,参考方案 mkk/oauth2-shiro - 码云 - 开源中国 ,更详细的内容,可以去读读 Shengzhao Li
开源的代码