OAuth2的原理应该从这张图说起
下面是相关的类图
返回值为{
"access_token": "a18a9359-cfc0-4d29-a16d-7ea75388d0e9",
"token_type": "bearer",
"refresh_token": "21a20eb7-69dd-499d-bc65-36343bc4cc88",
"expires_in": 28799,
"scope": "app"
}
进入oauth2源码TokenEndpoint,我们可以看到(加了注释)
@RequestMapping(
value = {"/oauth/token"},
method = {RequestMethod.POST}
)
public ResponseEntity
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")));
}
//对这种登录方式进行授权,产生一个通过token,OAuth2AccessToken是一个序列化类 OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest); if(token == null) { throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType()); } else { return this.getResponse(token); } } } } }
其中ClientDetailsService是一个接口,它决定了从哪里获取clientDetails,它有2个实现类,一个是从内存中InMemoryClientDetailsService
,一个是从数据库中JdbcClientDetailsService.
.我们主要讲从数据库中获取clientDetails.
public JdbcClientDetailsService(DataSource dataSource) { this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT; this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?"; this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)"; this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?"; this.passwordEncoder = NoOpPasswordEncoder.getInstance(); Assert.notNull(dataSource, "DataSource required"); this.jdbcTemplate = new JdbcTemplate(dataSource); this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate)); }
public void setPasswordEncoder(PasswordEncoder passwordEncoder) { this.passwordEncoder = passwordEncoder; }
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException { try { ClientDetails details = (ClientDetails)this.jdbcTemplate.queryForObject(this.selectClientDetailsSql, new JdbcClientDetailsService.ClientDetailsRowMapper(), new Object[]{clientId}); return details; } catch (EmptyResultDataAccessException var4) { throw new NoSuchClientException("No client with requested id: " + clientId); } }
数据库中数据如下
而我们需要在
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
中配置使用数据库,而不是在内存中获取clientDetails
@Autowired private DataSource dataSource;
使用Resource的yml文件中dataSource配置(以下使用的是mysql 8的配置)
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://XXX.XXX.XXX.XXX:3306/cloud_oauth?useSSL=FALSE&serverTimezone=UTC username: root password: xxxxxx type: com.alibaba.druid.pool.DruidDataSource filters: stat maxActive: 20 initialSize: 1 maxWait: 60000 minIdle: 1 timeBetweenEvictionRunsMillis: 60000 minEvictableIdleTimeMillis: 300000 validationQuery: select 'x' testWhileIdle: true testOnBorrow: false testOnReturn: false poolPreparedStatements: true maxOpenPreparedStatements: 20
以下是使用dataSource来配置jdbc,请注意注释掉的是内存配置,如果使用内存配置,将不会使用数据库配置.
@Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { // clients.inMemory().withClient("system").secret("system") // .authorizedGrantTypes("password", "authorization_code", "refresh_token").scopes("app") // .accessTokenValiditySeconds(3600);
clients.jdbc(dataSource);
}
另外一个重点的地方就是授权登录OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);TokenGranter也是一个接口,有一个抽象类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)); }
getAccessToken里有一个tokenServices.createAccessToken.tokenServices的定义为private final AuthorizationServerTokenServices tokenServices;AuthorizationServerTokenServices也是一个接口.实现类只有1个
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices, ConsumerTokenServices, InitializingBean
而且这个实现类同时实现了很多个接口.
@Transactional public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException { //如果不是第一次登陆,从tokenStore取出通过token;如果是第一次登陆为null OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication); OAuth2RefreshToken refreshToken = null; if(existingAccessToken != null) { if(!existingAccessToken.isExpired()) { //如果不是第一次登陆未过期,将token重新存入tokenStore this.tokenStore.storeAccessToken(existingAccessToken, authentication); return existingAccessToken; } //如果已经过期,移除token if(existingAccessToken.getRefreshToken() != null) { refreshToken = existingAccessToken.getRefreshToken(); this.tokenStore.removeRefreshToken(refreshToken); }
this.tokenStore.removeAccessToken(existingAccessToken);
}
//如果是第一次登陆,先创建RefreshToken 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); } } //创建token OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken); //将token存入tokenStore this.tokenStore.storeAccessToken(accessToken, authentication); refreshToken = accessToken.getRefreshToken(); if(refreshToken != null) { //将refreshToken存入tokenStore this.tokenStore.storeRefreshToken(refreshToken, authentication); }
return accessToken;
}
创建一个UUID的RefreshToken
private OAuth2RefreshToken createRefreshToken(OAuth2Authentication authentication) { if(!this.isSupportRefreshToken(authentication.getOAuth2Request())) { return null; } else { int validitySeconds = this.getRefreshTokenValiditySeconds(authentication.getOAuth2Request()); String value = UUID.randomUUID().toString(); return (OAuth2RefreshToken)(validitySeconds > 0?new DefaultExpiringOAuth2RefreshToken(value, new Date(System.currentTimeMillis() + (long)validitySeconds * 1000L)):new DefaultOAuth2RefreshToken(value)); } }
创建一个UUID的token
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);
}
其中TokenStore是一个接口,有5个实现类InMemoryTokenStore内存存储,JdbcTokenStore数据库存储,JwkTokenStore,JwtTokenStore,RedisTokenStore Redis存储,我们主要讲Redis存储.
redis存储token的代码
public void storeAccessToken(OAuth2AccessToken token, OAuth2Authentication authentication) { byte[] serializedAccessToken = this.serialize((Object)token); byte[] serializedAuth = this.serialize((Object)authentication); byte[] accessKey = this.serializeKey("access:" + token.getValue()); byte[] authKey = this.serializeKey("auth:" + token.getValue()); byte[] authToAccessKey = this.serializeKey("auth_to_access:" + this.authenticationKeyGenerator.extractKey(authentication)); byte[] approvalKey = this.serializeKey("uname_to_access:" + getApprovalKey(authentication)); byte[] clientId = this.serializeKey("client_id_to_access:" + authentication.getOAuth2Request().getClientId()); RedisConnection conn = this.getConnection();
try {
conn.openPipeline();
if(springDataRedis\_2\_0) {
try {
this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{accessKey, serializedAccessToken});
this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{authKey, serializedAuth});
this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{authToAccessKey, serializedAccessToken});
} catch (Exception var24) {
throw new RuntimeException(var24);
}
} else {
conn.set(accessKey, serializedAccessToken);
conn.set(authKey, serializedAuth);
conn.set(authToAccessKey, serializedAccessToken);
}
if(!authentication.isClientOnly()) {
conn.rPush(approvalKey, new byte\[\]\[\]{serializedAccessToken});
}
conn.rPush(clientId, new byte\[\]\[\]{serializedAccessToken});
if(token.getExpiration() != null) {
int seconds = token.getExpiresIn();
conn.expire(accessKey, (long)seconds);
conn.expire(authKey, (long)seconds);
conn.expire(authToAccessKey, (long)seconds);
conn.expire(clientId, (long)seconds);
conn.expire(approvalKey, (long)seconds);
}
OAuth2RefreshToken refreshToken = token.getRefreshToken();
if(refreshToken != null && refreshToken.getValue() != null) {
byte\[\] refresh = this.serialize(token.getRefreshToken().getValue());
byte\[\] auth = this.serialize(token.getValue());
byte\[\] refreshToAccessKey = this.serializeKey("refresh\_to\_access:" + token.getRefreshToken().getValue());
byte\[\] accessToRefreshKey = this.serializeKey("access\_to\_refresh:" + token.getValue());
if(springDataRedis\_2\_0) {
try {
this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{refreshToAccessKey, auth});
this.redisConnectionSet\_2\_0.invoke(conn, new Object\[\]{accessToRefreshKey, refresh});
} catch (Exception var23) {
throw new RuntimeException(var23);
}
} else {
conn.set(refreshToAccessKey, auth);
conn.set(accessToRefreshKey, refresh);
}
if(refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiringRefreshToken = (ExpiringOAuth2RefreshToken)refreshToken;
Date expiration = expiringRefreshToken.getExpiration();
if(expiration != null) {
int seconds = Long.valueOf((expiration.getTime() - System.currentTimeMillis()) / 1000L).intValue();
conn.expire(refreshToAccessKey, (long)seconds);
conn.expire(accessToRefreshKey, (long)seconds);
}
}
}
conn.closePipeline();
} finally {
conn.close();
}
}
因此,我们在
@Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter
中,需要实例化接口TokenStore为RedisTokenStore.
@Autowired private RedisConnectionFactory redisConnectionFactory;
@Autowired private AuthenticationManager authenticationManager;
@Bean public TokenStore tokenStore() { return new RedisTokenStore(redisConnectionFactory); }
并且具体实现
@Autowired private RedisAuthorizationCodeServices redisAuthorizationCodeServices;
@Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(this.authenticationManager); endpoints.tokenStore(tokenStore()); endpoints.authorizationCodeServices(redisAuthorizationCodeServices); }
以上就是把authenticationManager,tokenStore(),redisAuthorizationCodeServices给配置到endpoints中.
@Service public class RedisAuthorizationCodeServices extends RandomValueAuthorizationCodeServices {
@Autowired private RedisTemplate<Object, Object> redisTemplate;
/**
* 存储code到redis,并设置过期时间,10分钟
* value为OAuth2Authentication序列化后的字节
* 因为OAuth2Authentication没有无参构造函数
* redisTemplate.opsForValue().set(key, value, timeout, unit);
* 这种方式直接存储的话,redisTemplate.opsForValue().get(key)的时候有些问题,
* 所以这里采用最底层的方式存储,get的时候也用最底层的方式获取
*/
@Override
protected void store(String code, OAuth2Authentication authentication) {
redisTemplate.execute(new RedisCallback
@Override
public Long doInRedis(RedisConnection connection) throws DataAccessException {
connection.set(codeKey(code).getBytes(), SerializationUtils.serialize(authentication),
Expiration.from(10, TimeUnit.MINUTES), SetOption.UPSERT);
return 1L;
}
});
}
@Override
protected OAuth2Authentication remove(final String code) {
OAuth2Authentication oAuth2Authentication = redisTemplate.execute(new RedisCallback
@Override
public OAuth2Authentication doInRedis(RedisConnection connection) throws DataAccessException {
byte\[\] keyByte = codeKey(code).getBytes();
byte\[\] valueByte = connection.get(keyByte);
if (valueByte != null) {
connection.del(keyByte);
return SerializationUtils.deserialize(valueByte);
}
return null;
}
});
return oAuth2Authentication;
}
/** * 拼装redis中key的前缀 * * @param code * @return */ private String codeKey(String code) { return "oauth2:codes:" + code; } }
我们可以看到redis里大概是这个样子.
最后我们可以用refreshToken来刷新token
返回{
"access_token": "923c25aa-71f1-4dbf-9e48-46543d8a8048",
"token_type": "bearer",
"refresh_token": "845d549c-6e73-4bdc-a30d-6991f47353f9",
"expires_in": 28799,
"scope": "app"
}
这样过期时间就满了
另外我们要实现一个
public interface UserDetailsService { UserDetails loadUserByUsername(String var1) throws UsernameNotFoundException; }
的接口.UserDetails是一个继承了Serializable的接口.
@Service public class UserDetailServiceImpl implements UserDetailsService
我们用一个类来实现UserDetailsService接口
@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // 为了支持多类型登录,这里username后面拼装上登录类型,如username|type String[] params = username.split("\\|"); username = params[0];// 真正的用户名 //数据库查询,LoginAppUser是一个实现了UserDetails接口的类 LoginAppUser loginAppUser = userClient.findByUsername(username); if (loginAppUser == null) { throw new AuthenticationCredentialsNotFoundException("用户不存在"); } else if (!loginAppUser.isEnabled()) { throw new DisabledException("用户已作废"); }
return loginAppUser;
}
最后是在Spring Security的安全配置中,对整个Web进行配置
@EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true) public class SecurityConfig extends WebSecurityConfigurerAdapter
@Autowired public UserDetailsService userDetailsService;
@Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { return new BCryptPasswordEncoder(); }
@Autowired public void globalUserDetails(AuthenticationManagerBuilder auth) throws Exception { //auth.inMemoryAuthentication().withUser("user").password("password").roles("USER").and().withUser("admin") // .password("password").roles("USER", "ADMIN"); auth.userDetailsService(userDetailsService).passwordEncoder(bCryptPasswordEncoder()); }
上面的注释同样是内存注释,而我们是使用数据库来校验用户名,密码.
另外如果配置了FastJson为Web的Json解析器的话,Json的日期格式需要作出调整,否则在Feign调用user-center时会报日期无法解析的错误,OAuth中心和User中心都要做如下设置
@Configuration @EnableWebMvc public class WebConfig implements WebMvcConfigurer {
@Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
FastJsonHttpMessageConverter fastJsonConverter = new FastJsonHttpMessageConverter();
FastJsonConfig config = new FastJsonConfig();
config.setCharset(Charset.forName("UTF-8"));
config.setDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); // config.setSerializerFeatures(SerializerFeature.WriteMapNullValue);
fastJsonConverter.setFastJsonConfig(config);
List
而返回的token格式也会有所变化
{
"additionalInformation": {},
"expiration": "2019-05-28T00:22:36.065+0800",
"expired": false,
"expiresIn": 28799,
"refreshToken": {
"expiration": "2019-06-26T16:22:36.053+0800",
"value": "b535f2bc-29ce-493b-b562-92271594880a"
},
"scope": [
"app"
],
"tokenType": "bearer",
"value": "374f96bd-dd6f-4382-a92f-ee417f81b850"
}