12、oauth授权认证流程

使用Spring Security4的四种方法概述

1、一种是全部利用配置文件,将用户、权限、资源(url)硬编码在xml文件中;

2、 二种是用户和权限用数据库存储,而资源(url)和权限的对应采用硬编码配置。

3、三种是细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义过滤器,代替原有的FilterSecurityInterceptor过滤器         并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService,并在配置文件中进行相应配置。

4、四是修改spring security的源代码,主要是修改InvocationSecurityMetadataSourceService和UserDetailsService两个类。 前者是将配置文件     或数据库中存储的资源(url)提取出来加工成为url和权限列表的Map供Security使用,后者提取用户名和权限组成一个完整的(UserDetails)User     对象,该对象可以提供用户的详细信息供AuthentationManager进行认证与授权使用。

这里,我们先介绍第一种,见用户、权限、资源硬编码在代码中,后续文章,由浅入深实现后面几种情况;通过如下图,了解spring security4 的工作机制:

12、oauth授权认证流程_第1张图片

一、Oauth2.0 介绍

 OAuth2.0是2006年开始设计OAuth协议的下一个版本,OAuth2.0同时提供Web,桌面和移动应用程序的支持,并较1.0相比整个授权验证流程更简单更安全。目前国内百度开发平台、腾讯开放平台、新浪微博开发平台的认证都是使用该协议。

Oauth2.0协议相当于在第三方应用程序与用户资源提供商之间,设置了一个授权层(authorization server)。第三方应用不能直接登录用户资源提供商,只能登录到授权层,以此将用户的信息和资源与第三方应用分割开来。用户授权第三方应用以后,第三方应用拿到资源令牌,相当于有了用户获取资源的权限。但是却没有用户的用户名与密码,用户可以在登录的时候,指定资源令牌的权限范围和有效期。

二、单点登录用场景

   对于研发和测试人员来说、平时经常需要登录公司或者部门内部的各个平台进行相关的工作,比如:持续集成、工单系统、RMD(项目管理系统)等等。如果挨个平台输入用户名与密码进行登录,是非常繁琐的同时又有很大的安全隐患。为了解决这个问题,我们可以搭建统一的认证授权平台。这样只要用户登录了授权平台就可以免登陆进入授权平台接入的各个子系统。

所以单点登录解决了多系统中间切换的以下问题:

A、免登陆跳转、解决了流程繁琐的问题。

B、无需在各个平台中暴露用户名、密码,解决了安全的问题。

C、用户没法控制各个平台获取的用户信息的范围与时限的问题。

而Oauth2.0协议就能很好的解决这样的一些问题。

三、Oauth2.0单点登录流程

 

12、oauth授权认证流程_第2张图片

第一步: 搭建基于Oauth2.0的认证授权服务器。实现功能(Oauth2.0协议接口、资源服务器访问、应用授权、令牌管理)。

第二步: 第三方应用向授权中心申请接入。授权中心会赋予第三方应用client_id与secret_key。

第三步: 用户进入第三方应用,第三方应用要求用户给予用户资料访问的授权。此时会302跳转到认证授权服务器登录页面。其中授权的模式有以下几种,推荐使用authorization code模式。

 

A、authorization code:授权码模式,是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与"服务提供商"的认证服务器进行互动。授权的令牌与应用接入key对于普通用户来说是隔离的。

B、Implicit:简化模式,直接在浏览器中向认证服务器申请令牌,url中会直接暴露用户的AccessToken。

C、密码模式(resource owner password credentials):用户向第三方应用提供自己的用户名和密码。第三方应用使用这些信息,向"服务商提供商"索要授权。

D、客户端模式(client credentials):用户直接向客户端注册,客户端以自己的名义要求"服务提供商"提供服务,其实不存在授权问题。

 

流程演示:

注意:

(1)使用浏览器或其他HTTP工具调试的时候必须允许form表单提交,否则报401未授权错误;

(2)授权码模式过程简单可以描述为使用client_id获取授权码,使用授权码换取access_token的过程;

 

