OAuth2的原理应该从这张图说起
下面是相关的类图
首先我们从请求认证开始http://127.0.0.1:63739/oauth/token?grant_type=password&client_id=system&client_secret=system&scope=app&username=admin&password=admin
返回值为{ "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<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);
//从数据库表oauth_client_details,通过clientId获取clientDetails,clientDetails是一个序列化类
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
//产生一个带参数请求的Request
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")));
}
//对这种登录方式进行授权,产生一个通过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分钟<br>
* value为OAuth2Authentication序列化后的字节<br>
* 因为OAuth2Authentication没有无参构造函数<br>
* redisTemplate.opsForValue().set(key, value, timeout, unit);
* 这种方式直接存储的话,redisTemplate.opsForValue().get(key)的时候有些问题,
* 所以这里采用最底层的方式存储,get的时候也用最底层的方式获取
*/
@Override
protected void store(String code, OAuth2Authentication authentication) {
redisTemplate.execute(new RedisCallback<Long>() {
@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<OAuth2Authentication>() {
@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
http://127.0.0.1:51451/oauth/token?grant_type=refresh_token&client_id=system&client_secret=system&scope=app&refresh_token=845d549c-6e73-4bdc-a30d-6991f47353f9
返回{ "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<MediaType> list = new ArrayList<>();
list.add(MediaType.APPLICATION_JSON_UTF8);
fastJsonConverter.setSupportedMediaTypes(list);
converters.add(fastJsonConverter);
}
}
而返回的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" }