oauth2流程
基本概念
Resource Owner: 资源所有者,一般指用户。
Resource Server: 资源服务器。
Client:客户端 、第三方应用,一般指需要赋权的应用。
Authorization Serve:授权服务器。
client-id、secret:通过这个,授权服务器能够认识第三方应用
code:由授权服务器生成,然后发送给客户端,然后客户端通过code去请求access_token
access_token:由授权服务器返回给客户端,然后客户端通过这个去访问资源服务器的资源
简单梳理:用户(RO)在资源服务器(RS)上拥有资源,在访问第三方应用的时候,想使用资源服务器上的资源,这时候需要通过授权服务器(AS)赋予用户访问RS上的部分数据。
上图中就是一个标准的OAuth2授权流程,先理一下这角色的对应关系。
我打开开发者头条,点击登录的时候选择了QQ登录这种方式,点击QQ登录跳转到一个提示界面,询问我是否继续登录,我点击确认之后重新跳转到开发者头条,这时候我在开发者头条的登陆状态是:已登录。
这个时候,通过chrome调试工具,观察一下network中请求的链接
点击QQ登录的时候请求变化如下
https://toutiao.io/auth/qq
https://graph.qq.com/oauth2.0/authorize?client_id=101185770&redirect_uri=https%3A%2F%2Ftoutiao.io%2Fauth%2Fqq%2Fcallback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
上面的路径经过解码之后的寄过如下
https://graph.qq.com/oauth2.0/authorize?client_id=101185770&redirect_uri=https://toutiao.io/auth/qq/callback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101185770&redirect_uri=https%3A%2F%2Ftoutiao.io%2Fauth%2Fqq%2Fcallback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
上面的路径经过解码之后的寄过如下,这个其实就是对用图二中的那个授权界面。
https://graph.qq.com/oauth2.0/show?which=Login&display=pc&client_id=101185770&redirect_uri=https://toutiao.io/auth/qq/callback&response_type=code&state=9ce7f718304908b0f7be95fed65ccba921df1dc0cc701942
在授权界面点击确定登录时请求变化如下
https://graph.qq.com/oauth2.0/authorize ,下图是它的请求参数
之后,在头条服务器对应的 /auth/qq/callback 方法内,会使用 哪个code向qq授权服务器请求access_token
这时候各个角色对应的关系如下:
- 我:RS资源所有者
- 开发者头条:Client第三方应用
- QQ资源服务器:资源服务器
- QQ授权服务器:授权服务器
我自己理解的流程
XML配置
主要介绍oauth2在项目中的XML配置方式。
先看一下依赖,这里面的依赖已经包括了 spring-security 对 cas、oauth2、ldap 支持所需要的依赖
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-web</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-config</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-ldap</artifactId>
<version>${spring.security.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
<version>1.0.4.RELEASE</version>
</dependency>
配置文件
<beans:beans xmlns="http://www.springframework.org/schema/security"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:oauth2="http://www.springframework.org/schema/security/oauth2"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.2.xsd
http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-4.0.xsd
http://www.springframework.org/schema/security/oauth2 http://www.springframework.org/schema/security/spring-security-oauth2-2.0.xsd">
<!-- 配置参考 http://blog.csdn.net/monkeyking1987/article/details/16828059 -->
<!-- 配置参考 https://github.com/spring-projects/greenhouse/blob/master/src/main/java/com/springsource/greenhouse/config/security.xml -->
<!-- 授权服务器的对外接口,没有验证通过的用户将看到 确认授权界面,确认授权界面 -->
<http pattern="/oauth/authorize" create-session="ifRequired" use-expressions="false"
authentication-manager-ref="oauth2AuthenticationManager">
<csrf disabled="true"/>
<intercept-url pattern="/oauth/authorize" method="GET" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<form-login login-page='/login?oauth'/>
</http>
<!-- client端 申请 access_token 时候的认证和权限设置,授权服务器提供 -->
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
entry-point-ref="oauth2AuthenticationEntryPoint">
<csrf disabled="true"/>
<intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>
<anonymous enabled="false"/>
<http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<custom-filter ref="requestContextFilter" before="PRE_AUTH_FILTER"/>
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
<!--对外API Spring Security 配置-->
<!--
* pattern URL匹配模式
* create-session 是否创建后台session
* entry-point-ref 入口点,访问过程中如果发生异常,会返回到入口点
* access-decision-manager-ref 认证管理器
-->
<http pattern="/api/**"
create-session="stateless"
use-expressions="true"
entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager">
<csrf disabled="true"/>
<!--拒绝匿名访问-->
<anonymous enabled="true"/>
<!-- 对外公开访问的API -->
<intercept-url pattern="/api/public/**" access="permitAll"/>
<!-- 设置访问权限控制 -->
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"/>
<!--<intercept-url pattern="/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
<!-- oauth 资源过滤器,与resource server配置对应 -->
<custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
<!-- 访问次数过滤器 -->
<!--<custom-filter ref="accessLimitFilter" after="FILTER_SECURITY_INTERCEPTOR"/>-->
<!-- 访问拒绝处理器 -->
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
<!--内部API Spring Security 配置-->
<http pattern="/i/api/**"
create-session="stateless"
use-expressions="true"
entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager">
<csrf disabled="true"/>
<!--拒绝匿名访问-->
<anonymous enabled="false"/>
<!-- 设置访问权限控制 -->
<!--<intercept-url pattern="/i/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
<!-- oauth 资源过滤器,与resource server配置对应 -->
<custom-filter ref="iapiResourceServer" before="PRE_AUTH_FILTER"/>
<!-- 访问拒绝处理器 -->
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
<!--透传API Spring Security 配置-->
<http pattern="/r/api/**"
create-session="stateless"
use-expressions="true"
entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager">
<csrf disabled="true"/>
<!--拒绝匿名访问-->
<anonymous enabled="true"/>
<!-- 设置访问权限控制 -->
<intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
method="GET"/>
<intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
method="POST"/>
<intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
method="PUT"/>
<intercept-url pattern="/r/api/**" access="hasAnyRole('ROLE_USER','SCOPE_READ')"
method="DELETE"/>
<!-- oauth 资源过滤器,与resource server配置对应 -->
<custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
<!-- 访问拒绝处理器 -->
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
<!--访问限制过滤器-->
<!--<beans:bean id="accessLimitFilter" class="com.hand.hap.security.ApiAccessLimitFilter" />-->
<!--内部API Spring Security 配置-->
<beans:bean id="oauth2AuthenticationEntryPoint"
class="org.springframework.security.oauth2.provider.error.OAuth2AuthenticationEntryPoint">
</beans:bean>
<!-- oauth2认证管理器,确定用户,角色及相应的权限 -->
<beans:bean id="oauth2AccessDecisionManager" class="org.springframework.security.access.vote.UnanimousBased">
<!-- 投票器 -->
<beans:constructor-arg>
<beans:list>
<!---->
<beans:bean class="org.springframework.security.web.access.expression.WebExpressionVoter"/>
<beans:bean class="org.springframework.security.oauth2.provider.vote.ScopeVoter"/>
<beans:bean class="org.springframework.security.access.vote.RoleVoter"/>
<beans:bean class="org.springframework.security.access.vote.AuthenticatedVoter"/>
<beans:bean class="com.hand.hap.security.PermissionVoter"/>
</beans:list>
</beans:constructor-arg>
</beans:bean>
<!-- 外部API Resource -->
<oauth2:resource-server id="apiResourceServer" resource-id="api-resource" token-services-ref="tokenServices"/>
<!-- 内部API Resource -->
<oauth2:resource-server id="iapiResourceServer" resource-id="iapi-resource" token-services-ref="tokenServices"/>
<beans:bean id="oauth2AccessDeniedHandler"
class="org.springframework.security.oauth2.provider.error.OAuth2AccessDeniedHandler"/>
<beans:bean id="tokenServices" class="org.springframework.security.oauth2.provider.token.DefaultTokenServices">
<beans:property name="tokenStore" ref="tokenStore"/>
<beans:property name="tokenEnhancer" ref="jwtAccessTokenConverter"/>
<beans:property name="supportRefreshToken" value="true"/>
<beans:property name="accessTokenValiditySeconds" value="3000"/>
<beans:property name="clientDetailsService" ref="clientDetailsService"/>
</beans:bean>
<beans:bean id="tokenStore" class="com.hand.hap.security.CustomJwtTokenStore">
<beans:constructor-arg ref="jwtAccessTokenConverter"/>
</beans:bean>
<!--<beans:bean id="jwtAccessTokenConverter" class="org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter">
<beans:property name="signingKey" value="handhand"/>
</beans:bean>-->
<beans:bean id="jwtAccessTokenConverter" class="com.hand.hap.security.CustomJwtAccessTokenConverter">
<beans:property name="signingKey" value="handhand"/>
</beans:bean>
<beans:bean id="requestContextFilter"
class="com.hand.hap.security.AuthenticationRequestContextFilter"/>
<!--<beans:bean id="tokenStore" class="org.springframework.security.oauth2.provider.token.JdbcTokenStore">
<beans:constructor-arg index="0" ref="dataSource"/>
</beans:bean>-->
<!-- ============================== -->
<!-- OAUTH 2 : AUTHORIZATION SERVER -->
<!-- ============================== -->
<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
authorization-request-manager-ref="OAuth2RequestFactory"
user-approval-page="forward:/oauth_approval.html"
user-approval-handler-ref="oauthUserApprovalHandler">
<oauth2:authorization-code/>
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password authentication-manager-ref="authenticationManager_oauth"/>
</oauth2:authorization-server>
<beans:bean id="clientCredentialsTokenEndpointFilter"
class="org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter">
<beans:property name="authenticationManager" ref="oauth2AuthenticationManager"/>
</beans:bean>
<authentication-manager id="oauth2AuthenticationManager">
<authentication-provider ref="customClientAuthenticationProvider"/>
</authentication-manager>
<beans:bean id="customClientAuthenticationProvider"
class="com.hand.hap.security.CustomAuthenticationProvider">
<beans:property name="userDetailsService" ref="oauth2ClientDetailsUserService"/>
</beans:bean>
<beans:bean id="oauth2ClientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<beans:constructor-arg ref="clientDetailsService"/>
</beans:bean>
<authentication-manager id="authenticationManager_oauth">
<authentication-provider ref="customUserAuthenticationProvider"/>
</authentication-manager>
<beans:bean id="customUserAuthenticationProvider"
class="com.hand.hap.security.CustomAuthenticationProvider">
<beans:property name="userDetailsService" ref="customUserDetailsService"/>
<beans:property name="passwordEncoder" ref="passwordManager"/>
</beans:bean>
<beans:bean id="OAuth2RequestFactory"
class="org.springframework.security.oauth2.provider.request.DefaultOAuth2RequestFactory">
<beans:constructor-arg ref="clientDetailsService"/>
</beans:bean>
<!-- Web Application clients -->
<beans:bean id="clientDetailsService" class="com.hand.hap.security.CustomJdbcClientDetailsService">
</beans:bean>
<!--<oauth2:client-details-service id="clientDetailsService" >
<oauth2:client client-id="client"
secret="secret"
authorities="APP"
authorized-grant-types="authorization_code,refresh_token"
resource-ids="api-resource"
scope="read,write,trust"/>
<oauth2:client client-id="client2"
secret="secret"
authorities="APP"
authorized-grant-types="password,refresh_token"
resource-ids="api-resource"
scope="read,write,trust"/>
<oauth2:client client-id="client3"
secret="secret"
authorities="APP"
authorized-grant-types="client_credentials"
resource-ids="api-resource"
scope="read,write,trust"/>
<oauth2:client client-id="client4"
secret="secret"
authorities="APP"
authorized-grant-types="implicit"
resource-ids="api-resource"
scope="read,write,trust"/>
</oauth2:client-details-service>-->
<beans:bean id="oauthUserApprovalHandler"
class="org.springframework.security.oauth2.provider.approval.TokenStoreUserApprovalHandler">
<beans:property name="tokenStore" ref="tokenStore"/>
<beans:property name="requestFactory" ref="OAuth2RequestFactory"/>
<beans:property name="clientDetailsService" ref="clientDetailsService"/>
</beans:bean>
</beans:beans>
先解释一下上面注释掉的一部分内容,<oauth2:client-details-service /> 用于配置 client客户端,上面注释的那些代码,其实就是配置了4个clien,可以看出,每个client的参数都对应着上面的属性,比如 授权模式、client-id、secret等参数。
现在不需要配置这个的原因,是因为这些配置已经动态配置在数据库里面了,对应的表名:sys_oauth_client_details ,可以看看表里面的内容,和配置文件里面的内容基本一致,不过更方便管理。
解读
认证服务器对外授权接口/oauth/authorize
<!-- 授权服务器的对外接口,没有验证通过的用户将看到 确认授权界面,确认授权界面 -->
<http pattern="/oauth/authorize" create-session="ifRequired" use-expressions="false"
authentication-manager-ref="oauth2AuthenticationManager">
<csrf disabled="true"/>
<intercept-url pattern="/oauth/authorize" method="GET" access="IS_AUTHENTICATED_ANONYMOUSLY"/>
<form-login login-page='/login?oauth'/>
</http>
pattern="/oauth/authorize" create-session="ifRequired"这个地址是有特殊用处的,要不然这里也不会单独配置。举一个例子来说明:当你在开发者头条点击QQ登录的时候,会跳转到QQ授权界面,这时候的处理流程: 用处 -->开发者头条后台-->QQ认证服务器对外的授权接口。这里的/oauth/authorize就相当于是QQ认证服务器对外的授权接口。
create-session="ifRequired" 表示为每个登录成功的用户会新建一个Session。
create-session="stateless" 表示对登录成功的用户不会创建Session了,你的application也不会允许新建session,而且Spring Security会跳过所有的 filter chain:HttpSessionSecurityContextRepository, SessionManagementFilter, RequestCacheFilter。也就是说每个请求都是无状态的独立的,需要被再次认证re-authentication。开销显然是增大了,因为每次请求都必须在服务器端重新认证并建立用户角色和权限的上下文。
oauth2AuthenticationManager 在 spring-security基本原理章节已经介绍过了,权限管理器,负责权限控制。对应的还有资源管理器,负责资源控制,在这个配置文件中,资源管理器对应 oauth2AccessDecisionManager
<authentication-manager id="oauth2AuthenticationManager">
<authentication-provider ref="customClientAuthenticationProvider"/>
</authentication-manager>
<beans:bean id="customClientAuthenticationProvider"
class="com.hand.hap.security.CustomAuthenticationProvider">
<beans:property name="userDetailsService" ref="oauth2ClientDetailsUserService"/>
</beans:bean>
<beans:bean id="oauth2ClientDetailsUserService"
class="org.springframework.security.oauth2.provider.client.ClientDetailsUserDetailsService">
<beans:constructor-arg ref="clientDetailsService"/>
</beans:bean>
<!-- Web Application clients -->
<beans:bean id="clientDetailsService" class="com.hand.hap.security.CustomJdbcClientDetailsService">
以上就是oauth2AuthenticationManager的配置,和之前讲的流程一样:manager-->provider-->userService,这些其实是属于 spring-security的概念。这里表示 访问 /oauth/authorize 会对应 oauth2AuthenticationManager 里面权限控制逻辑。其实不难发现,自己写的逻辑就是在 com.hand.hap.security.CustomJdbcClientDetailsService 这个类中,其对应源码如下:
package com.hand.hap.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.hand.hap.security.oauth.dto.Oauth2ClientDetails;
import com.hand.hap.security.oauth.service.IOauth2ClientDetailsService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.NoSuchClientException;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;
import org.springframework.util.StringUtils;
import java.util.Map;
/**
* @author Qixiangyu
* @author njq.niu@hand-china.com
* @date 2017/4/18.
*/
public class CustomJdbcClientDetailsService implements ClientDetailsService {
private static final Logger logger = LoggerFactory.getLogger(CustomJdbcClientDetailsService.class);
/**
* 警告: 仅用于内部登录生成 AccessToken 使用!
*/
public static final String DEFAULT_CLIENT_ID = "HAP_INNER_CLIENT_ID";
public static final String DEFAULT_CLIENT_SECRET = "2d6be1aa-5e3b-4b03-b8c9-d553ea276a05";
private static final String DEFAULT_SCOPE = "default";
private static final String DEFAULT_RESOURCE_ID = "api-resource";
private static final String DEFAULT_AUTHORIZED_GRANT_TYPES ="password,refresh_token";
private static final String DEFAULT_AUTHORITIES ="authorities";
private static final ThreadLocal<ClientDetails> CLIENT_DETAILS = new ThreadLocal<>();
@Autowired
private ObjectMapper objectMapper;
@Autowired
private IOauth2ClientDetailsService clientDetailsService;
public ClientDetails loadInnerClient(){
BaseClientDetails baseClientDetails = new BaseClientDetails(DEFAULT_CLIENT_ID,DEFAULT_RESOURCE_ID,DEFAULT_SCOPE,DEFAULT_AUTHORIZED_GRANT_TYPES,DEFAULT_AUTHORITIES, null);
baseClientDetails.setClientSecret(DEFAULT_CLIENT_SECRET);
// 默认设置1天
baseClientDetails.setAccessTokenValiditySeconds(86400);
return baseClientDetails;
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
if(DEFAULT_CLIENT_ID.equalsIgnoreCase(clientId)){
return loadInnerClient();
}
if(CLIENT_DETAILS.get() != null){
return CLIENT_DETAILS.get();
}
Oauth2ClientDetails oauth2ClientDetails = clientDetailsService.selectByClientId(clientId);
if (oauth2ClientDetails == null) {
throw new NoSuchClientException("No client with requested id: " + clientId);
}
BaseClientDetails details = new BaseClientDetails(oauth2ClientDetails.getClientId(),
oauth2ClientDetails.getResourceIds(), oauth2ClientDetails.getScope(),
oauth2ClientDetails.getAuthorizedGrantTypes(), oauth2ClientDetails.getAuthorities(),
oauth2ClientDetails.getRedirectUri());
details.setAutoApproveScopes(StringUtils.commaDelimitedListToSet(oauth2ClientDetails.getAutoApprove()));
details.setClientSecret(oauth2ClientDetails.getClientSecret());
if(oauth2ClientDetails.getRefreshTokenValidity() != null) {
details.setRefreshTokenValiditySeconds(oauth2ClientDetails.getRefreshTokenValidity().intValue());
}
if(oauth2ClientDetails.getAccessTokenValidity() != null) {
details.setAccessTokenValiditySeconds(oauth2ClientDetails.getAccessTokenValidity().intValue());
}
if (oauth2ClientDetails.getAdditionalInformation() != null) {
try {
Map<String, Object> additionalInformation = objectMapper.readValue(oauth2ClientDetails.getAdditionalInformation(), Map.class);
details.setAdditionalInformation(additionalInformation);
}
catch (Exception e) {
logger.warn("Could not decode JSON for additional information: " + details, e);
}
}
CLIENT_DETAILS.set(details);
return details;
}
public static void clearInfo(){
if(CLIENT_DETAILS != null) {
CLIENT_DETAILS.remove();
}
}
}
主要就是 loadClientByClientId 方法,这里 从数据库获取client信息,然后 封装成 BaseClientDetails 对象,最后交由spring -security管理。
认证服务器对获取token_access接口 /oauth/authorize
<!-- client端 申请 access_token 时候的认证和权限设置,授权服务器提供 -->
<http pattern="/oauth/token" create-session="stateless" authentication-manager-ref="oauth2AuthenticationManager"
entry-point-ref="oauth2AuthenticationEntryPoint">
<csrf disabled="true"/>
<intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>
<anonymous enabled="false"/>
<http-basic entry-point-ref="oauth2AuthenticationEntryPoint"/>
<custom-filter ref="requestContextFilter" before="PRE_AUTH_FILTER"/>
<custom-filter ref="clientCredentialsTokenEndpointFilter" before="BASIC_AUTH_FILTER"/>
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
tpattern="/oauth/token" create-session="stateless" /oauth/token 是用来 client 获取access_token的,注意其中的 <intercept-url pattern="/oauth/token" access="isFullyAuthenticated()"/>,表示访问 /oauth/token 接口需要 isFullyAuthenticatd()的权限。还有 entry-point-ref="oauth2AuthenticationEntryPoint" ,表示入口点,访问过程中如果发生异常,会返回到入口点。
接口访问权限
<!--对外API Spring Security 配置-->
<!--
* pattern URL匹配模式
* create-session 是否创建后台session
* entry-point-ref 入口点,访问过程中如果发生异常,会返回到入口点
* access-decision-manager-ref 认证管理器
-->
<http pattern="/api/**"
create-session="stateless"
use-expressions="true"
entry-point-ref="oauth2AuthenticationEntryPoint"
access-decision-manager-ref="oauth2AccessDecisionManager">
<csrf disabled="true"/>
<!--拒绝匿名访问-->
<anonymous enabled="true"/>
<!-- 对外公开访问的API -->
<intercept-url pattern="/api/public/**" access="permitAll"/>
<!-- 设置访问权限控制 -->
<intercept-url pattern="/api/**" access="isFullyAuthenticated()"/>
<!--<intercept-url pattern="/api/**" access="hasAnyRole('ROLE_UNITY','SCOPE_READ')"/>-->
<!-- oauth 资源过滤器,与resource server配置对应 -->
<custom-filter ref="apiResourceServer" before="PRE_AUTH_FILTER"/>
<!-- 访问次数过滤器 -->
<!--<custom-filter ref="accessLimitFilter" after="FILTER_SECURITY_INTERCEPTOR"/>-->
<!-- 访问拒绝处理器 -->
<access-denied-handler ref="oauth2AccessDeniedHandler"/>
</http>
以 /api/** 开头的url,需要通过资源控制器oauth2AccessDecisionManager的认证,要具有 isFullyAuthenticated()的权限;以/api/public/**表示所有人都可以访问。配置文件中其它的 那些 内部API也是类似。
授权服务器
<!-- ============================== -->
<!-- OAUTH 2 : AUTHORIZATION SERVER -->
<!-- ============================== -->
<oauth2:authorization-server client-details-service-ref="clientDetailsService" token-services-ref="tokenServices"
authorization-request-manager-ref="OAuth2RequestFactory"
user-approval-page="forward:/oauth_approval.html"
user-approval-handler-ref="oauthUserApprovalHandler">
<oauth2:authorization-code/>
<oauth2:implicit/>
<oauth2:refresh-token/>
<oauth2:client-credentials/>
<oauth2:password authentication-manager-ref="authenticationManager_oauth"/>
</oauth2:authorization-server>
请求access_token原理
本章节主要介绍的是和 "请求access_token" 相关,即获取token的流程
AbstractAuthenticationProcessingFilter
AbstractAuthenticationProcessingFilter 是一个抽象类,在 spring security中,位于过滤链中的第 5 个。实际运行的时候,根据不同的场景,调用不同的实现类,其主要实现类如下
可能会好奇,spring securit 怎么就知道在什么时候调用哪个子类,它是怎么区分不同的场景的?追源码到 FilterChainProxy 类下,在其中的doFilterInternal方法中:
private void doFilterInternal(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
FirewalledRequest fwRequest = firewall
.getFirewalledRequest((HttpServletRequest) request);
HttpServletResponse fwResponse = firewall
.getFirewalledResponse((HttpServletResponse) response);
List<Filter> filters = getFilters(fwRequest);
if (filters == null || filters.size() == 0) {
if (logger.isDebugEnabled()) {
logger.debug(UrlUtils.buildRequestUrl(fwRequest)
+ (filters == null ? " has no matching filters"
: " has an empty filter list"));
}
fwRequest.reset();
chain.doFilter(fwRequest, fwResponse);
return;
}
VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
vfc.doFilter(fwRequest, fwResponse);
}
注意其中的 getFilters(fwRequest); 方法:
根据不同的请求路径,获取到不同的过滤器数组,这部分是在项目启动时初始化进去的。
可以看出,有关于 token的请求,都会被 ClientCredentialsTokenEndpointFilter 处理。
然后 通过这个进行身份认证
return this.getAuthenticationManager().authenticate(authRequest);
ProviderManager 实现了 AuthenticationManager接口, ProviderManager 拥有 一组 AuthenticationProvider 对象 (List<AuthenticationProvider> providers),AuthenticationProvider 是一个接口,对应一系列实现。然后遍历 providers,对每个 AuthenticationProvider 进行身份认证。默认是有两个 AuthenticationProvider 的, 如图:
进行校验
DaoAuthenticationProvider 继承了 AbstractUserDetailsAuthenticationProvider, 所以很多是功能都是在 AbstractUserDetailsAuthenticationProvider 抽象类中是实现的。
对于这个步骤,也是根据不同的场景调用不用的 UserDetailsService 实现类查询用户。
获取token的时候,调用的时候,实现类是 ClientDetailsUserDetailsService ,在 loadUserByUsername 方法里面 调用了 ClientDetailsService 实现类的 的 loadClientByClientId 方法, 就是根据client_id找到客户端信息
返回的用户信息如下
获取用户信息之后,紧接着对 用户信息进行校验,就是检查一下用户信息是否被锁住,是否失效啥的。additionalAuthenticationChecks 是对 密码进行校验
然后又有个校验,校验密码是否过期
都校验通过之后,然后返回一个 UsernamePasswordAuthenticationToken 对象
校验完成之后,将一些信息拷贝到 校验后的结果中,这里是将 authentication 中的 details 放到 result 中
至此一个 AuthenticationProvider 的校验已经完成,ProviderManager 中的 private List<AuthenticationProvider> providers 属性中 有多少个 AuthenticationProvider ,就重复多少次 这样的校验。
当所有的 AuthenticationProvider 都执行完之后,然后就抹去密码
然后就根据实际情况,调用验证成功的逻辑。
然后将 result 返回到 AbstractAuthenticationProcessingFilter中,如果没问题,就调用session 策略进行处理吧
不过这个处理不太清楚是什么场景下用到
至此,ClientCredentialsTokenEndpointFilter (也就是AbstractAuthenticationProcessingFilter )处理完毕,接着执行下面的过滤器。
所有过滤器执行完毕之后, 会请求 TokenEndpoint 类中的 方法,这里会校验 scope等一些信息。
使用access_token原理
主要介绍的是 和 "使用access_token" 相关,即通过 access_token 请求资源的时候,内部的执行流程。
ResourceServerSecurityConfigurer
在配置 资源服务器的时候, 使用了一个 @EnableResourceServer 注解,如下:
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(ResourceServerSecurityConfigurer resources) {
resources.resourceId(API_RESOURCE_ID).stateless(true);
}
@Override
public void configure(HttpSecurity http) throws Exception {
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
// 配置order访问控制,必须认证过后才可以访问
.and().anonymous()
.and().requestMatchers().anyRequest()
.and().authorizeRequests().antMatchers("/order/**").authenticated();
}
}
可以看出,和资源相关的配置类:ResourceServerSecurityConfigurer,其主要配置如下:
@Override
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (eventPublisher != null) {
resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
}
if (tokenExtractor != null) {
resourcesServerFilter.setTokenExtractor(tokenExtractor);
}
if (authenticationDetailsSource != null) {
resourcesServerFilter.setAuthenticationDetailsSource(authenticationDetailsSource);
}
resourcesServerFilter = postProcess(resourcesServerFilter);
resourcesServerFilter.setStateless(stateless);
// @formatter:off
http
.authorizeRequests().expressionHandler(expressionHandler)
.and()
.addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler)
.authenticationEntryPoint(authenticationEntryPoint);
// @formatter:on
}
private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
if (authenticationManager != null) {
if (authenticationManager instanceof OAuth2AuthenticationManager) {
oauthAuthenticationManager = (OAuth2AuthenticationManager) authenticationManager;
}
else {
return authenticationManager;
}
}
oauthAuthenticationManager.setResourceId(resourceId);
oauthAuthenticationManager.setTokenServices(resourceTokenServices(http));
oauthAuthenticationManager.setClientDetailsService(clientDetails());
return oauthAuthenticationManager;
}
该配置主要功能:
- new OAuth2AuthenticationManager()
- new OAuth2AuthenticationProcessingFilter()
- setAuthenticationManager(oauthAuthenticationManager)
即 创建OAuth2AuthenticationManager, 创建 OAuth2AuthenticationProcessingFilter, 然后将 OAuth2AuthenticationProcessingFilter 和 OAuth2AuthenticationManager相关联。它并没有将OAuth2AuthenticationManager添加到spring的容器中,不然可能会影响spring security的普通认证流程(非oauth2请求),只有被OAuth2AuthenticationProcessingFilter拦截到的oauth2相关请求才被特殊的身份认证器处理。
OAuth2AuthenticationProcessingFilter
当请求 http://localhost:7070/order/1?access_token=e50acb5a-c9b0-4e39-a8f6-221264ea25d7 的时候,需要进入 OAuth2AuthenticationProcessingFilter 过滤器,但是这里有一个前提,需要在配置文件中设置 OAuth2AuthenticationProcessingFilter 的 Order。
security:
oauth2:
resource:
filter-order: 3
如果不设置Order OAuth2AuthenticationProcessingFilter 不会生效,生效的是 UsernamePasswordAuthenticationFilter,即会走标准的 spring-security认证流程。
OAuth2AuthenticationProcessingFilter 的核心代码如下:
public class OAuth2AuthenticationProcessingFilter implements Filter, InitializingBean {
private final static Log logger = LogFactory.getLog(OAuth2AuthenticationProcessingFilter.class);
private AuthenticationEntryPoint authenticationEntryPoint = new OAuth2AuthenticationEntryPoint();
private AuthenticationManager authenticationManager;
private AuthenticationDetailsSource<HttpServletRequest, ?> authenticationDetailsSource = new OAuth2AuthenticationDetailsSource();
private TokenExtractor tokenExtractor = new BearerTokenExtractor();
private AuthenticationEventPublisher eventPublisher = new NullEventPublisher();
private boolean stateless = true;
......
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {
if (stateless && isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
Authentication authResult = authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + failed);
}
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
}
当我们通过一个access_token请求资源的时候,可以看看过滤器链
首先提取 access_token
Authentication authentication = tokenExtractor.extract(request);
这一部分功能是通过 TokenExtractor 接口的实现类 BearerTokenExtractor 实现的。BearerTokenExtractor的核心代码如下
public Authentication extract(HttpServletRequest request) {
String tokenValue = this.extractToken(request);
if (tokenValue != null) {
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
} else {
return null;
}
}
protected String extractToken(HttpServletRequest request) {
String token = this.extractHeaderToken(request);
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
token = request.getParameter("access_token");
if (token == null) {
logger.debug("Token not found in request parameters. Not an OAuth2 request.");
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, "Bearer");
}
}
return token;
}
protected String extractHeaderToken(HttpServletRequest request) {
Enumeration headers = request.getHeaders("Authorization");
String value;
do {
if (!headers.hasMoreElements()) {
return null;
}
value = (String)headers.nextElement();
} while(!value.toLowerCase().startsWith("Bearer".toLowerCase()));
String authHeaderValue = value.substring("Bearer".length()).trim();
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, value.substring(0, "Bearer".length()).trim());
int commaIndex = authHeaderValue.indexOf(44);
if (commaIndex > 0) {
authHeaderValue = authHeaderValue.substring(0, commaIndex);
}
return authHeaderValue;
}
代码逻辑很简单,先是从 请求头中的 Authorization 获取 access_token ,如果没有找到,再从请求参数中获取 access_token:request.getParameter("access_token")。获取access_token之后再封装成一个 PreAuthenticatedAuthenticationToken 对象返回。
获取token之后,再校验身份
这里的authenticationManager, 不再是之前的ProvideManager,而是 OAuth2AuthenticationManager。
其核心代码如下:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
}
checkClientDetails(auth);
if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
// Guard against a cached copy of the same details
if (!details.equals(auth.getDetails())) {
// Preserve the authentication details from the one loaded by token services
details.setDecodedDetails(auth.getDetails());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
首先根据token获取身份信息。
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
这里的tokenServices是 对应 private ResourceServerTokenServices tokenServices;
其默认实现类DefaultTokenServices
对应的方法如下
先从tokenStore里面获取获取client_id,然后 clientDetailsService 通过client_id 获取 身份信息
clientDetailsService.loadClientByClientId(clientId);
获取身份信息之后,再进行身份校验
checkClientDetails(auth);
private void checkClientDetails(OAuth2Authentication auth) {
if (clientDetailsService != null) {
ClientDetails client;
try {
client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
}
catch (ClientRegistrationException e) {
throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
}
Set<String> allowed = client.getScope();
for (String scope : auth.getOAuth2Request().getScope()) {
if (!allowed.contains(scope)) {
throw new OAuth2AccessDeniedException(
"Invalid token contains disallowed scope (" + scope + ") for this client");
}
}
}
}
主要就是校验一下scope是否匹配,校验通过后,设置身份为已经校验成功,然后返回已经校验通过的身份信息。
token存储
主要介绍token的存储
TokenStore
spring-security-oauth2中access_token存储主要交由 TokenStore 接口管理,其实现类如下
主要的常用方法:读取、存储等
RedisTokenStore
spring:
datasource:
url: jdbc:mysql://localhost:3306/vue?characterEncoding=utf8&autoReconnect=true&useSSL=false
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
redis:
host: 127.0.0.1
database: 15
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
RedisConnectionFactory redisConnectionFactory;
......
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
.tokenStore(new RedisTokenStore(redisConnectionFactory))
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
// 允许 GET、POST 请求获取 token,即访问端点:oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
}
JwtTokenStore
当我们使用JWT的时候,需要使用JwtTokenStore,同时需要再pom.xml文件中添加依赖
<!-- 使用 JwtTokenStore 方式存儲令牌 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-jwt</artifactId>
</dependency>
JwtTokenStore需要使用 JwtAccessTokenConverter。
JwtAccessTokenConverter
JwtAccessTokenConverter用于将令牌数据存储在令牌中,它有两个实现类:
- JwtAccessTokenConverter:用于令牌 JWT 编码与解码
- DefaultAccessTokenConverter:AccessTokenConverter 的默认实现。官方的解释如下:
Helper that translates between JWT encoded token values and OAuth authentication
information (in both directions). Also acts as a {@link TokenEnhancer} when tokens are
granted.
TokenEnhancer
可用于自定义令牌策略,在令牌被 AuthorizationServerTokenServices 的实现存储之前增强令牌的策略,它有两个实现类:
- JwtAccessTokenConverter:用于令牌 JWT 编码与解码。
- TokenEnhancerChain:一个令牌链,可以存放多个令牌,并循环的遍历令牌并将结果传递给下一个令牌。
OAuth2AccessToken
OAuth2 Token(令牌)实体类,包含了令牌、类型(Bearer)、失效时间等。
最后的配置如下:
@Configuration
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Autowired
@Qualifier("authenticationManagerBean")
AuthenticationManager authenticationManager;
// @Autowired
// RedisConnectionFactory redisConnectionFactory;
@Autowired
PasswordEncoder passwordEncoder;
@Autowired
UserDetailsService userDetailsService;
@Override
public void configure(ClientDetailsServiceConfigurer clientConfigure) throws Exception {
clientConfigure.withClientDetails(new CustomClientDetailsService());
/**
* 使用jwt方式存储token
*
* @return
*/
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 使用 JWT 令牌
*
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter converter = new JwtAccessTokenConverter() {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication().getName();
/** 自定义一些token属性 ***/
final Map<String, Object> additionalInformation = new HashMap<>(5);
additionalInformation.put("userName", userName);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
// 用于加密的 密钥
converter.setSigningKey("123");
return converter;
}
@Bean
@Primary
public AuthorizationServerTokenServices defaultTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter());
defaultTokenServices.setTokenStore(tokenStore());
return defaultTokenServices;
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
/**
* 使用RedisTokenStore 存储 access_token
*/
// TokenStore tokenStore = new RedisTokenStore(redisConnectionFactory);
/**
* JWT方式存储token
*/
TokenStore tokenStore = tokenStore();
endpoints.tokenStore(tokenStore)
.tokenServices(defaultTokenServices())
.accessTokenConverter(jwtAccessTokenConverter())
.authenticationManager(authenticationManager)
.userDetailsService(userDetailsService)
// 允许 GET、POST 请求获取 token,即访问端点:oauth/token
.allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
}
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
// 允许表单认证
oauthServer.realm(API_RESOURCE_ID).allowFormAuthenticationForClients();
// oauthServer.realm(API_RESOURCE_ID)
// //url:/oauth/token_key,exposes public key for token verification if using JWT tokens
// .tokenKeyAccess("permitAll()")
// //url:/oauth/check_token allow check token
// .checkTokenAccess("isAuthenticated()")
// .checkTokenAccess("hasAuthority('ME')")
//
// //允许表单认证
// .allowFormAuthenticationForClients();
}
}
JWT可以添加一些其它用户信息,比如上面就添加了一个 userName 属性,在浏览器上访问token,其返回结果如下