1、 用户进入第三方应用,第三方应用要求用户给予用户资料访问的授权。此时会302跳转到认证授权服务器登录页面,即访问授权服务页面模拟;

打开浏览器并且输入:

请求地址:http://localhost:8081/auth/oauth/authorize

请求参数:

名称

含义

备注

client_id

接入客户端的id

client_id 向认证中心申请

response_type

返回响应的类型

 

redirect_uri

重定向的回调地址

 

测试地址:

http://localhost:8081/auth/oauth/authorize?response_type=code&client_id=SampleClientId&redirect_uri=http://localhost:8082/ui/login

2、在用户授权登录页页面输入用户名和密码,我这里的用户名是john,密码是123,然后点击登录

跳转到授权的登录页面地址:http://localhost:8081/auth/login

12、oauth授权认证流程_第3张图片

3、用户通过用户名和密码验证后,授权第三方应用获取用户资料, 此时从认证服务器302跳转回以前的第三方应用,同时附带上授权码。第三方应用得到了认证服务器的授权码

http://localhost:8082/ui/login?code=5heUsD

浏览器重定向到地址:http://localhost:8082/ui/login?code=5heUsD,目的是将获取到的code传送给第三方应用,所以给定的第三方应用地址能够接收获取改code授权码即可(获取到授权码然后才有下一步用授权码code换取code)

12、oauth授权认证流程_第4张图片

3、用户通过用户名和密码验证后,授权第三方应用获取用户资料, 此时从认证服务器302跳转回以前的第三方应用,同时附带上授权码。第三方应用得到了认证服务器的授权码

http://localhost:8082/ui/login?code=5heUsD

浏览器重定向到地址:http://localhost:8082/ui/login?code=5heUsD,目的是将获取到的code传送给第三方应用,所以给定的第三方应用地址能够接收获取改code授权码即可(获取到授权码然后才有下一步用授权码code换取code)

12、oauth授权认证流程_第5张图片

4、打开Postman调试工具,第三方应用根据授权码与自身的secret_key向认证服务器(以POST方式)申请资源令牌。此处是在后台进行的,用户是不可见的

选择POST方式、输入获取授权码地址 http://localhost:8081/auth/oauth/token ,选择发送的body,body数据类型选择form-data,填写如下参数:

名称

含义

备注

client_id

接入客户端的id

client_id 向认证中心申请

client_secret

接入客户端的key

key 向认证中心申请

redirect_uri

重定向的回调地址

 

grant_type

授权类型

固定为:authorization_code

code

授权码

 

具体参数如下:

12、oauth授权认证流程_第6张图片

发送获取的结果:

12、oauth授权认证流程_第7张图片

{

    "access_token": "111f2618-c627-408f-8650-38949749153e",

    "token_type": "bearer",

    "expires_in": 40523,

    "scope": "user_info"

}

注意,

这里没有refresh_token,必须要授权改用户才可以(在AuthorizationServerConfigurerAdapter实现类ClientDetailsServiceConfigurer clients授权)

 clients.inMemory()                                         // 内存模式认证
             .withClient("SampleClientId")                 // client_id

             .secret(passwordEncoder.encode("secret"))     // client_secret

             .authorizedGrantTypes("authorization_code","refresh_token")        
             // grant_type,可以填写多个授权类型,可以通过refresh_token更换过期token
             .scopes("user_info")     // 申请的权限范围scope-用于获取用户信息
             .autoApprove(true) 
// 必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。
// 此地址必须与在应用注册时填写的回调地址一致,注意:该地址是第三方应用注册的地址,此处写死             .redirectUris("http://localhost:8082/ui/login","http://localhost:8083/ui2/login");

 

5、根据授权服务器授权的access_token获取用户信息

请求地址: http://localhost:8081/auth/user/me

请求方式:GET

请求参数: ?access_token=111f2618-c627-408f-8650-38949749153e

返回结果:

