Spring Security源码解析篇介绍了Spring Security的原理,复习下几个概念
- Principle GrantedAuthority Authentication AbstractAuthenticationToken UsernamePasswordAuthenticationToken
- AuthenticationManager AuthenticationProvider DaoAuthenticationProvider ProviderManager
- UserDetail CredentialsContainer UserDetailsService UserCache
- WebSecurityConfigurerAdapter WebSecurity HttpSecuirty FilterChainProxy
接下来介绍oauth2的相关概念。oauth2是建立在spring security基础上的一套规范,并非框架。
主要角色有
- 授权服务器 AuthorizationServer 配置client,tokenStore,authenticationManager等
- 资源服务器 ResourceServer 配置HttpSecurity,哪些uri需要验证;配置ResourceServerSecurityConfigurer,如设置tokenService
- 客户端 client 包含clientid,secret
- 用户 user 包含username,password
一般来说,资源服务器和授权服务器都是一个机构提供的,授权服务器给客户端授权,客户端再去请求资源服务器(授权服务器和资源服务器可以是一个应用程序也可以是两个应用程序)。下面例子中的QQ既是资源服务器,也是授权服务器。
用户想登录csdn(客户端),发现还没有注册csdn(客户端)的账号,刚好看见有一个通过QQ登录的方式,遂输入QQ的账户密码,点击授权。此时QQ服务器(授权服务器)对csdn(客户端)和用户密码进行验证,验证成功后给csdn(客户端)发放令牌,csdn凭借令牌去获取用户的QQ头像(资源服务器)等信息。
目录
1. 好基友一辈子 OAuth2Authentication和OAuth2AccessToken
2. TokenGranter、TokenStore、TokenExtractor
2.4 ResourceServerTokenServices
3. ClientDetails ClientDetailsService
3.3 ClientDetailsServiceBuilder
4. 资源服务器配置 ResourceServerConfigurerAdapter
5. 授权服务器配置 AuthorizationServerConfigurerAdapter
6.TokenEndPoint,AuthorizationEndPoint,CheckTokenEndPoint
1. 好基友一辈子 OAuth2Authentication和OAuth2AccessToken
1.1 OAuth2Authentication
OAuth2Authentication顾名思义是Authentication的子类,存储用户信息和客户端信息,但多了2个属性
private final OAuth2Request storedRequest; private final Authentication userAuthentication;
这样OAuth2Authentication可以存储2个Authentication,一个给client(必要),一个给user(只是有些授权方式需要)。除此之外同样有principle,credentials,authorities,details,authenticated等属性。
OAuth2Request 用于存储request中的Authentication信息(grantType,responseType,resouceId,clientId,scope等),这里就引出了OAuth2 中的三大request。
1.2 BaseRequest
BaseRequest是抽象类,有3个属性:clienId、scope和requestParameters。
abstract class BaseRequest implements Serializable {
private String clientId;
private Set<String> scope = new HashSet<String>();
private Map<String, String> requestParameters = Collections
.unmodifiableMap(new HashMap<String, String>());
/** setter,getter */
}
继承类见下图,3个类都在OAuth2包中,这些request都会存在于OAuth2的验证流程中,用于传递clientId,scope,requestParameters等属性,与HttpServletRequest有本质区别!
1.2.1 AuthorizationRequest
向授权服务器AuthorizationEndPoint (/oauth/authorize)请求授权,AuthorizationRequest作为载体存储state,redirect_uri等参数,生命周期很短且不能长时间存储信息,可用OAuth2Request代替存储信息。
public class AuthorizationRequest extends BaseRequest implements Serializable {
// 用户同意授权传递的参数,不可改变
private Map<String, String> approvalParameters = Collections.unmodifiableMap(new HashMap<String, String>());
// 客户端发送出的状态信息,从授权服务器返回的状态应该不变才对
private String state;
// 返回类型集合
private Set<String> responseTypes = new HashSet<String>();
// resource ids 可变
private Set<String> resourceIds = new HashSet<String>();
// 授权的权限
private Collection<? extends GrantedAuthority> authorities = new HashSet<GrantedAuthority>();
// 终端用户是否同意该request发送
private boolean approved = false;
// 重定向uri
private String redirectUri;
// 额外的属性
private Map<String, Serializable> extensions = new HashMap<String, Serializable>();
// 持久化到OAuth2Request
public OAuth2Request createOAuth2Request() {
return new OAuth2Request(getRequestParameters(), getClientId(), getAuthorities(), isApproved(), getScope(), getResourceIds(), getRedirectUri(), getResponseTypes(), getExtensions());
}
// setter,getter
}
1.2.2 TokenRequest
向授权服务器TokenEndPoint(/oauth/token)发送请求获得access_token时,tokenRequest作为载体存储请求中grantType等参数。常和tokenGranter.grant(grantType,tokenRequest)结合起来使用。
TokenRequest携带了新属性grantType,和方法createOAuth2Request(用于持久化)
private String grantType;
public OAuth2Request createOAuth2Request(ClientDetails client) {
Map<String, String> requestParameters = getRequestParameters();
HashMap<String, String> modifiable = new HashMap<String, String>(requestParameters);
// Remove password if present to prevent leaks
modifiable.remove("password");
modifiable.remove("client_secret");
// Add grant type so it can be retrieved from OAuth2Request
modifiable.put("grant_type", grantType);
return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),
client.getResourceIds(), null, null, null);
}
1.2.3 OAuth2Request
用来存储TokenRequest或者AuthorizationRequest的信息,只有构造方法和getter方法,不提供setter方法。它作为OAuth2Authentication的一个属性(StoredRequest),存储request中的authentication信息(grantType,approved,responseTypes)。
1.2.4 OAuth2RequestFactory
工厂类生成OAuth2Request、TokenRequest、AuthenticationRequest。
public interface OAuth2RequestFactory {
/**
* 从request请求参数中获取clientId,scope,state
* clientDetailsService loadClientByClientId(clientId) 获取clientDetails resourcesId Authorities
* 根据以上信息生成AuthenticationRequest
*/
AuthorizationRequest createAuthorizationRequest(Map<String, String> authorizationParameters);
/**
* AuthorizationRequest request 有生成OAuth2Request的方法
* request.createOAuth2Request()
*/
OAuth2Request createOAuth2Request(AuthorizationRequest request);
OAuth2Request createOAuth2Request(ClientDetails client, TokenRequest tokenRequest);
TokenRequest createTokenRequest(Map<String, String> requestParameters, ClientDetails authenticatedClient);
TokenRequest createTokenRequest(AuthorizationRequest authorizationRequest, String grantType);
}
1.3 OAuth2AccessToken
OAuth2AccessToken是一个接口,提供安全令牌token的基本信息,不包含用户信息,仅包含一些静态属性(scope,tokenType,expires_in等)和getter方法,如String getScope,OAuth2RefreshToken getRefreshToken,String getTokenType,String getValue()等。TokenGranter.grant()返回的值即OAuth2AccessToken。
OAuth2AccessToken和OAuth2Authentication是一对好基友,谁要先走谁是狗!!!
TokenStore同时存储OAuth2AccessToken和OAuth2Authentication,也可根据OAuth2Authentication中的OAuth2Request信息可获取对应的OAuth2AccessToken。
DefaultTokenServices有如下方法,都可以通过一个获得另一个的值
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication)
OAuth2Authentication loadAuthentication(String accessTokenValue)
// 当tokenStore是jdbcTokenStore,表示从数据库中根据OAuth2Authentication获取OAuth2AccessToken
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
DefaultOAuth2AccessToken是OAuth2AccessToken的实现类,多了构造方法,setter方法和OAuth2AccessToken valueOf(Map<String,Object> tokenParams)。经过json转换后就是我们常见的access_token对象,如下所示。
{
"access_token": "1e95d081-0048-4397-a081-c76f7823fe54",
"token_type": "bearer",
"refresh_token": "7f6db28b-50dc-40a2-b381-3e356e30af2b",
"expires_in": 1799,
"scope": "read write"
}
OAuth2RefreshToken是接口,只有String getValue()方法。
DefaultOAuth2RefreshToken是OAuth2RefreshToken的实现类。
2. TokenGranter、TokenStore、TokenExtractor
2.1 TokenGranter(/oauth/token)
一般在用户请求TokenEndPoints中的路径/oauth/token时,根据请求参数中的grantType,username,password,client_id,client_secret等,调用TokenGranter给用户分发OAuth2AccessToken。
根据grantType(password,authorization-code)和TokenRequest(requestParameters,clientId,grantType)授予人OAuth2AccessToken令牌。
public interface TokenGranter {
OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest);
}
回忆下TokenRequest包含了基本信息clientId,scope,requestParameters,grantType等。根据tokenRequest获取OAuth2Request,初始化获得OAuth2Authentication,再去数据库里找oauth2accesstoken,如果有则直接返回,如果没有则创建新的oauth2accesstoken,并且和OAuth2Authentication一起存入数据库中。
2.1.1 AbstractTokenGranter
TokenGranter抽象继承类AbstractTokenGranter,实现了grant方法,源码如下。
执行顺序为根据tokenRequest====》clientId ====》clientDetails====》OAuth2Authentication(getOAuth2Authentication(client,tokenRequest))====》OAuth2AccessToken(tokenService.createAccessToken)
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
validateGrantType(grantType, client);
logger.debug("Getting access token for: " + clientId);
// getAccessToken 先获得OAuth2Authentication,再创建OAuth2AccessToken
return getAccessToken(client, tokenRequest);
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
// AbstractTokenGranter的继承类重写了该方法
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
OAuth2Request storedOAuth2Request = requestFactory.createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, null);
}
实现AbstractTokenGranter的类有5种。其中如果用password的方式进行验证,那么TokenGranter类型是ResourceOwnerPasswordTokenGranter,该类中重写了getOAuth2Authentication方法,里面调用了authenticationManager.manage()方法。
用户可自行定义granter类继承AbstractTokenGranter,重写getOAuth2Authentication()方法,并将该granter类添加至CompositeTokenGranter中。
TokenGranter的继承类如下图所示
2.1.2 CompositeTokenGranter
有继承类CompositeTokenGranter,包含List<TokenGranter> tokenGranters属性,grant方法是遍历tokenGranters进行逐一grant,只要有一个有返回值就返回。
public class CompositeTokenGranter implements TokenGranter {
private final List<TokenGranter> tokenGranters;
public CompositeTokenGranter(List<TokenGranter> tokenGranters) {
this.tokenGranters = new ArrayList<TokenGranter>(tokenGranters);
}
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
public void addTokenGranter(TokenGranter tokenGranter) {
if (tokenGranter == null) {
throw new IllegalArgumentException("Token granter is null");
}
tokenGranters.add(tokenGranter);
}
}
2.2 TokenStore(/oauth/token)
一般在TokenGranter执行grant方法完毕后,将OAuth2AccessToken和OAuth2Authentication存储起来,方便以后根据其中一个查询另外一个(如根据access_token查询获得OAuth2Authentication)。
存储OAuth2AccessToken和OAuth2Authentication(比Authentication多了两个属性storedRequest,userAuthentication),存储方法如下。还有各种read,remove方法
void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication);
实现类有5类,其中JdbcTokenStore是通过连接数据库来存储OAuth2AccessToken的,这也是我们一般存储token的方法。条件是数据库里的表结构必须按照标准建立。(JwtTokenStore不存储token和authentication,直接根据token解析获得authentication)
oauth_access_token表结构如下,可见表里存储了OAuth2AccessToken和OAuth2Authentication两个对象,值得注意的是token_id并不等于OAuth2AccessToken.getValue(),value经过MD5加密后才是token_id。同理authentication_id 和 refresh_token也是经过加密转换存储的。
第一次获得token,直接存入数据库表里。
如果重复post请求/oauth/token, JdbcTokenStore会先判断表中是否已有该用户的token,如果有先删除,再添加。
2.3 TokenExtractor (OAuth2AuthentiactionProcessingFilter)
用户携带token访问资源,过滤器进行到OAuth2AuthentiactionProcessingFilter时,从HttpServletRequest中获取access_token(可以从header或者params中获取),拼接成PreAuthenticatedAuthenticationToken(Authentication子类)
Authentication extract(HttpServletRequest request);
BearerTokenExtractor是它的实现类,实现了从request中获取Authentication的方法。
1.header中 Authentication:Bearer xxxxxxxx--xxx
2.request parameters中 access_token=xxxx-xxxx-xxxx
protected String extractToken(HttpServletRequest request) {
// 1.直接从header中提取key为Authentication,value是以Bearer 开头的header
// 如Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135
String token = extractHeaderToken(request);
// bearer type allows a request parameter as well
if (token == null) {
logger.debug("Token not found in headers. Trying request parameters.");
// 2.如果header中不包含,则从param中获取"access_token"对应的值
token = request.getParameter(OAuth2AccessToken.ACCESS_TOKEN);
if (token == null) {
logger.debug("Token not found in request parameters. Not an OAuth2 request.");
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_TYPE, OAuth2AccessToken.BEARER_TYPE);
}
}
return token;
}
2.4 ResourceServerTokenServices
两个方法。用户携access_token访问资源服务器时,资源服务器会将该字符串进行解析,获得OAuth2Authentication和OAuth2AccessToken。
loadAuthentication根据字符串accessToken获得OAuth2Authentication;
readAccessToken根据字符串accessToken获得OAuth2AccessToken。
public interface ResourceServerTokenServices {
/**
* Load the credentials for the specified access token.
*
* @param accessToken The access token value.
* @return The authentication for the access token.
* @throws AuthenticationException If the access token is expired
* @throws InvalidTokenException if the token isn't valid
*/
OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;
/**
* Retrieve the full access token details from just the value.
*
* @param accessToken the token value
* @return the full access token with client id etc.
*/
OAuth2AccessToken readAccessToken(String accessToken);
}
有两重要继承类,DefaultTokenServices和RemoteTokenServices
2.4.1 DefaultTokenServices
实现了两个接口AuthorizationServerTokenServices和ResourceServerTokenServices。常在granter().grant()方法中调用tokenServices.createAccessToken()方法获得oauth2accesstoken。
其中重要方法createAccessToken(OAuth2Authentication oauth2)源码如下
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果数据库中已存了authentication和accesstoken,则直接提取
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// Only create a new refresh token if there wasn't an existing one
// associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in
// the case that the old access token
// expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 第一次创建access_token,并且存储到数据库中
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
2.4.2 RemoteTokenServices
当授权服务和资源服务不在一个应用程序的时候,资源服务可以把传递来的access_token递交给授权服务的/oauth/check_token进行验证,而资源服务自己无需去连接数据库验证access_token,这时就用到了RemoteTokenServices。
loadAuthentication方法,设置head表头Authorization 存储clientId和clientSecret信息,请求参数包含access_token字符串,向AuthServer的CheckTokenEndpoint (/oauth/check_token)发送请求,返回验证结果map(包含clientId,grantType,scope,username等信息),拼接成OAuth2Authentication。
重要!!!
AuthServer需要配置checkTokenAcess,否则默认为“denyAll()”,请求访问/oauth/check_token会提示没权限。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) {
oauthServer.realm(QQ_RESOURCE_ID).allowFormAuthenticationForClients();
// 访问/oauth/check_token 需要client验证
oauthServer.checkTokenAccess("isAuthenticated()");、
// 也可配置访问/oauth/check_token无需验证
// oauthServer.checkTokenAccess("permitAll()");
}
不支持readAccessToken方法。
public class RemoteTokenServices implements ResourceServerTokenServices {
protected final Log logger = LogFactory.getLog(getClass());
private RestOperations restTemplate;
private String checkTokenEndpointUrl;
private String clientId;
private String clientSecret;
private String tokenName = "token";
private AccessTokenConverter tokenConverter = new DefaultAccessTokenConverter();
@Override
public OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException {
MultiValueMap<String, String> formData = new LinkedMultiValueMap<String, String>();
formData.add(tokenName, accessToken);
HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", getAuthorizationHeader(clientId, clientSecret));
Map<String, Object> map = postForMap(checkTokenEndpointUrl, formData, headers);
if (map.containsKey("error")) {
logger.debug("check_token returned error: " + map.get("error"));
throw new InvalidTokenException(accessToken);
}
Assert.state(map.containsKey("client_id"), "Client id must be present in response from auth server");
return tokenConverter.extractAuthentication(map);
}
@Override
public OAuth2AccessToken readAccessToken(String accessToken) {
throw new UnsupportedOperationException("Not supported: read access token");
}
}
3. ClientDetails ClientDetailsService
这两个概念简单的有些可怕,完全就是UserDetails和UserDetailsService的翻版。一个是对应user,一个是对应client。
client需要事先注册到授权服务器,这样授权服务器会根据client的授权请求获取clientId,secret等信息,进行验证后返回token。
3.1 ClientDetails
client的信息,存于授权服务器端,这样只需要知道客户端的clientId,就可以获取到客户端能访问哪些资源,是否需要密码,是否限制了scope,拥有的权限等等。
public interface ClientDetails extends Serializable {
String getClientId();
// client能访问的资源id
Set<String> getResourceIds();
// 验证client是否需要密码
boolean isSecretRequired();
String getClientSecret();
// client是否限制了scope
boolean isScoped();
// scope集合
Set<String> getScope();
// 根据哪些grantType验证通过client
Set<String> getAuthorizedGrantTypes();
// 注册成功后跳转的uri
Set<String> getRegisteredRedirectUri();
// client拥有的权限
Collection<GrantedAuthority> getAuthorities();
// client的token时效
Integer getAccessTokenValiditySeconds();
// client的refreshToken时效
Integer getRefreshTokenValiditySeconds();
// true:默认自动授权;false:需要用户确定才能授权
boolean isAutoApprove(String scope);
// 额外的信息
Map<String, Object> getAdditionalInformation();
}
3.2 ClientDetailsService
根据clientId获取clientDetails
public interface ClientDetailsService {
/**
* Load a client by the client id. This method must not return null.
*
* @param clientId The client id.
* @return The client details (never null).
* @throws ClientRegistrationException If the client account is locked, expired, disabled, or invalid for any other reason.
*/
ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException;
}
有两个子类 InMemoryClientDetailsService(内存) 和 JdbcClientDetailsService(数据库,OAUTH_CLIENT_DETAILS、OAUTH_CLIENT_TOKEN表等)。说白了就是一个是把ClientDetails存内存里,一个存数据库里(oauth_client_details表)。
一般在AuthServer中配置ClientDetailsServiceConfigurer。
1、配置JdbcDetailsService
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
/**
* 配置客户端 a configurer that defines the client details service.
* Client details can be initialized, or you can just refer to an existing store.
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.withClientDetails(clientDetails());
}
2、配置InMemoryClientDetailsService
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// @formatter:off
clients.inMemory().withClient("aiqiyi")
.resourceIds(QQ_RESOURCE_ID)
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT")
// , "get_fanslist"
.scopes("get_fanslist")
.secret("secret")
.redirectUris("http://localhost:8081/aiqiyi/qq/redirect")
.autoApprove(true)
.autoApprove("get_user_info")
.and()
.withClient("youku")
.resourceIds(QQ_RESOURCE_ID)
.authorizedGrantTypes("authorization_code", "refresh_token", "implicit")
.authorities("ROLE_CLIENT")
.scopes("get_user_info", "get_fanslist")
.secret("secret")
.redirectUris("http://localhost:8082/youku/qq/redirect");
// @formatter:on
}
3.3 ClientDetailsServiceBuilder
创建InMemoryClientDetailsService或者JdbcClientDetailsService,有内部类ClientBuilder。
public class ClientDetailsServiceBuilder<B extends ClientDetailsServiceBuilder<B>> extends
SecurityConfigurerAdapter<ClientDetailsService, B> implements SecurityBuilder<ClientDetailsService> {
private List<ClientBuilder> clientBuilders = new ArrayList<ClientBuilder>();
public InMemoryClientDetailsServiceBuilder inMemory() throws Exception {
return new InMemoryClientDetailsServiceBuilder();
}
public JdbcClientDetailsServiceBuilder jdbc() throws Exception {
return new JdbcClientDetailsServiceBuilder();
}
@SuppressWarnings("rawtypes")
public ClientDetailsServiceBuilder<?> clients(final ClientDetailsService clientDetailsService) throws Exception {
return new ClientDetailsServiceBuilder() {
@Override
public ClientDetailsService build() throws Exception {
return clientDetailsService;
}
};
}
// clients.inMemory().withClient("clientId").scopes().secret()...
public ClientBuilder withClient(String clientId) {
ClientBuilder clientBuilder = new ClientBuilder(clientId);
this.clientBuilders.add(clientBuilder);
return clientBuilder;
}
@Override
public ClientDetailsService build() throws Exception {
for (ClientBuilder clientDetailsBldr : clientBuilders) {
addClient(clientDetailsBldr.clientId, clientDetailsBldr.build());
}
return performBuild();
}
protected void addClient(String clientId, ClientDetails build) {
}
protected ClientDetailsService performBuild() {
throw new UnsupportedOperationException("Cannot build client services (maybe use inMemory() or jdbc()).");
}
public final class ClientBuilder {
// ...
public ClientDetailsServiceBuilder<B> and() {
return ClientDetailsServiceBuilder.this;
}
}
}
4. 资源服务器配置 ResourceServerConfigurerAdapter
配置哪些路径需要认证后才能访问,哪些不需要。自然就联想到了HttpSecurity(配置HttpSecurity就相当于配置了不同uri对应的filters)。
Spring Security中我们是这样配置WebSecurityConfigurerAdapter的。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter
{
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.anyRequest().authenticated()//所有请求必须登陆后访问
.and().httpBasic()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/index")
.failureUrl("/login?error")
.permitAll()//登录界面,错误界面可以直接访问
.and()
.logout().logoutUrl("/logout").logoutSuccessUrl("/login")
.permitAll().and().rememberMe();//注销请求可直接访问
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and()
.withUser("admin").password("password").roles("USER", "ADMIN");
}
}
作为资源服务器ResourceServerConfigurerAdapter,需要和@EnableResourceServer搭配,然后和上面一样需配置HttpSecurity就好了。还能配置ResourceServerSecurityConfigurer,设置tokenService等
/**
* 配置资源服务器
*/
@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Autowired
private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;
@Autowired
private CustomLogoutSuccessHandler customLogoutSuccessHandler;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.exceptionHandling()
.authenticationEntryPoint(customAuthenticationEntryPoint)
.and()
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(customLogoutSuccessHandler)
.and()
.authorizeRequests()
// hello路径允许直接访问
.antMatchers("/hello/").permitAll()
// secure路径需要验证后才能访问
.antMatchers("/secure/**").authenticated();
}
// 远程连接authServer服务
@Autowired
public RemoteTokenServices remoteTokenServices;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.tokenServices(remoteTokenServices);
}
}
5. 授权服务器配置 AuthorizationServerConfigurerAdapter
注册client信息,可以同时配置多个不同类型的client。
/**
* 配置认证服务器 @EnableAuthorizationServer自动注册到spring context中
*/
@EnableAuthorizationServer
protected static class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter implements EnvironmentAware {
private static final String ENV_OAUTH = "authentication.oauth.";
private static final String PROP_CLIENTID = "clientid";
private static final String PROP_SECRET = "secret";
private static final String PROP_TOKEN_VALIDITY_SECONDS = "tokenValidityInSeconds";
private RelaxedPropertyResolver propertyResolver;
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
return new JdbcTokenStore(dataSource);
}
@Autowired
@Qualifier("authenticationManagerBean")
private AuthenticationManager authenticationManager;
/**
* 可以设置tokenStore,tokenGranter,authenticationManager,requestFactory等接口使用什么继承类,但一般沿用默认的就好了
* 如果使用的是密码方式授权,则必须设置authenticationManager
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints)
throws Exception {
endpoints
.tokenStore(tokenStore())
.authenticationManager(authenticationManager);
}
/**
* 注册clients到授权服务器,这里是注册到内存中,且配置了scopes,authorities等信息
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients
.inMemory()
.withClient(propertyResolver.getProperty(PROP_CLIENTID))
.scopes("read", "write")
.authorities(Authorities.ROLE_ADMIN.name(), Authorities.ROLE_USER.name())
.authorizedGrantTypes("password", "refresh_token")
.secret(propertyResolver.getProperty(PROP_SECRET))
// 给客户端的token时效为1800秒
.accessTokenValiditySeconds(propertyResolver.getProperty(PROP_TOKEN_VALIDITY_SECONDS, Integer.class, 1800));
}
@Override
public void setEnvironment(Environment environment) {
this.propertyResolver = new RelaxedPropertyResolver(environment, ENV_OAUTH);
}
}
6.TokenEndPoint,AuthorizationEndPoint,CheckTokenEndPoint
6.1 TokenEndPoint
客户端post请求"/oauth/token",验证用户信息并获取OAuth2AccessToken,必须先经过client验证。这一步的最终目的是存储OAuth2AccessToken+OAuth2Authentication并返回OAuth2AccessToken。
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
...
// AuthorizationServerEndpointsConfigurer
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
6.2 AuthorizationEndPoint
这个一般只适用于authorization code模式,客户端请求authorization server中的/oauth/authorize(请求前先得登录oauth server获得authentication),验证client信息后根据redirect_uri请求重定向回client,同时带上code值。client附带code值再次向/oauth/token请求,返回accesstoken。
具体流程将在下章中介绍,这里只引出相关概念。
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// Pull out the authorization request first, using the OAuth2RequestFactory. All further logic should
// query off of the authorization request instead of referring back to the parameters map. The contents of the
// parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// The resolved redirect URI is either the redirect_uri from the parameters or the one from
// clientDetails. Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// We intentionally only validate the parameters requested by the client (ignoring any data that may have
// been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// Some systems may allow for approval decisions to be remembered or approved by default. Check for
// such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
// 生成code值并返回
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the session
// for approveOrDeny to use. That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
6.3 CheckTokenEndpoint
当采用RemoteTokenServices时,resouceServer无法自行验证access_token字符串是否正确,遂递交给另一个应用程序中的authserver里CheckTokenEndpoint(/oauth/check_token)进行检验,检验结果返回给resourceServer。
@RequestMapping(value = "/oauth/check_token")
@ResponseBody
public Map<String, ?> checkToken(@RequestParam("token") String value) {
OAuth2AccessToken token = resourceServerTokenServices.readAccessToken(value);
if (token == null) {
throw new InvalidTokenException("Token was not recognised");
}
if (token.isExpired()) {
throw new InvalidTokenException("Token has expired");
}
OAuth2Authentication authentication = resourceServerTokenServices.loadAuthentication(token.getValue());
Map<String, ?> response = accessTokenConverter.convertAccessToken(token, authentication);
return response;
}
7.总结
复习下这章讲到的知识点。
- 四大角色:ResouceServer AuthorizationServer client user
- OAuth2AccessToken OAuth2Authentiaction
- OAuth2Request TokenRequest AuthorizationRequest
- TokenGranter TokenStore TokenExtractor DefaultTokenServices RemoteTokenServices
- ResourceServerConfigurerAdapter AuthorizationServerConfigurerAdapter
- TokenEndPoint(/oauth/token) AuthorizationEndPoint(/oauth/authorize) CheckTokenEndpoint(/oauth/check_token)