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" }