{
    "authorities": [
        {
            "authority": "ROLE_USER"
        }
    ],
    "details": {
        "remoteAddress": "0:0:0:0:0:0:0:1",
        "sessionId": null,
        "tokenValue": "111f2618-c627-408f-8650-38949749153e",
        "tokenType": "Bearer",
        "decodedDetails": {
            "remoteAddress": "0:0:0:0:0:0:0:1",
            "sessionId": "FD0AD84B15731EAD357F5D8795E224B3",
            "tokenValue": "111f2618-c627-408f-8650-38949749153e",
            "tokenType": "Bearer",
            "decodedDetails": null
        }
    },
    "authenticated": true,
    "userAuthentication": {
        "authorities": [
            {
                "authority": "ROLE_USER"
            }
        ],
        "details": {
            "remoteAddress": "0:0:0:0:0:0:0:1",
            "sessionId": "DD3AB1F0DF034789B554F8603AEC0C2E"
        },
        "authenticated": true,
        "principal": {
            "password": null,
            "username": "john",
            "authorities": [
                {
                    "authority": "ROLE_USER"
                }
            ],
            "accountNonExpired": true,
            "accountNonLocked": true,
            "credentialsNonExpired": true,
            "enabled": true
        },
        "credentials": null,
        "name": "john"
    },
    "credentials": "",
    "clientOnly": false,
    "principal": {
        "password": null,
        "username": "john",
        "authorities": [
            {
                "authority": "ROLE_USER"
            }
        ],
        "accountNonExpired": true,
        "accountNonLocked": true,
        "credentialsNonExpired": true,
        "enabled": true
    },
    "oauth2Request": {
        "clientId": "SampleClientId",
        "scope": [
            "user_info"
        ],
        "requestParameters": {
            "code": "5heUsD",
            "grant_type": "authorization_code",
            "scope": "user_info",
            "response_type": "code",
            "redirect_uri": "http://localhost:8082/ui/login",
            "client_secret": "secret",
            "client_id": "SampleClientId"
        },
        "resourceIds": [],
        "authorities": [],
        "approved": true,
        "refresh": false,
        "redirectUri": "http://localhost:8082/ui/login",
        "responseTypes": [
            "code"
        ],

        "extensions": {},
        "grantType": "authorization_code",
        "refreshTokenRequest": null
    },
    "name": "john"
}

结果附图:

12、oauth授权认证流程_第8张图片

12、oauth授权认证流程_第9张图片

access_token 有效期 与 refresh_token

    在 OAuth 2.0 中,access_token 不再长期有效。在授权获取 access_token 时会一并返回其有效期,也就是返回值中的 expires_in 参数。在 access_token 使用过程中,如果服务器返回106错误:“access_token_has_expired ”,此时,说明 access_token 已经过期,除了通过再次引导用户进行授权来获取 access_token 外,还可以通过 refresh_token 的方式来换取新的 access_token 和 refresh_token

通过 refresh_token 换取 access_token 的处理过程如下:

请求地址:http://localhost:8081/auth/oauth/token

请求参数:

参数名称

参数说明

client_id

必选参数,应用的唯一标识,对应于 APIKey

client_secret

必选参数,应用的唯一标识,对应于豆瓣 secret

redirect_uri

必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。此地址必须与在应用注册时填写的回调地址一致

grant_type

必选参数,此值可以为 authorization_code 或者 refresh_token。在本流程中,此值为 refresh_token

refresh_token

必选参数,刷新令牌

注意:此请求必须是 HTTP POST 方式,refresh_token 只有在 access_token 过期时才能使用,并且只能使用一次。当换取到的 access_token 再次过期时,使用新的 refresh_token 来换取 access_token

例如:

http://localhost:8081/auth/oauth/token?
client_id=SampleClientId&
client_secret=secret&
redirect_uri=http://localhost:8082/ui/login&
grant_type=refresh_token&
refresh_token=5d633d136b6d56a41829b73a424803ec

返回结果:

{
"access_token":"0e63c03dfb66c4172b2b40b9f2344c45",
"expires_in":3920,
"refresh_token":"84406d40cc58e0ae8cc147c2650aa20a",
"douban_user_id":"1000"
}

token级别如下说明如下:

级别

access_token 有效期

refresh_token 有效期

