1、一种是全部利用配置文件,将用户、权限、资源(url)硬编码在xml文件中;
2、 二种是用户和权限用数据库存储,而资源(url)和权限的对应采用硬编码配置。
3、三种是细分角色和权限,并将用户、角色、权限和资源均采用数据库存储,并且自定义过滤器,代替原有的FilterSecurityInterceptor过滤器 并分别实现AccessDecisionManager、InvocationSecurityMetadataSourceService和UserDetailsService,并在配置文件中进行相应配置。
4、四是修改spring security的源代码,主要是修改InvocationSecurityMetadataSourceService和UserDetailsService两个类。 前者是将配置文件 或数据库中存储的资源(url)提取出来加工成为url和权限列表的Map供Security使用,后者提取用户名和权限组成一个完整的(UserDetails)User 对象,该对象可以提供用户的详细信息供AuthentationManager进行认证与授权使用。
这里,我们先介绍第一种,见用户、权限、资源硬编码在代码中,后续文章,由浅入深实现后面几种情况;通过如下图,了解spring security4 的工作机制:
OAuth2.0是2006年开始设计OAuth协议的下一个版本,OAuth2.0同时提供Web,桌面和移动应用程序的支持,并较1.0相比整个授权验证流程更简单更安全。目前国内百度开发平台、腾讯开放平台、新浪微博开发平台的认证都是使用该协议。
Oauth2.0协议相当于在第三方应用程序与用户资源提供商之间,设置了一个授权层(authorization server)。第三方应用不能直接登录用户资源提供商,只能登录到授权层,以此将用户的信息和资源与第三方应用分割开来。用户授权第三方应用以后,第三方应用拿到资源令牌,相当于有了用户获取资源的权限。但是却没有用户的用户名与密码,用户可以在登录的时候,指定资源令牌的权限范围和有效期。
对于研发和测试人员来说、平时经常需要登录公司或者部门内部的各个平台进行相关的工作,比如:持续集成、工单系统、RMD(项目管理系统)等等。如果挨个平台输入用户名与密码进行登录,是非常繁琐的同时又有很大的安全隐患。为了解决这个问题,我们可以搭建统一的认证授权平台。这样只要用户登录了授权平台就可以免登陆进入授权平台接入的各个子系统。
所以单点登录解决了多系统中间切换的以下问题:
A、免登陆跳转、解决了流程繁琐的问题。
B、无需在各个平台中暴露用户名、密码,解决了安全的问题。
C、用户没法控制各个平台获取的用户信息的范围与时限的问题。
而Oauth2.0协议就能很好的解决这样的一些问题。
第一步: 搭建基于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
3、用户通过用户名和密码验证后,授权第三方应用获取用户资料, 此时从认证服务器302跳转回以前的第三方应用,同时附带上授权码。第三方应用得到了认证服务器的授权码
http://localhost:8082/ui/login?code=5heUsD
浏览器重定向到地址:http://localhost:8082/ui/login?code=5heUsD,目的是将获取到的code传送给第三方应用,所以给定的第三方应用地址能够接收获取改code授权码即可(获取到授权码然后才有下一步用授权码code换取code)
3、用户通过用户名和密码验证后,授权第三方应用获取用户资料, 此时从认证服务器302跳转回以前的第三方应用,同时附带上授权码。第三方应用得到了认证服务器的授权码
http://localhost:8082/ui/login?code=5heUsD
浏览器重定向到地址:http://localhost:8082/ui/login?code=5heUsD,目的是将获取到的code传送给第三方应用,所以给定的第三方应用地址能够接收获取改code授权码即可(获取到授权码然后才有下一步用授权码code换取code)
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 |
授权码 |
|
具体参数如下:
发送获取的结果:
{
"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"
}
结果附图:
在 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天 |
|
在用户、应用、服务器 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技术快餐
更多资料等你来拿!