Oauth介绍
OAuth全称是:Open Authentication
- 开放授权(OAuth)是一个开放标准,允许用户让第三方应用访问该用户在某一网站上存储的私密的资源(如照片,视频,联系人列表),而无需将用户名和密码提供给第三方应用。
- OAuth允许用户提供一个令牌,而不是用户名和密码来访问他们存放在特定服务提供者的数据。每一个令牌授权一个特定的网站在特定的时段内访问特定的资源。OAuth让用户可以授权第三方网站访问他们存储在另外服务提供者的某些特定信息,而非所有内容。
Oauth2.0授权模型
Oauth2.0支持四种授权模式:
1.密码模式(username&password)
- 这种模式类似于QQ登录等
- 支持refresh token
2.授权码模式(authorization code)
- 设计了auth code,通过auth code再获取token
- token放在服务端,不经历客户端,防止token泄露等
- 支持refresh token
3.简化模式(implicit)
- 这种模式比较授权码模式少了code环节,回调的url直接携带token
- 不支持refresh token
4.客户端模式(client credentials)
- 这种模式直接根据client的id和密钥即可获取token,无需用户参与
- 这种模式比较合适消费api的后端服务
- 不支持refresh token
简单介绍一下refresh token,access token即token申请到后是会存在过期时间,申请token的同时,授权服务器通常会伴随着refresh token进行授予(前提是支持refresh token的模式),当token过期时,就可以直接使用refresh token重新向授权服务器重新申请access token,就不需要重新验证。
整合Demo
授权访问的流程
要实现这个Demo,我们先从源码出发,从获取token的源头出发
package org.springframework.security.oauth2.provider.endpoint;
@FrameworkEndpoint
public class TokenEndpoint extends AbstractEndpoint {
private OAuth2RequestValidator oAuth2RequestValidator = new DefaultOAuth2RequestValidator();
private Set<HttpMethod> allowedRequestMethods;
public TokenEndpoint() {
this.allowedRequestMethods = new HashSet(Arrays.asList(HttpMethod.POST));
}
@RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.GET}
)
public ResponseEntity<OAuth2AccessToken> getAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!this.allowedRequestMethods.contains(HttpMethod.GET)) {
throw new HttpRequestMethodNotSupportedException("GET");
} else {
return this.postAccessToken(principal, parameters);
}
}
@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.");
} else {
String clientId = this.getClientId(principal);
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
} else {
if (authenticatedClient != null) {
this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
} else if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
} else {
if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
this.logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.emptySet());
}
if (this.isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
}
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
return this.getResponse(token);
}
}
}
}
}
...省略了部分异常处理器的声明...
protected String getClientId(Principal principal) {
Authentication client = (Authentication)principal;
if (!client.isAuthenticated()) {
throw new InsufficientAuthenticationException("The client is not authenticated.");
} else {
String clientId = client.getName();
if (client instanceof OAuth2Authentication) {
clientId = ((OAuth2Authentication)client).getOAuth2Request().getClientId();
}
return clientId;
}
}
private ResponseEntity<OAuth2AccessToken> getResponse(OAuth2AccessToken accessToken) {
HttpHeaders headers = new HttpHeaders();
headers.set("Cache-Control", "no-store");
headers.set("Pragma", "no-cache");
headers.set("Content-Type", "application/json;charset=UTF-8");
return new ResponseEntity(accessToken, headers, HttpStatus.OK);
}
}
授权码的获取逻辑,该类是AuthorizationEndpoint
@RequestMapping({"/oauth/authorize"})
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) {
AuthorizationRequest authorizationRequest = this.getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
} else if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
} else {
try {
if (principal instanceof Authentication && ((Authentication)principal).isAuthenticated()) {
ClientDetails client = this.getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
String redirectUriParameter = (String)authorizationRequest.getRequestParameters().get("redirect_uri");
String resolvedRedirect = this.redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException("A redirectUri must be either supplied or preconfigured in the ClientDetails");
} else {
authorizationRequest.setRedirectUri(resolvedRedirect);
this.oauth2RequestValidator.validateScope(authorizationRequest, client);
authorizationRequest = this.userApprovalHandler.checkForPreApproval(authorizationRequest, (Authentication)principal);
boolean approved = this.userApprovalHandler.isApproved(authorizationRequest, (Authentication)principal);
authorizationRequest.setApproved(approved);
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return this.getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(this.getAuthorizationCodeResponse(authorizationRequest, (Authentication)principal));
}
}
model.put("authorizationRequest", authorizationRequest);
model.put("org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint.ORIGINAL_AUTHORIZATION_REQUEST", this.unmodifiableMap(authorizationRequest));
return this.getUserApprovalPageResponse(model, authorizationRequest, (Authentication)principal);
}
} else {
throw new InsufficientAuthenticationException("User must be authenticated with Spring Security before authorization can be completed.");
}
} catch (RuntimeException var11) {
sessionStatus.setComplete();
throw var11;
}
}
}
小总结: 获取Token主要做了这几件事:
- 是否获得授权码
- 通过ClientDetailsService加载客户端信息
- 构造TokenRequest请求
- 调用TokenGranter的grant()得到Token
Demo的实现
AuthorizationServerConfig (授权服务器的Endpoint配置)
@Component
@Configuration
@EnableAuthorizationServer
@AutoConfigureAfter(AuthorizationServerEndpointsConfigurer.class)
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private ValidateCodeService validateCodeService ;
@Autowired(required = false)
private TokenStore tokenStore;
@Autowired(required = false)
private JwtAccessTokenConverter jwtAccessTokenConverter;
@Autowired
private WebResponseExceptionTranslator webResponseExceptionTranslator;
@Autowired
private RedisClientDetailsService redisClientDetailsService;
@Autowired(required = false)
private RandomValueAuthorizationCodeServices authorizationCodeServices;
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.tokenStore(tokenStore).authenticationManager(authenticationManager)
.userDetailsService(userDetailsService);
if (tokenStore instanceof JwtTokenStore) {
endpoints.accessTokenConverter(jwtAccessTokenConverter);
}
endpoints.authorizationCodeServices(authorizationCodeServices);
endpoints.exceptionTranslator(webResponseExceptionTranslator);
ClientDetailsService clientDetails = endpoints.getClientDetailsService();
AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
OAuth2RequestFactory requestFactory = endpoints.getOAuth2RequestFactory();
List<TokenGranter> tokenGranters = new ArrayList<>();
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
tokenGranters.add(new PasswordEnhanceTokenGranter(authenticationManager, tokenServices,clientDetails, requestFactory,validateCodeService));
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,requestFactory));
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
tokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory));
tokenGranters.add(new SMSCodeTokenGranter( userDetailsService, validateCodeService , tokenServices, clientDetails, requestFactory));
endpoints.tokenGranter(new CompositeTokenGranter(tokenGranters));
}
- 上面已经AccessToken是通过tokenGranters中具体的Granter生成的,我们这里讲其中的一种授权码模式如何生成的
AuthorizationCodeTokenGranter类
new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,requestFactory)
...
public class AuthorizationCodeTokenGranter extends AbstractTokenGranter {
... 省略部分代码 ...
private static final String GRANT_TYPE = "authorization_code";
private final AuthorizationCodeServices authorizationCodeServices;
实际上是初始化了tokenServices(token生成服务),authorizationCodeServices(授权码生成服务),clientDetailsService(客户端信息服务),requestFactory(请求构造器)
public AuthorizationCodeTokenGranter(AuthorizationServerTokenServices tokenServices, AuthorizationCodeServices authorizationCodeServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
this(tokenServices, authorizationCodeServices, clientDetailsService, requestFactory, "authorization_code");
}
protected AuthorizationCodeTokenGranter(AuthorizationServerTokenServices tokenServices, AuthorizationCodeServices authorizationCodeServices, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory, String grantType) {
super(tokenServices, clientDetailsService, requestFactory, grantType);
this.authorizationCodeServices = authorizationCodeServices;
}
}
- 再来看AbstractTokenGranter类,这里实现了具体的逻辑
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
} else {
String clientId = tokenRequest.getClientId();
ClientDetails client = this.clientDetailsService.loadClientByClientId(clientId);
this.validateGrantType(grantType, client);
if (this.logger.isDebugEnabled()) {
this.logger.debug("Getting access token for: " + clientId);
}
return this.getAccessToken(client, tokenRequest);
}
}
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
}
- 由于AuthorizationServerTokenServices是个接口,我们查看DefaultTokenServices的createAccessToken
@Transactional 开启事务
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (!existingAccessToken.isExpired()) {
this.tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
this.tokenStore.removeRefreshToken(refreshToken);
}
this.tokenStore.removeAccessToken(existingAccessToken);
}
if (refreshToken == null) {
refreshToken = this.createRefreshToken(authentication);
} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = this.createRefreshToken(authentication);
}
}
OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
this.tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
this.tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
- 继续进入该类的重载方法createAccessToken
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = this.getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return (OAuth2AccessToken)(this.accessTokenEnhancer != null ? this.accessTokenEnhancer.enhance(token, authentication) : token);
}
附上其他部分的代码
RedisAuthorizationCodeServices
这个类主要继承了RandomValueAuthorizationCodeServices 类,该类和TokenServices的作用差不多,不过一个是生成Token,一个是生成授权码,这里就不多说了
RandomValueAuthorizationCodeServices类
private RandomValueStringGenerator generator = new RandomValueStringGenerator();
public String createAuthorizationCode(OAuth2Authentication authentication) {
String code = this.generator.generate();
this.store(code, authentication);
return code;
}
public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {
private RedisTemplate<String,Object> redisTemplate;
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@Override
protected void store(String code, OAuth2Authentication authentication) {
redisTemplate.opsForValue().set(redisKey(code), authentication, 10, TimeUnit.MINUTES);
}
@Override
protected OAuth2Authentication remove(final String code) {
String codeKey =redisKey(code) ;
OAuth2Authentication token = (OAuth2Authentication) redisTemplate.opsForValue().get(codeKey) ;
this.redisTemplate.delete(codeKey);
return token;
}
private String redisKey(String code) {
return "oauth:code:" + code;
}
}
RedisClientDetailsService
这个是存储客户端信息,建立数据库连接的类
@Slf4j
@SuppressWarnings("all")
public class RedisClientDetailsService extends JdbcClientDetailsService {
... 省略查询数据库的语句 ...
private RedisTemplate<String,Object> redisTemplate;
private final JdbcTemplate jdbcTemplate;
public RedisTemplate<String, Object> getRedisTemplate() {
return redisTemplate;
}
public void setRedisTemplate(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public RedisClientDetailsService(DataSource dataSource) {
super(dataSource);
this.jdbcTemplate = new JdbcTemplate(dataSource);
setSelectClientDetailsSql(SELECT_CLIENT_DETAILS_SQL) ;
setFindClientDetailsSql(SELECT_FIND_STATEMENT) ;
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
ClientDetails clientDetails = null;
try {
String value = (String) redisTemplate.boundHashOps(UaaConstant.CACHE_CLIENT_KEY).get(clientId);
if (StringUtils.isBlank(value)) {
clientDetails = cacheAndGetClient(clientId);
} else {
clientDetails = JSONObject.parseObject(value, BaseClientDetails.class);
}
} catch (Exception e) {
log.error("clientId:{},{}", clientId, clientId );
throw new InvalidClientException ("应用获取失败"){};
}
return clientDetails;
}
private ClientDetails cacheAndGetClient(String clientId) {
ClientDetails clientDetails = null ;
try {
clientDetails = jdbcTemplate.queryForObject(SELECT_CLIENT_DETAILS_SQL, new ClientDetailsRowMapper(), clientId);
if (clientDetails != null) {
redisTemplate.boundHashOps(UaaConstant.CACHE_CLIENT_KEY).put(clientId, JSONObject.toJSONString(clientDetails));
log.info("缓存clientId:{},{}", clientId, clientDetails);
}
}catch (EmptyResultDataAccessException e) {
log.error("clientId:{},{}", clientId, clientId );
throw new AuthenticationException ("应用不存在"){};
} catch (NoSuchClientException e){
log.error("clientId:{},{}", clientId, clientId );
throw new AuthenticationException ("应用不存在"){};
}catch (InvalidClientException e) {
throw new AuthenticationException ("应用状态不合法"){};
}
return clientDetails;
}
@Override
public void updateClientDetails(ClientDetails clientDetails) throws NoSuchClientException {
super.updateClientDetails(clientDetails);
cacheAndGetClient(clientDetails.getClientId());
}
@Override
public void updateClientSecret(String clientId, String secret) throws NoSuchClientException {
super.updateClientSecret(clientId, secret);
cacheAndGetClient(clientId);
}
@Override
public void removeClientDetails(String clientId) throws NoSuchClientException {
super.removeClientDetails(clientId);
removeRedisCache(clientId);
}
private void removeRedisCache(String clientId) {
redisTemplate.boundHashOps(UaaConstant.CACHE_CLIENT_KEY).delete(clientId);
}
public void loadAllClientToCache() {
if (redisTemplate.hasKey(UaaConstant.CACHE_CLIENT_KEY)) {
return;
}
log.info("将oauth_client_details全表刷入redis");
List<ClientDetails> list = this.listClientDetails();
if (CollectionUtils.isEmpty(list)) {
log.error("oauth_client_details表数据为空,请检查");
return;
}
list.parallelStream().forEach(client -> {
redisTemplate.boundHashOps(UaaConstant.CACHE_CLIENT_KEY).put(client.getClientId(), JSONObject.toJSONString(client));
});
}
public List<ClientDetails> listClientDetails() {
return jdbcTemplate.query(SELECT_FIND_STATEMENT, new ClientDetailsRowMapper());
}
private static class ClientDetailsRowMapper implements RowMapper<ClientDetails> {
private com.open.capacity.uaa.server.json.JsonMapper mapper = createJsonMapper();
@Override
public ClientDetails mapRow(ResultSet rs, int rowNum) throws SQLException {
DefaultClientDetails details = new DefaultClientDetails(rs.getString(1), rs.getString(3), rs.getString(4),
rs.getString(5), rs.getString(7), rs.getString(6));
details.setClientSecret(rs.getString(2));
if (rs.getObject(8) != null) {
details.setAccessTokenValiditySeconds(rs.getInt(8));
}
if (rs.getObject(9) != null) {
details.setRefreshTokenValiditySeconds(rs.getInt(9));
}
String json = rs.getString(10);
if (json != null) {
try {
Map<String, Object> additionalInformation = mapper.read(json, Map.class);
details.setAdditionalInformation(additionalInformation);
}
catch (Exception e) {
log.warn("Could not decode JSON for additional information: " + details, e);
}
}
String scopes = rs.getString(11);
long ifLimit = rs.getLong(12) ;
details.setIfLimit(ifLimit);
long limitCount = rs.getLong(13) ;
details.setLimitCount(limitCount);
details.setId( rs.getLong(14));
if (scopes != null) {
details.setAutoApproveScopes(org.springframework.util.StringUtils.commaDelimitedListToSet(scopes));
}
return details;
}
}
private static com.open.capacity.uaa.server.json.JsonMapper createJsonMapper() {
if (ClassUtils.isPresent("org.codehaus.jackson.map.ObjectMapper", null)) {
return new com.open.capacity.uaa.server.json.JacksonMapper();
}
else if (ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", null)) {
return new com.open.capacity.uaa.server.json.Jackson2Mapper();
}
return new com.open.capacity.uaa.server.json.NotSupportedJsonMapper();
}
}
总结
- 结合Oauth2的特性和Spring Security的部分源码的讲解,深入的了解token的创建流程。
- 主要分为客户端信息的存储 -> 授权码请求的发起 -> 授权码的生成 -> 授权码的存储 -> accessToken和RefreshToken的生成存储 -> 接口验证通过 -> 访问服务
|