说明

L1

7天

14天

 

L2

30天

60天

 

L3

90天

180天

 

需要授权的 API 访问速度控制

在用户、应用、服务器 IP、scope 等维度对接口的访问速度进行限制。

针对服务器IP:

级别

限制

L1

5000次/小时

L2

10000次/小时

L3

20000次/小时

针对单用户每应用每 scope:

级别

限制

L1

500次/小时

L2

1000次/小时

L3

2000次/小时

返回结果的 header 里会有当前访问限制信息:

Header名称

含义

X-Ratelimit-Limit

单用户每小时次数

X-RateLimit-Remaining

单用户每小时剩余次数

X-Ratelimit-Limit2

单ip每小时次数

X-RateLimit-Remaining2

单ip每小时剩余次数

错误代码

如果在 API 使用过程中,有错误,则返回结果为:

{

"code":113,

"msg":"required_parameter_is_missing: client_id",

"request":"GET /shuo/statuses/232323"

}

错误代码

错误说明

100

invalid_request_scheme 错误的请求协议

101

invalid_request_method 错误的请求方法

102

access_token_is_missing 未找到 access_token

103

invalid_access_token access_token 不存在或已被用户删除,或者用户修改了密码

104

invalid_apikey apikey 不存在或已删除

105

apikey_is_blocked apikey 已被禁用

106

access_token_has_expired access_token 已过期

107

invalid_request_uri 请求地址未注册

108

invalid_credencial1 用户未授权访问此数据

109

invalid_credencial2 apikey 未申请此权限

110

not_trial_user 未注册的测试用户

111

rate_limit_exceeded1 用户访问速度限制

112

rate_limit_exceeded2 IP 访问速度限制

113

required_parameter_is_missing 缺少参数

114

unsupported_grant_type 错误的 grant_type

115

unsupported_response_type 错误的 response_type

116

client_secret_mismatch client_secret不匹配

117

redirect_uri_mismatch redirect_uri不匹配

118

invalid_authorization_code authorization_code 不存在或已过期

119

invalid_refresh_token refresh_token 不存在或已过期

120

username_password_mismatch 用户名密码不匹配

121

invalid_user 用户不存在或已删除

122

user_has_blocked 用户已被屏蔽

123

access_token_has_expired_since_password_changed 因用户修改密码而导致 access_token 过期

124

access_token_has_not_expired access_token 未过期

125

invalid_request_scope 访问的 scope 不合法,开发者不用太关注,一般不会出现该错误

126

invalid_request_source 访问来源不合法

127

thirdparty_login_auth_faied 第三方授权错误

128

user_locked 用户被锁定

999

unknown 未知错误

 

HTTP状态码

说明

200

表明 API 的请求正常

400

表明 API 的请求出错,具体原因参考上面列出的错误码

到此,系统SSO认证已经完成。如下附加上授权服务、网络安全配置代码:

package com.easystudy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    
    @Autowired    
    private BCryptPasswordEncoder passwordEncoder;

    
    /**
     * 用postman使用post方式向/oauth/token或/oauth/accessToken这个接口获取access_token的时候报错401
     * 允许以form形式提交表单信息
     */
    @Override
    public void configure(final AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
        oauthServer
            .tokenKeyAccess("permitAll()")
            .checkTokenAccess("isAuthenticated()")
            .allowFormAuthenticationForClients();
    }


    /**
     * 授权码换取(第一步校验):
     * 1、第三方应用通过client_id,response_typey以及redirect_uri获取对应的授权码,
     * 2、然后通过授权码在换取access_token(此时就需要用到client_secret、scope以及grant_type(authorization_code)、redirect_uri
     */
    @Override
    public void configure(final ClientDetailsServiceConfigurer clients) throws Exception {
// 注意:我们直接把账号密码写到内存里了,真正使用的时候这里要换成自定义的userDetailsService
// secret密码配置从 Spring Security 5.0开始必须以 {bcrypt}+加密后的密码 这种格式填写
        clients.inMemory()                                                          // 内存模式认证
            .withClient("SampleClientId")                                           // client_id
            .secret(passwordEncoder.encode("secret"))                               // client_secret
            .authorizedGrantTypes("authorization_code","refresh_token")             // grant_type,可以填写多个授权类型,可以通过refresh_token更换过期token
            .scopes("user_info")                                                    // 申请的权限范围scope-用于获取用户信息
            .autoApprove(true)                                                      // 必选参数,用户授权完成后的回调地址,应用需要通过此回调地址获得用户的授权结果。
                                                                                  
//此地址必须与在应用注册时填写的回调地址一致,注意:该地址是第三方应用注册的地址,此处写死
.redirectUris("http://localhost:8082/ui/login","http://localhost:8083/ui2/login");
         // .accessTokenValiditySeconds(3600)        // 1 hour
    }

}

