简介Oauth2.0授权步骤

授权码模式的基本步骤

原文链接地址

(A)用户访问客户端,后者将前者导向认证服务器。

(B)用户选择是否给予客户端授权。

(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。

(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。

(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。

各步骤详细解析

A步骤中,客户端申请认证的URI,包含以下参数:

  • response_type:表示授权类型,必选项,此处的值固定为"code"
  • client_id:表示客户端的ID,必选项
  • redirect_uri:表示重定向URI,可选项
  • scope:表示申请的权限范围,可选项
  • state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
  • 下面是一个例子。

    /authorize?response_type=code&client_id=s6BhdRkqt3&state=xyz

    &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

C步骤中,服务器回应客户端的URI,包含以下参数:

  • code:表示授权码,必选项。该码的有效期应该很短,通常设为10分钟,客户端只能使用该码一次,否则会被授权服务器拒绝。该码与客户端ID和重定向URI,是一一对应关系。
  • state:如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数
  • 下面是一个例子。 https://client.example.com/cb?code=SplxlOBeZQQYbYS6WxSbIA

    &state=xyz

D步骤中,客户端向认证服务器申请令牌的HTTP请求,包含以下参数:

  • grant_type:表示使用的授权模式,必选项,此处的值固定为"authorization_code"。
  • code:表示上一步获得的授权码,必选项。
  • redirect_uri:表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。
  • client_id:表示客户端ID,必选项。
  • 下面是一个例子。 /token?grant_type=authorization_code&code=SplxlOBeZQQYbYS6WxSbIA

    &redirect_uri=https%3A%2F%2Fclient%2Eexample%2Ecom%2Fcb

E步骤中,认证服务器发送的HTTP回复,包含以下参数:

  • access_token:表示访问令牌,必选项。
  • token_type:表示令牌类型,该值大小写不敏感,必选项,可以是bearer类型或mac类型。
  • expires_in:表示过期时间,单位为秒。如果省略该参数,必须其他方式设置过期时间。
  • refresh_token:表示更新令牌,用来获取下一次的访问令牌,可选项。
  • scope:表示权限范围,如果与客户端申请的范围一致,此项可省略。
  • 下面是一个例子。{

    "access_token":"2YotnFZFEjr1zCsicMWpAA",

    "token_type":"example",

    "expires_in":3600,

    "refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",

    "example_parameter":"example_value"

    }

项目实战总结

项目背景简介

  • 项目用的spring全家桶+shiro+oauth2.0,不懂shiro也没关系
  • A系统为门户系统,需要用户登录;
  • B系统是一个业务系统,嵌在A系统里面,所以不需要再重新登陆,不提供相关登陆接口;

具体代码及流程

先介绍B网站登陆认证流程

第一步

  • 有一个filter,继承自shiro的【UserFilter】,onAccessDenied方法(即未登录状态下访问B接口)
@Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
0. 获取当前请求的requestUri=requestUri()
1. 生成uuid
2. 将request保存到redis,有效期30秒,redis.set(uuid,request)
3. loginUrl = getLoginUrl()="/B/login?oauth";配置文件
4. 重定向到loginUrl?redirect_uri=/B/proxy?_oauth=uuid
即/B/login?oauth&redirect_uri=/B/proxy?_oauth=uuid 总结:单纯系统内拦截未登录的请求,并将请求信息保存下来,然后跳转到B系统的登陆url,附加了一些参数,方便回调
}

第二步

@RequestMapping("/login")
public void login(
HttpServletRequest request,
HttpServletResponse response
) throws URISyntaxException, IOException{ String code=request.getParameter("code");
if(StringUtils.isNotBlank(code)){
// ...
// 判断code的先省略,一会再看
// code不为空,代表是从A系统授权过来的请求
// ....
}else{
else 部分的方法见下面 } // else 部分的方法 // 获取loginUrl=http://localhost:8080/B/login相当于是写死的
1.loginUrl=String.format("%s://%s:%d%s/%s",request.getScheme(),request.getServerName(),request.getServerPort(),request.getContextPath(),"login");
2.取第一步中的传过来的参数redirect_uri,拼接到loginUrl的后面,
3.新的loginUrl=http://localhost:8080/B/login?redirect_uri=/B/proxy?_oauth=b54896bc-2c26-4763-a2a6-7cbfb9223c76
// 以上都只是在自己的系统中拼装参数之类的
// 接下来就是应用Oauth2相关的协议了
我们在B系统的配置文件中配置了以下配置项
oauth2.url=http://localhost:8080/A
oauth2.loginUrl=http://localhost/A
oauth2.resource.charset=utf-8
oauth2.authorize=/oauth2/authorize
oauth2.access_token=/oauth2/access_token
oauth2.resource=/oauth2/resource
oauth2.client_id=c3991093-f25c-11e6-b7ab-524f72a73ffc
oauth2.client_secret=d02153b8-f25c-11e6-b7ab-524f72a73ffc
4.接下来就要调用/A系统中的方法授权方法了,取{oauth2.loginUrl}和{oauth2.authorize}俩个参数,拼装成:http://localhost/portal.web/oauth2/authorize。
5.接下来再拼装Oauth的参数
4和5俩不代码如下
URIBuilder uriBuilder=new URIBuilder((StringUtils.isBlank(loginUrl)?url:loginUrl)+authorizeUrl);
uriBuilder.addParameter("response_type", "code");
uriBuilder.addParameter("client_id", clientId);
//参数logoutUrl的获取方式与本方法的loginUrl获取方式一致
uriBuilder.addParameter("logout_uri",logoutUrl);
uriBuilder.addParameter("state", "portal");
// reUrl就是本方法步骤3生成的loginUrl
uriBuilder.addParameter("redirect_uri", reUrl); 6.最后一步就是跳转到A系统请求授权redirect(URIBuilder.toString)
}

第三步

  • A系统的方法

    1. A系统同样有一个filter用来拦截请求,继承自shiro的UserFilter,当遇到没有登陆的请求时,
    @Override
protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
// 获取请求
String requestURI = getPathWithinApplication(request);
// 判断请求,如果是登陆请求则直接拦截(意味着shiro会将重定向到登陆页面);如果不是登陆请求,则保存请求,然后重定向
boolean isLogin=requestURI.indexOf(getLoginUrl())>-1;
if(!isLogin){ String url=request.getRequestURI().toLowerCase();
if(url.indexOf("?")>0){ //去掉?后面的参数
url=url.substring(0,url.indexOf("?"));
}
if(url.endsWith(DEFAULT_REQUEST_JSON) || isJsonRequest(request)){
forwardResponseJson(request, response);
}else if(url.endsWith(DEFAULT_REQUEST_JSONP)){
forwardResponseJsonp(request, response);
}else if(url.endsWith(DEFAULT_REQUEST_XML)){
forwardResponseXml(request, response);
}else{
//判断是否是oauth2请求,否则返回局部界面
String requestURI = getPathWithinApplication(request);
if(requestURI.equals("/oauth2/authorize")) {
// 第二步中发起的授权请求会进入此处
saveRequestAndRedirectToLogin(request, response);
}else{
redirectToLogin(request, response);
}
}
}
return isLogin;
}

第3.1步 saveRequestAndRedirectToLogin方法

   // shiro的方法
@Override
protected void saveRequestAndRedirectToLogin(ServletRequest request, ServletResponse response) throws IOException {
saveRequest(request);
redirectToLogin(request, response);
}

第4步

经过3.1步后,此时系统跳转到A系统的登陆页面,用户输入账号密码后,登陆成功后判断是否是非登陆的请求,是的话则跳转到之前的请求。登陆代码略过,
private void addRedirectUrl(LoginVo lv) {
Subject subject =SecurityUtils.getSubject();
SavedRequest savedRequest=(SavedRequest)subject.getSession().getAttribute(WebUtils.SAVED_REQUEST_KEY);
if(savedRequest!=null) {
// 此时uri = /portal.web/oauth2/authorize,就是3.1步中保存下来的请求
String uri=savedRequest.getRequestURI();
if(uri.endsWith("/oauth2/authorize")){
// 取即将跳转的url
// url=/portal.web/oauth2/authorize?response_type=code&client_id=c3991093-f25c-11e6-b7ab-524f72a73ffc&logout_uri=http%3A%2F%2Flocalhost%3A8080%2Fcrm-web%2Flogout&state=portal&redirect_uri=http%3A%2F%2Flocalhost%3A8080%2Fcrm-web%2Flogin%3Fredirect_uri%3D%2Fcrm-web%2Fproxy%3F_oauth%3D9041035e-8cd1-48fb-9c90-adc46f9abcae
//
String url=savedRequest.getRequestUrl();
// 这里就是关键了,前端会根据这个字段来判断跳转到哪里,如果不为空就会跳转到url所在的url。前端代码
lv.setRedirectUrl(url); // 再从shiro的session中移除该请求的信息
Subject subject =SecurityUtils.getSubject();
subject.getSession().removeAttribute(WebUtils.SAVED_REQUEST_KEY);
}
}
}

第5步

此时肯定是已经登陆了A系统,即将访问A系统的/oauth2/authorize方法如下

这里先把第3/4步的请求本方法的参数打开,方便观看,携带的参数如下

{

response_type=code,

client_id=c3991093-f25c-11e6-b7ab-524f72a73ffc,

logout_uri=http://localhost:8080/crm-web/logout,

state=portal,

redirect_uri=redirect_uri=http://localhost:8080/crm-web/login?redirect_uri=/crm-web/proxy?_oauth=9041035e-8cd1-48fb-9c90-adc46f9abcae

}

这个redirect_uri参数注意一下,它还接着一个redirect_uri,带着了一会登陆B系统后再跳转到后接着的这个redirect_uri参数

@RequestMapping(value = "/authorize",method = RequestMethod.GET)
protected void authorize(HttpServletRequest request,HttpServletResponse response){
OAuthResponse resp=null;
OAuthAuthzRequest oauthRequest = oauthRequest = new OAuthAuthzRequest(request);
// 判断clientId是否存在
if(exists(oauthRequest.getClientId())){
// 生成一个授权code
MD5Generator generator=new MD5Generator();
OAuthIssuer oauthIssuer = new OAuthIssuerImpl(generator);
String authCode = oauthIssuer.authorizationCode();
// OAuth.OAUTH_REDIRECT_URI = "redirect_uri"
String redirectUri=oauthRequest.getParam(OAuth.OAUTH_REDIRECT_URI);
// 然后将刚生成的code与redirect_uri拼在一起生成一个新的code
String code=generator.generateValue(String.format("%s@%s",authCode,redirectUri));
// 将新生成的code封装成code参数拼装到redirectUri的后面
redirectUri=String.format("%s&code=%s",redirectUri,code);
// 这一串看着挺吓人,就是bean的builder模式,简单说就是根据属性值生成一个bean而已
resp = OAuthASResponse
.setCode(code)
.setParam("rs-platform-id", String.valueOf(platId))
.location(redirectUri)
.buildQueryMessage(); //将clientId,code,redirectUri, user封成一个对象,存储到redis
redis.save(clientId,code,redirectUri, user) // 跳转到resp.getLocationUri()
// http://localhost:8080/B/login?redirect_uri=/B/proxy?_oauth=549c9e2e-f74a-4a53-8029-b8f8ceb144cb&code=224d33890c31c78070e151897c6f2882&code=224d33890c31c78070e151897c6f2882&state=portal
//至此,终于又跳回到B系统的login方法了,相比第一次,多了一个code参数
response.sendRedirect(resp.getLocationUri()); }
}

第6步

补全B系统的login方法

@RequestMapping("/login")
public void login(
HttpServletRequest request,
HttpServletResponse response
) throws URISyntaxException, IOException{
// 补全判断code相关的方法,其余方法见第2步
String code=request.getParameter("code");
if(StringUtils.isNotBlank(code)){
String host=request.getRemoteHost();
String reutrnUri=request.getParameter(OAuth2Utils.OAUTH_RETURN_URI); RunsaOAuthToken token=new RunsaOAuthToken(code,false,host);
ResourceVo resource=new ResourceVo();
try {
// 用code换access_token
OAuth2Response oauthResponse = OAuth2Utils.accessToken(code);
// 用access_token换资源,我们项目中只用到了免登陆,所以取的是登陆信息
resource=OAuth2Utils.resource(oauthResponse);
token.setPrincipal(new PrincipalVo(resource.getUser(),resource.getAuthz()));
// 系统B的登陆
subject.login(token);
//调转到访问路径
if(StringUtils.isNotBlank(reutrnUri)){
response.sendRedirect(reutrnUri);
}
}catch (Exception e) {
logger.error(ExceptionUtils.getStackTrace(e));
OutputStream outputStream=response.getOutputStream();
IOUtils.write(ExceptionUtils.getStackTrace(e), outputStream);
IOUtils.closeQuietly(outputStream);
}
}else{
// 剩下的方法详细代码见第2步
}
}

到这里整个大概的流程就结束

下面介绍用code换取token和用token换取source这俩个方法。在我们项目中不太重视这个,我们俩个A/B俩个项目用的一个数据库,想取啥数据直接数据库取就好了。只是免登陆,减少用户操作罢了,没有用到oauth的精髓。我们后来又有了一个项目C,就没有再用oauth这种重量级的东西来仅仅做一个免登陆功能了,用的jwt。用户登录A后将登陆信息生成一个jwt放在cookie里,请求C接口时带上jwt就好了。仅仅一个免登陆就要用oauth2来实现,有点大材小用的感觉。当然这不妨碍我们学习。我们项目还自写了类似mybatis的东西;为了一个搜索,还引入了es,可惜最后没有投入使用,只进行了一半。(es做一半离职了)炫技的结果

第7步 code换access_token

@RequestMapping(value = "/access_token",method = RequestMethod.POST)
protected void token(HttpServletRequest request,
HttpServletResponse response) throws OAuthSystemException, IOException {
OAuthTokenRequest oauthRequest = null;
OAuthResponse oauthResponse=null;
OAuthIssuer oauthIssuerImpl = new OAuthIssuerImpl(new MD5Generator());
try {
oauthRequest = new OAuthTokenRequest(request);
String authzCode = oauthRequest.getCode();
OAuth2Secret oAuth2Secret=oAuth2Service.selectSecretById(oauthRequest.getClientId(), oauthRequest.getClientSecret());
if(null!=oAuth2Secret){
OAuth2Vo vo=oAuthTokenUtils.select(authzCode);
if(null!=vo){
// 生成令牌,简单理解其实就是uuid
String accessToken = oauthIssuerImpl.accessToken();
String refreshToken = oauthIssuerImpl.refreshToken();
// 拼装一个json
oauthResponse = OAuthASResponse
.tokenResponse(HttpServletResponse.SC_OK)
.setAccessToken(accessToken)
.setExpiresIn("3600")
.setTokenType("Bearer")
.setRefreshToken(refreshToken)
.buildJSONMessage();
// 安全考虑,场景限制,只请求一次就足够了,用完就删
oAuthTokenUtils.clear(authzCode); oAuthTokenUtils.save(oauthRequest.getClientId(),
oauthRequest.getClientSecret(),
accessToken,
refreshToken,
vo.getUsers(),
oAuth2Secret.getMeId());
}
}
} catch (OAuthProblemException ex) {
oauthResponse = OAuthResponse
.errorResponse(HttpServletResponse.SC_UNAUTHORIZED)
.error(ex)
.buildJSONMessage();
}finally{
response.setStatus(oauthResponse.getResponseStatus());
OutputStream outputStream=response.getOutputStream();
IOUtils.write(oauthResponse.getBody(), outputStream);
IOUtils.closeQuietly(outputStream);
}
}

第8步 access_token换source

场景限制,只能访问这一种资源,所有直接写成一个方法了,否则肯定要隔离
@RequestMapping("/resource")
protected void resource(HttpServletRequest request, HttpServletResponse response)throws OAuthSystemException, IOException{
try {
OAuthAccessResourceRequest oauthRequest = new OAuthAccessResourceRequest(request, ParameterStyle.HEADER);
String accessToken =decodeBase64(oauthRequest.getAccessToken());
// 之前用code换取access_token时保存在redis里的
OAuth2Vo vo=oAuthTokenUtils.select(accessToken);
if(null!=vo){
Map<String, Object> map= 各种操作
response.setContentType("application/json;charset=UTF-8");
response.setStatus(HttpServletResponse.SC_OK);
OutputStream outputStream=response.getOutputStream();
IOUtils.write(JSON.toJSONString(map), outputStream);
IOUtils.closeQuietly(outputStream);
// 老规矩,用完就删
oAuthTokenUtils.clear(accessToken);
}else{
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader(OAuth.HeaderType.WWW_AUTHENTICATE, "failure access_token");
}
} catch(OAuthProblemException e) {
logger.error(e.getMessage());
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.addHeader(OAuth.HeaderType.WWW_AUTHENTICATE, "authorization header error");
}
}

项目总结

  • 只用来做免登陆有点可惜;
  • 因场景限制,项目中流程有一定的局限性;
  • 后面会考虑写一个demo放到github上

个人demo实例

前言

为了更好的理解oauth2,有了这个demo,

主要功能:

  • 支持动态添加合作网站
  • 支持token的级别隔离,目前常见的都只有一种token,尝试打造支持多级别的token

项目总结之Oauth2.0免登陆及相关知识点总结的更多相关文章

  1. OAuth2.0 微博登陆网站功能的实现(一)获取用户授权及令牌 Access Token

    在登陆一些网站的时候,可以选择登陆方式为第三方登陆,例如微博登陆,以爱奇艺为例,进入首页,点击 ”登陆“,会弹出登录框: 除了本站登陆外,还可以选择其他第三方登陆,比如微博登陆.QQ 登陆.微信登陆等 ...

  2. QQ互联OAuth2.0 .NET SDK 发布以及网站QQ登陆示例代码(转)

    OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容. QQ登录OAuth2 ...

  3. QQ互联OAuth2.0 .NET SDK 发布以及网站QQ登陆示例代码

    OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他们数据的所有内容. QQ登录OAuth2 ...

  4. Spring Cloud OAuth2.0 微服务中配置 Jwt Token 签名/验证

    关于 Jwt Token 的签名与安全性前面已经做了几篇介绍,在 IdentityServer4 中定义了 Jwt Token 与 Reference Token 两种验证方式(https://www ...

  5. OAuth2.0实战之微信授权篇

    微信开发三大坑: 微信OAuth2.0授权 微信jssdk签名 微信支付签名 本篇先搞定微信OAuth2.0授权吧! 以简书的登陆页面为例,来了解一下oauth2.0验证授权的一些背景知识: 1) 传 ...

  6. 基于OAuth2.0的token无感知刷新

    目前手头的vue项目关于权限一块有一个需求,其实架构师很早就要求我做了,但是由于这个紧急程度不是很高,最近临近项目上线,我才想起,于是赶紧补上这个功能.这个项目是基于OAuth2.0认证,需要在每个请 ...

  7. Spring官方宣布:新的Spring OAuth2.0授权服务器已经来了

    1. 前言 记不记得之前发过一篇文章Spring 官方发起Spring Authorization Server 项目.该项目是由Spring Security主导的一个社区驱动的.独立的孵化项目.由 ...

  8. 什么是“QQ登录OAuth2.0”

    1. 什么是“QQ登录OAuth2.0 OAuth: OAuth(开放授权)是一个开放标准,允许用户授权第三方网站访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方网站或分享他 ...

  9. Oauth2.0 QQ&微信&微博实现第三方登陆

    一.写在前面 目前对于大多数的App或Web网站都支持有第三方登陆这个功能,用户可使用 QQ/ 微信/ 微博 帐号快速登录你的网站,降低注册门槛,为你的网站带来海量新用户.最近在新项目上刚好用到了,在 ...

随机推荐

  1. 去freessl.org申请免费ssl服务器证书

    去freessl.org申请免费ssl服务器证书 来源: 本文链接 来自osnosn的博客 写于: 2019-03-30. 想搞个自签名证书,可以参考这篇: 用openssl为WEB服务器生成证书(自 ...

  2. bzoj5109: [CodePlus 2017]大吉大利,晚上吃鸡!

    Description 最近<绝地求生:大逃杀>风靡全球,皮皮和毛毛也迷上了这款游戏,他们经常组队玩这款游戏.在游戏中,皮皮 和毛毛最喜欢做的事情就是堵桥,每每有一个好时机都能收到不少的快 ...

  3. 学习笔记《Java多线程编程实战指南》二

    2.1线程属性 属性 属性类型及用途  只读属性  注意事项 编号(id) long型,标识不同线程  是  不适合用作唯一标识 名称(name) String型,区分不同线程  否  设置名称有助于 ...

  4. c# 简单方便的连接oracle方式

    通过nuget安装ManagedDataAccess (自动生成的config里面的配置都可以删掉) winform程序,拖出一个datagridview和button using Oracle.Ma ...

  5. 激活WINDOWS SERVER 2019

    Windows Server 2019 Datacenter WMDGN-G9PQG-XVVXX-R3X43-63DFGWindows Server 2019 Standard N69G4-B89J2 ...

  6. DataGridView导出数据到Excel

    //传入DataGridView /// <summary> /// 输出数据到Excel /// </summary> /// <param name="da ...

  7. SSM框架-MyBatis框架数据库的增删查改操作

    话不多说,在User.xml文件中主要写一下操作数据库的sql语句,增,删,查,改是最常见的数据库操作 User.xml文件下:

  8. Kubernetes及Dashboard详细安装配置(Ubuntu14.04)

    前些日子部门计划搞并行开发,需要对开发及测试环境进行隔离,所以打算用kubernetes对docker容器进行版本管理,搭建了下Kubernetes集群,过程如下: 本流程使用了阿里云加速器,配置流程 ...

  9. Python的闭包和装饰器

    什么是闭包 python中函数名是一个特殊的变量,它可以作为另一个函数的返回值,而闭包就是一个函数返回另一个函数后,其内部的局部变量还被另一个函数引用. 闭包的作用就是让一个变量能够常驻内存. def ...

  10. SQLServer 2008 已成功与服务器建立连接,但是在登录前的握手期间发生错误。 (provider: SSL Provider, error: 0 - 等待的操作过时。

    在用SQL Server 2008 在连接其他电脑的实例时,一直提示“已成功与服务器建立连接,但是在登录前的握手期间发生错误. (provider: SSL Provider, error: 0 - ...