SpringBoot使用security实现OAuth2
OAuth2
OAuth
是一个开放标准,允许用户授权地方应用访问他们存储在另外的服务提供者上的信息,而不需要将用户名和密码提供给第三方应用或者分享他们数据的所有内容。
我们从一个常见的例子来看:
我们打王者,第一次登录的时候要求我们选择微信登录还是QQ
登录,这时假设我们点击QQ
登录,那么就会跳转到一个认证界面,询问我们是否同意王者使用QQ
的数据,例如好友列表等等。当我们点击同意之后就会跳转回王者,之后进入王者我们可以发现好友列表内容就是我们的QQ
中的好友列表。并且过段时间不登录之后,我们会发现又要再次认证,这是因为之前的认证令牌过期了,需要重新申请。
这就是OAuth
的一个大概思路流程
- 客户端要求用户给予授权
- 用户同意授权
- 客户端通过获得授权向认证服务器申请
token
- 认证服务器对客户端进行认证,通过认证之后发放
token
- 客户端可以通过
token
向资源服务器申请资源 - 资源服务器确认通过之后,向客户端开放资源
OAuth2
又分为四种不同的授权模式(简化模式和密码模式由于安全度太低已被遗弃):
- 授权码模式:这是最常用也是安全度最高的一种模式,通过获取授权码增加安全性
简化模式:直接申请令牌并且返回令牌,主要用于申请端只有前端没有服务器的情况,例如微信小程序密码模式:需要用户提供账号密码,通过账号密码获取token
,这种模式安全度极低,不推荐- 客户端模式:与用户无关的一种模式,直接是服务器之间的通信,例如内部系统间的
API
调用
接下来主要讲讲授权码这个核心授权模式
授权码模式
授权码模式authorization code
,指的是客户端首先向认证服务器申请一个授权码,然后通过该授权码再去向授权服务器申请token
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
步骤如下:
A 用户访问客户端,客户端将用户导向认证服务器,并且携带重定向URI
https://authorization-server.com/auth?
response_type=code
&client_id=CLIENT_ID
&redirect_uri=REDIRECT_URI
&scope=photos
&state=1234zyx
&code_challenge=CODE_CHALLENGE
&code_challenge_method=S256
response_type=code
表示授权类型为授权码模式
client_id
表示客户端ID
,第一次创建应用的时候获得
redirect_uri
表示重定向URI
用户在认证完成之后将用户返回到特定URI
scope
表示申请的权限范围,例如READ
state
应用随机指定的值,用于后期验证
code_challenge
code_challenge=transform(code_verifier,[Plain|S256])
如果method=Plain
,那么code-challenge=code_verifier
如果method=S256
,那么code_challenge
等于code_verifier
的Sha256
哈希
在授权码请求中带上code_challenge
以及method
,这两者与服务器颁发的授权码绑定。
code_verifier
为客户端生成一个的随机字符串
客户端在用授权码换取token
时,带上初始生成的code verifier
,根据绑定的方法进行计算,计算结果与code_challenge
相比,如果一致再颁发token
code_challenge_method=S256
标明使用S256 Hashing
方法
B 用户选择是否对客户端授权
C 授权之后,认证服务器将用户导向之前传入的重定向URI
,并且附上授权码
如果用户点击了Allow
了,那么服务器将重定向并且附上授权码
https://example-app.com/cb?code=AUTH_CODE_HERE&state=1234zyx
code
即为授权码,授权码有效期很短,一般为10分钟,并且客户端只能使用一次。该码与客户端ID
和重定向URI
是一对一关系
state
之前传入的state
我们首先要比较传入的state
与之前的state
是否相同(之前的state
可以存在cookie
中),用于确认没有被劫持。
D 客户端收到授权码后,附上重定向URI
以及授权码,向认证服务器申请token
(这一步是在客户端的后台服务器上完成,对用户不可见)
客户端向认证服务器发送申请token
的HTTP
请求
POST https://api.authorization-server.com/token
grant_type=authorization_code&
code=AUTH_CODE_HERE&
redirect_uri=REDIRECT_URI&
client_id=CLIENT_ID&
code_verifier=CODE_VERIFIER
grant_tyoe
标明为授权码模式
code
之前收到的授权码
redirect_uri
重定向URI
,必须与一开始发送的重定向URI
一样
client_id
客户端ID
,也必须和之前发送的一样
code_verifier
之前随机生成的字符串,服务器根据之前传入的code-challenge
的method
进行计算,看是否以之前传入的code_challenge
相同,相同才会颁发token
E 认证服务器认证授权码等信息,确认无误后向客户端发送token
和refresh token
(可选)
通过认证后,服务器发送包含token
的HTTPResponse
{
"access_token":"2YotnFZFEjr1zCsicMWpAA",
"token_type":"bear",
"expires_in":3600,
"refresh_token":"tGzv3JOkF0XG5Qx2TlKWIA",
}
access_token
表示访问令牌
token_type
表示token
类型,可以是bear
也可以是mac
expires_in
表示过期时间,单位为秒
refresh_token
表示更新令牌,用来获取下次的访问令牌。即当token
过期的时候,向服务器发送请求,告知token
过期并且将token
更新为refresh_token
中的值
SpringBoot +springsecurity 实现OAuth2
springsecurity
实现OAuth2
分为两个服务,Authorization Server
和Resource Server
分别作为授权服务器和资源服务器
Authorization Server Configuration
正如上面OAuth2
流程中提到,授权服务器主要作用便是验证客户端,拉起授权页面,用户授权之后通过重定向URI
携带授权码返回,之后根据授权码验证客户端,发放令牌Access Token
springboot
中,我们在配置类上加上@EnableAuthorizationServer
并且实现AuthorizationServerConfigurer
也可以直接继承
springsecurity
提供的AuthorizationServerConfigurerAdapter
@Configuration
@EnableAuthorizationServer
public class MyAuthorizationConfig extends AuthorizationServerConfigurerAdapter
配置类中,我们可以通过复写三个不同的configure
完成对于授权服务器的所有配置
ClientDetailsServiceConfigurer
配置客户端信息
@Override
public void configure(ClientDetailsServiceConfigurer clients)throws Exception {
// 采用内存模式,也可以使用数据库模式
clients.inMemory()
// 设置client_id
.withClient("client-a")
// 设置client_secret
.secret(passwordEncoder.encode(("client-a-secret")))
// 设置授权模式
.authorizedGrantTypes("authorization_code")
// 设置权限
.scopes("read")
// 设置当前client可以访问的资源ID
.resourceIds("resource1")
//自动授权,无需人工点击
// .autoApprove(true)
// 重定向URI
.redirectUris("http://localhost:9000/callback");
}
我们这里采用的是内存模式配置客户端,也可以通过JDBC
连接数据库配置客户端信息
withClient
: 配置clientId
,授权不仅仅对用户授权,还要对客户端授权,例如我信任谷歌客户端,不信任百度客户端。于是通过clientId
和clientSecret
进行验证
secret
:配置clientSecret
authorizedGrantTypes
:配置授权模式
scopes
:配置权限,默认是所有权限
resourceIds
:配置该client
具有的资源服务器,每个资源服务器都有一个唯一的资源ID
,如果客户端访问没有授权的资源服务器会提示没有权限
autoApprove
:配置是否自动授权,设置true
即为开启
redirectUris
:配置重定向URI
AuthorizationServerSecurityConfigurer
配置token endpoint
的安全约束,即提供一些安全访问规则和过滤器
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
// 允许表单认证
// 对于请求/oauth/token的,如果配置了支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的会走ClientCredentialsTokenEndpointFilter
security.allowFormAuthenticationForClients()
// 开启/oauth/check_token 验证端口认证权限访问
.checkTokenAccess("isAuthenticated()");
}
对于类似/oauth/check_token
或者/oauth/token_key
这些端点默认是关闭的即"denyAll()"
所以说如果要使用这些端点,我们就要对具有某些权限的用户开启
allowFormAuthenticationForClients()
:开启表单认证,对于端口/oauth/token
,如果开启此配置,并且url
中有client_id
和client_secret
会触发ClientCredentialsTokenEndpointFilter
用于校验客户端是否有权限
checkTokenAccess
:开启端口/oauth/check_token
,用于资源服务器的将获取的token
进行验证
isAuthenticated()
标明访问用户是通过验证的,类似permitAll
或者hasAuthority()
等等
addTokenEndpointAuthenticationFilter(IntegrationAuthenticationFilter)
:添加过滤去,可以实现自定义认证,例如短信认证等等
tokenKeyAccess()
:开启/oauth/token_key
端口
AuthorizationServerEndpointConfigurer
springsecurity-oauth2
默认提供以下端口
/oauth/authorize
:授权端口
/oauth/token
:令牌端口
/oauth/confirm_access
:用户确认授权提交端口
/oauth/error
:授权服务错误信息端口
/oauth/check_token
:用于资源服务器访问的令牌解析端口
/oauth/token_key
:提供公有秘钥端口,如果使用的是JWT
令牌的话
AuthorizationServerEndpointConfigurer
提供了一个方法可以配置自定义的端口URL
链接
pathMappring(String 该端口默认URL,String 想要替代的URL)
AuthorizationServerEndPointsConfigurer
其实是一个装载类,装载Endpoints
所有相关的类配置(TokenStore
,TokenService
,UserDetailsService
等等)
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)throws Exception{
endpoints
// 配置tokenstore,默认存在内存里
.tokenStore(new InMemoryTokenStore())
// 添加authenticationManager用于密码授权方式
// .authenticationManager()
// 不添加无法使用refresh_token
// .userDetailsService()
.allowedTokenEndpointRequestMethods(HttpMethod.POST,HttpMethod.GET);
}
}
tokenStoren
:配置token
存储的位置,默认是存储在内存中,也可以存储在Redis
等数据库中
authenticationManager
:用于配置密码授权方式
userDetailsService
:配置用于使用refresh_token
allowedTokenEndpointRequestMethods
:配置TokenEndpoint
允许请求方式
SecurityConfig
因为只是demo
项目,所以将授权用户存储在内存中
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
// 设置用户名密码存在内存中
auth.inMemoryAuthentication()
.withUser("cwh")
.password(passwordEncoder().encode("12345"))
.authorities(Collections.emptyList());
}
Resource Server Configuration
资源服务器提供一些受token
令牌保护的资源
资源服务器可以和授权服务器在同一个应用中,也可以是分开为两个不同的应用
springboot
中,我们在配置类上加上@EnableResourceServer
并且实现ResourceServerConfigurer
也可以直接继承
springsecurity
提供的ResourceServerConfigurerAdapter
@Configuration
@EnableResourceServer
public class ResourceConfig extends ResourceServerConfigurerAdapter {
ResourceServerConfigurerAdapter
内部关联了ResourceServerSecurityConfigurer
和HttpSecurity
可以配置以下功能:
-
tokenServices
:定义令牌服务的Bean
(ResourceServerTokenService
的实例) -
resourceId
:资源id
- 资源服务器的其他扩展点(例如
tokenExtractor
用于从传入请求中获取令牌) - 请求受保护资源的匹配器 (默认为全部)
- 受保护资源的访问规则(默认为普通的
authenticated
) -
Spring Security
中HttpSecurity
配置器所允许的受保护资源的其他自定义情况
这里我们简单配置以下reourceId
和tokenServices
ResourceServerSecurityConfigurer
用于资源服务器的配置
@Override
public void configure(ResourceServerSecurityConfigurer resources){
// resourceID: 规定的资源ID
// stateless:表示是否只允许基于token的身份验证
resources.resourceId(RESOURCE_ID).stateless(true);
}
resourceId
:设置资源服务器的ID
,用于授权服务器中的权限验证
stateless
:设置该资源服务器是否只允许基于token
的身份验证,true
即为只允许token
tokenStore
:设置token
的存储方式
RemoteTokenServices
资源服务器的主要逻辑流程便是对传入请求携带的令牌进行验证,验证通过则放行,验证失败则报错
ResourceServerTokenServices
便是主要完成验证的工作
如果资源服务器和授权服务器放在同一个应用中,那么授权服务器通过AuthorizationServerEndpointsConfigurer
默认构建了DefaultTokenServices
,它实现了所有必要的接口。
如果我们的资源服务器是一个单独的应用程序,那么我们必须要确保能够匹配授权服务器的功能,并提供知道如何正确解码token
的ResourceServerTokenServices
。
我们这里使用的是RemoteTokenServices
,允许资源服务器通过授权服务器/oauth/check_token
上的HTTP
资源来进行解码,这适用于资源服务器没有大量流量,或者可以负担缓存结果(因为每个请求都必须使用授权服务器进行验证)。
@Primary
@Bean
public RemoteTokenServices remoteTokenServices(){
final RemoteTokenServices remoteTokenServices = new RemoteTokenServices();
// 设置/oauth/check_token端口
remoteTokenServices.setCheckTokenEndpointUrl("http://localhost:8080/oauth/check_token");
// 设置客户端信息
remoteTokenServices.setClientId("client-a");
remoteTokenServices.setClientSecret("client-a-secret");
return remoteTokenServices;
}
@Primary
用于一个接口有多个实现Bean
的情况,装载时若没@Qualifier
特殊说明则优先装载
这里我们新建了RemoteTokenServices
对象
设置授权服务器中/oauth/check_token
对应URL
设置了客户端信息
这样每当有请求携带token
过来时,资源服务器都会访问授权服务器中/oauth/check_token
端口解析令牌
HttpSecurity
这就是我们之前spring security
中配置的内容,用于配置一些访问规则,这里就不在赘述
WebSecurityConfigurerAdapter
中的配置优先级高于ResourceServerConfigurerAdapter
中的配置
@Override
public void configure(HttpSecurity http) throws Exception {
// 设置session创建策略
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
// 所有端口全部需要验证
http.authorizeRequests()
.anyRequest().authenticated();
}
运行结果
我们访问localhost:8080/oauth/authorize
并且携带client_id
,client_secret
,response_type
输入账号密码之后
可以发现,浏览器重定向到了我们设定的重定向URI
,并且携带了授权码code=DIvTGk
现在我们携带这个授权码访问localhost:8080/oauth/token
请求token
成功拿到token
接着我们再来看看我们的资源服务器
当我们直接访问时,提示unauthorized
,即没有被授权
现在我们携带上我们刚刚拿到的token
发现访问接口成功
总结
这次主要是了解了OAuth2
和如何在springsecurity
中简单的实现OAuth2
。
对于授权服务器,我这里只是简单的存在内存中,更合理的话应该是自定义tokenStore
并且存在Redis
中
对于资源服务器,由于我是demo
使用,所以简单使用了remoteTokenServices
,实际操作中还是建议使用自定义的DefaultTOkenService
实现自解码。
a-cwh.oss-cn-hangzhou.aliyuncs.com/20210617181942.png" style=“zoom:80%;” />
当我们直接访问时,提示unauthorized
,即没有被授权
现在我们携带上我们刚刚拿到的token
发现访问接口成功
总结
这次主要是了解了OAuth2
和如何在springsecurity
中简单的实现OAuth2
。
对于授权服务器,我这里只是简单的存在内存中,更合理的话应该是自定义tokenStore
并且存在Redis
中
对于资源服务器,由于我是demo
使用,所以简单使用了remoteTokenServices
,实际操作中还是建议使用自定义的DefaultTOkenService
实现自解码。