URL拦截代码:

package com.easystudy.config;
import org.springframework.context.annotation.Bean;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

/**
 * URL拦截配置
 * 实现功能(作用):
 * 1、要求用户在进入你的应用的任何URL之前都进行验证
 * 2、创建一个用户名是“john”,密码是“123”,角色是“ROLE_USER”的用户【自动添加了“ROLE_”前缀】
 * 3、启用HTTP Basic和基于表单的验证
 * 4、Spring Security将会自动生成一个登陆页面和登出成功页面
 */
@Order(1)            // 使用注解方式使bean的加载顺序得到控制,配置改类被加载的顺序,优先级为1
@EnableWebSecurity   // 包含@Configuration,@EnableWebSecurity注解以及WebSecurityConfigurerAdapter一起配合提供基于web的security
public class SecurityConfig extends WebSecurityConfigurerAdapter{
     /**
      * Override this method to configure the HttpSecurity. Typically subclasses should not invoke this method by calling super as it may override their configuration. The default configuration is:
      * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
      */
      @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.requestMatchers()
              .antMatchers("/login", "/oauth/authorize", "/oauth/accessToken", "/auth/oauth/token")      // 这些页面不需要授权
              .and()
              .authorizeRequests()        .anyRequest()                                                                                                          // 其他所有页面必须授权
              .authenticated()
              .and()
.formLogin()                                                                                                      // 允许表单登录-生成表单登录页面
              .permitAll();
    }

     /**
      * 1、创建用户
      * 2、验证用户
      */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {    
     // 内存中创建[验证]一个用户名为john,密码为123的用户,第三方应用登录的时候必须指定改用户名和密码通过认证后才发放code授权码
     // 第三方应用凭借用户授权(使用用户名和密码)获取的授权码换取access_token然后通过access_token获取用户信息
     // 角色名会默认被添加上"ROLE_"前缀,如下USER角色,实际的名称为ROLE_USER
     auth.inMemoryAuthentication()   
             .withUser("john")                                       // 创建的用户名
             .password(passwordEncoder().encode("123"))        // 验证的用户密码
             .roles("USER")                                               // 改用户的角色
             .and()                                                       // 级联
             .withUser("lixx")                                       // 创建用户lixx
             .password(passwordEncoder().encode("dw123456"))     // 用户密码
             .roles("ADMIN", "USER");                               // 用户角色为ROLE_ADMIN和ROLE_USER有两种角色

    }


    /**
     * 拦截URL,设置忽略安全校验的静态资源拦截
     * Override this method to configure WebSecurity. For example, if you wish to ignore certain requests.
     */
    @Override
     public void configure(WebSecurity web) throws Exception {
         web
        .ignoring()                              // 忽略如下请求
        .antMatchers("/resources/**");           // 忽略以/resources/打头的请求
     }

     /**
     * 创建加密对象,对密码进行加密
     * @return
     */
    @Bean
    public BCryptPasswordEncoder passwordEncoder(){
        return new BCryptPasswordEncoder();
    }
}

快来成为我的朋友或合作伙伴,一起交流,一起进步!
QQ群:961179337
微信:lixiang6153
邮箱:[email protected]
公众号:IT技术快餐
更多资料等你来拿!

 

你可能感兴趣的:(SpringCloud,oauth2)