OAuth2的原理應該從這張圖提及mysql
下面是相關的類圖web
首先咱們從請求認證開始http://127.0.0.1:63739/oauth/token?grant_type=password&client_id=system&client_secret=system&scope=app&username=admin&password=adminredis
返回值爲{
"access_token": "a18a9359-cfc0-4d29-a16d-7ea75388d0e9",
"token_type": "bearer",
"refresh_token": "21a20eb7-69dd-499d-bc65-36343bc4cc88",
"expires_in": 28799,
"scope": "app"
}spring
進入oauth2源碼TokenEndpoint,咱們能夠看到(加了註釋)sql
@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.app
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); } }
數據庫中數據以下dom
而咱們須要在ide
@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" }