S p r i n g B o o t O a u t h 2 认 证 文 档 SpringBoot Oauth2认证文档 SpringBootOauth2认证文档
OAuth是一个关于授权(authorization)的开放网络标准,在全世界得到广泛应用,目前的版本是2.0版。本文根据RFC6749
认证服务器(Authorization server): 认证服务器,即服务提供商专门用来处理认证的服务器。
资源服务器(Resource server):资源服务器,即服务提供商存放用户生成的资源的服务器。
资源所有者(Resource Owner):资源所有者,本文中又称"用户"(user)。
第三方客户端(Third-party application):第三方应用程序,本文中又称"客户端"(client)。
###oauth2思路
OAuth在"客户端"与"服务提供商"之间,设置了一个授权层(authorization layer)。“客户端"不能直接登录"服务提供商”,只能登录授权层,以此将用户与客户端区分开来。"客户端"登录授权层所用的令牌(access_token),与用户的密码不同。用户可以在登录的时候,指定授权层令牌的权限范围。
"客户端"登录授权层以后,"服务提供商"根据令牌的权限范围和有效期,向"客户端"开放用户储存的资料。
oauth2 协议总共定义了四种授权模式,本文详细介绍授权码模式和密码模式的使用。
1、授权码模式
2、密码模式
3、简易模式
4、客户端模式
授权码模式(authorization code)是功能最完整、流程最严密的授权模式。它的特点就是通过客户端的后台服务器,与认证服务器进行互动。
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
(A)步骤中,客户端申请认证,包含以下参数
例子:GET http://192.168.1.168:9867/as/oauth/authorize?response_type=code&client_id=xiaobin&redirect_uri=https://www.baidu.com
参数 | 备注 |
---|---|
response_type | 表示授权类型,必选项,此处的值固定为"code" |
client_id | 表示客户端的ID,必选项 |
redirect_uri | 表示重定向URI,可选项 |
scope | 表示申请的权限范围,可选项 |
state | 表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值 |
©步骤中,回调地址后面附带参数
例子:https://www.baidu.com/?code=V2t9MY
参数 | 备注 |
---|---|
code | 授权码,授权码通常为10分钟,而且只能使用一次,和回调地址URL对应关系 |
state | 如果客户端的请求中包含这个参数,认证服务器的回应也必须一模一样包含这个参数 |
(D)步骤中,客户端申请令牌,包含以下参数
例子:POST http://192.168.1.168:9867/as/oauth/token?grant_type=authorization_code&client_id=xiaobin&code=V2t9MY&client_secret=123456
参数 | 备注 |
---|---|
grant_type | 表示使用的授权模式,必选项,此处的值固定为"authorization_code" |
code | 表示上一步获得的授权码,必选项 |
redirect_uri | 表示重定向URI,必选项,且必须与A步骤中的该参数值保持一致。 |
client_id | 表示客户端ID,必选项。 |
client_secret | 客户端secret,必选项 |
(E)步骤中,认证服务器返回的消息,包含以下参数
{
“access_token”: “57fb4a8b-2158-4464-85c2-8e4bcb1eb672”,
“token_type”: “bearer”,
“refresh_token”: “86779eb4-5e70-4d10-9c20-2f5599da81f9”,
“expires_in”: 43157,
“scope”: “user”
}
参数 | 备注 |
---|---|
access_token | 访问令牌 |
token_type | 令牌类型,该值大小写不敏感 |
expires_in | 过期时间,单位秒,认证服务器可以进行设置过期时间 |
refresh_token | 更新令牌,用来获取下一次的访问令牌 |
scope | 授权的scope权限范围 |
密码模式(Resource Owner Password Credentials Grant)中,用户向客户端提供自己的用户名、密码、客户端ID、客户端secret,通常用在用户对客户端高度信任的情况下,一般在公司内部产品使用。
(A)用户向客户端提供用户名和密码。
(B)客户端将用户名、密码、客户端ID和客户端secret发给认证服务器,向后者请求令牌。
(C)认证服务器确认无误后,向客户端提供访问令牌。
(B)步骤中,客户端申请令牌,包含以下参数
例子:POST http://192.168.1.168:9867/as/oauth/token?grant_type=password&username=admin&password=admin&client_id=xiaobin&client_secret=123456
参数 | 备注 |
---|---|
grant_type | 表示授权类型,此处的值固定为"password",必选项。 |
username | 表示用户名,必选项。 |
password | 表示用户的密码,必选项。 |
scope | 可选项,不填获取全部的scope权限范围 |
client_id | 表示客户端ID,必选项。 |
client_secret | 客户端secret,必选项 |
(B)步骤中,发起刷新token请求,包含以下参数
例子:POST http://192.168.1.168:9867/as/oauth/token?grant_type=refresh_token&refresh_token=0280a488-ca85-4873-a703-d2e6b8adb418
参数 | 备注 |
---|---|
grant_type | 表示授权类型,此处的值固定为"refresh_token",必选项。 |
refresh_token | 表示早前收到的更新令牌,必选项。 |
scope | 表示申请的授权范围,不可以超出上一次申请的范围,如果省略该参数,则表示与上一次一致。 |
1、第三方的客户端需要获取到本公司内部服务,需要遵循oauth2授权码模式接入认证申请。
2、第三方客户端访问本公司内部服务时,请求头使用http authorization头提供token。
3、受信任的客户端使用密码模式,访问内部服务请求头使用http authorization头提供token。
4、token中包含scope范围列表,authorities权限列表。
5、当访问资源服务时,scope授权范围和authorities权限范围不足是不能够进行访问的。
1、spring security oauth2 通过注解@PreAuthorize进行authorities权限和scope权限拦截,该注解使用在方法上面。
2、通过自定义注解@ScopeDetail,注解使用在方法上面有效,用于描述scope信息。
3、项目编译时,通过注解处理器把@ScopeDetails自定义注解中的scope信息转换为执行的sql语句。
数据表设计
client_info表,存储oauth2客户端认证信息
字段名 | 类型 | 描述 |
---|---|---|
client_id | varchar | 客户端ID |
client_secret | varchar | 客户端密码 |
authorized_grant_types | varchar | 授权类型 |
redirect_uri | varchar | 回调URL |
access_token_validity | int | token过期时间 |
refresh_token_validity | int | 刷新token过期时间 |
CREATE TABLE `client_info` (
`client_id` varchar(60) NOT NULL COMMENT '客户端ID ',
`client_secret` varchar(256) NOT NULL COMMENT '客户端密码',
`authorized_grant_types` varchar(256) NOT NULL COMMENT '授权类型',
`redirect_uri` varchar(256) NULL DEFAULT NULL COMMENT '回调URL ',
`access_token_validity` int(11) NULL DEFAULT NULL COMMENT 'token过期时间',
`refresh_token_validity` int(11) NULL DEFAULT NULL COMMENT '刷新token过期时间',
PRIMARY KEY (`client_id`)
)
t_client_scope表,存储客户端和scope权限列表中间表
字段名 | 类型 | 描述 |
---|---|---|
id | int | 主键自增ID |
client_id | varchar | 客户端ID |
scope_id | int | scope主键ID |
CREATE TABLE `t_client_scope` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`client_id` varchar(60) NOT NULL DEFAULT NULL COMMENT '客户端ID',
`scope_id` int(11) NOT NULL DEFAULT NULL COMMENT 'scope主键ID',
PRIMARY KEY (`id`)
)
t_scope表,存储scope权限
字段名 | 类型 | 描述 |
---|---|---|
scope_id | int | scope自增ID |
scope | varchar | scope权限名 |
service_id | int | 服务模块ID |
desc | varchar | 详细描述 |
scope | varchar | scope权限名 |
CREATE TABLE `t_scope` (
`scope_id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'scope自增ID',
`scope` varchar(256) NOT NULL COMMENT 'scope权限名',
`service_id` int(11) NOT NULL COMMENT '服务模块ID',
`desc` varchar(256) NULL DEFAULT NULL COMMENT '详细描述',
`name` varchar(255) NULL DEFAULT NULL COMMENT '描述名',
PRIMARY KEY (`scope_id`)
)
SQL语句根据注解处理器处理@PreAuthorize和@ScopeDetails两个注解,使用在方法上面,获取到指定scope信息
注解形式 | 描述 |
---|---|
@PreAuthorize("#oauth2.hasScope(‘scope-test’)") | 判断scope范围是否有scope-test权限 |
@PreAuthorize("(#oauth2.hasScope(‘scope-test’) and hasAuthority(‘user.read’)) or hasAuthority(‘scope-test2’)") | 判断是否有scope-test的scope并且是否有device.read权限或者有scope-test2的scope权限 |
生成的SQL语句会将重复的scope给去除,这里只有一个scope-test
@ScopeDetails({
@ScopeDetail(name="描述名",desc = "详细描述",scope = "scope-test"),
@ScopeDetail(name="描述名",desc = "详细描述",scope = "scope-test")
})
生成的SQL语句会将重复的scope给去除,下面例子scope只有scope-test和scope-test2,@PreAuthorize注解和
@ScopeDetail注解相同scope会去除,优先取@ScopeDetail。#oauth2.hasScope中scope没有描述信息。
@PreAuthorize("#oauth2.hasScope('scope-test') or #oauth2.hasScope('scope-test2')")
@ScopeDetails({
@ScopeDetail(name="描述名",desc = "详细描述",scope = "scope-test"),
@ScopeDetail(name="描述名",desc = "详细描述",scope = "scope-test")
})
SpringBoot 的版本为 2.1.4.RELEASE,使用的Spring security 5以后,密码都是以加密形式存储,读取形式为{bcrypt}password:bcrypt加密方式官方推荐,详情Security学习。配置Spring Security认证服务器,需要引入Spring Security的两个依赖
org.springframework.boot
spring-boot-starter-security
org.springframework.security.oauth.boot
spring-security-oauth2-autoconfigure
2.1.0.RELEASE
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfigurer extends AuthorizationServerConfigurerAdapter {
/**
* 数据源
*/
@Resource
private DataSource dataSource;
/**
* TUserService 实现 UserDetailsService 接口,获取用户信息
*/
@Autowired
private TUserService userService;
/**
* 查询自定义数据表加载客户端信息
*/
@Autowired
private ClientDetailsService clientDetailsService;
/**
* 注入authenticationManager
* 来支持 password grant type
*/
@Autowired
private AuthenticationManager authenticationManager;
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
//开放oauth2的授权端点
//允许表单认证
security.allowFormAuthenticationForClients();
//允许check_token访问
security.checkTokenAccess("permitAll()");
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
// 没有它,在使用refresh_token的时候会报错 IllegalStateException,UserDetailsService is required.
endpoints.userDetailsService(userService)
//来支持 password grant type
.authenticationManager(authenticationManager)
// 不加报错"method_not_allowed"
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(ClientDetailsServiceConfigurer clients)
throws Exception {
//使用自定义的ClientDetailsService
clients.withClientDetails(clientDetailsService);
}
}
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
/**
* TUserService 实现 UserDetailsService 接口,获取用户信息
*/
@Autowired
private TUserService userService;
/**
* 将AuthenticationManager注入到Spring容器中,认证服务器配置需要用到
* @return
* @throws Exception
*/
@Bean
@Override
protected AuthenticationManager authenticationManager() throws Exception {
return super.authenticationManager();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//使用UserDetailsService,密码使用的是BCryptPasswordEncoder加密
auth.userDetailsService(userService).passwordEncoder(new BCryptPasswordEncoder());
}
/**
* security的拦截路径,使用表单认证,并且拦截所有请求
* @param http
* @throws Exception
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.formLogin().and()
.authorizeRequests().anyRequest().authenticated();
// Security的默认拦截器里,默认会开启CSRF处理,判断请求是否携带了token
http .csrf().disable();
}
}
/**
* 加载Bean到Spring容器中,自定义ClientDetailsService
* 指定的clientID从数据库中查询是否存在,加载客户端的一些配置信息
* @param oauth2Service 查询数据的Service
* @return
*/
@Bean
public ClientDetailsService clientDetailsService(ClientInfoService oauth2Service) {
return clientId -> {
//通过clientId查询客户端信息
List clients1 = oauth2Service.selectByClientId(clientId);
if (clients1 == null || clients1.size() == 0) {
//返回的错误信息
throw new ClientRegistrationException("clientId无效");
}
// client信息实体类 ClientInfo
ClientInfo client = clients1.get(0);
BaseClientDetails clientDetails = new BaseClientDetails();
//设置clientID
clientDetails.setClientId(client.getClienId());
//设置clientSecret
clientDetails.setClientSecret(client.getClientSecret());
//设置多个跳转的URL地址
clientDetails.setRegisteredRedirectUri(new HashSet<>(Arrays.asList(client.getRedirectUri().split(","))));
//设置授权类型
clientDetails.setAuthorizedGrantTypes(Arrays.asList(client.getAuthorizedGrantTypes().split(",")));
List list = new ArrayList<>();
for (ClientScope scope : client.getScopes()) {
list.add(scope.getScope());
}
//设置scope范围列表
clientDetails.setScope(list);
return clientDetails;
};
}
@Configuration
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CORSFilter implements Filter {
private FilterConfig config;
@Override
public void destroy() {
}
/**
* 解决oauth2认证跨域
* @param req
* @param resp
* @param chain
* @throws IOException
* @throws ServletException
*/
@Override
public void doFilter(ServletRequest req, ServletResponse resp,
FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) resp;
HttpServletRequest request = (HttpServletRequest) req;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "x-requested-with, authorization, Content-Type, Authorization, credential, X-XSRF-TOKEN");
if ("OPTIONS".equalsIgnoreCase(request.getMethod())) {
response.setStatus(HttpServletResponse.SC_OK);
} else {
chain.doFilter(req, resp);
}
}
@Override
public void init(FilterConfig filterConfig) throws ServletException {
config = filterConfig;
}
}