在上一節我們講述的配置是把授權碼存儲在redis中,把相應的請求的路徑用使用in-memory存儲 ,這個是放在了內存中,但是實際開發我們的數據希望是從數據表中查詢的,那應該怎么做呢?
1.回顧in-memory存儲
/**
* inMemory是存儲到內存中 並未到數據庫
*/
clients.inMemory()
//client Id
.withClient("normal-app")
.authorizedGrantTypes("authorization_code", "implicit")
.authorities("ROLE_CLIENT")
.scopes("read","write")
.resourceIds(resourceId)
.accessTokenValiditySeconds(accessTokenValiditySeconds)//授權碼存活時間
.and()
.withClient("trusted-app")
.authorizedGrantTypes("client_credentials", "password")
.authorities("ROLE_TRUSTED_CLIENT")
.scopes("read", "write")
.resourceIds(resourceId)
.accessTokenValiditySeconds(accessTokenValiditySeconds)
.secret("secret");
如果使用的是這種方式,我們對應的授權碼的請求路徑如下:
http://localhost:8787/oauth/authorize?client_id=normal-app&response_type=code&scope=read&redirect_uri=/resources/user
相應的參數請對照上
然后我們使用的是jwt的令牌方式,相應的請求路徑如下:
http://localhost:8787/oauth/token?code=r8YBUL&grant_type=authorization_code&client_id=normal-app&redirect_uri=/resources/user
這個是放在內存中的存儲方式
2.如果我需要從數據庫讀取相應的字段的參數 可如下配置:
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//默認值InMemoryTokenStore對於單個服務器是完全正常的(即,在發生故障的情況下,低流量和熱備份備份服務器)。大多數項目可以從這里開始,也可以在開發模式下運行,以便輕松啟動沒有依賴關系的服務器。
//這JdbcTokenStore是同一件事的JDBC版本,它將令牌數據存儲在關系數據庫中。如果您可以在服務器之間共享數據庫,則可以使用JDBC版本,如果只有一個,則擴展同一服務器的實例,或者如果有多個組件,則授權和資源服務器。要使用JdbcTokenStore你需要“spring-jdbc”的類路徑。
//這個地方指的是從jdbc查出數據來存儲
clients.withClientDetails(clientDetails());
}
這里可以看到我們是把之前的從內存讀取的方式給去掉了,取而代之的是clientDetails()這個方法,然后我們看下這個方法:
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
只需配置這個bean即可 但是我們的datasource是在yml配置文件中配置好了的,只需要注入:
import javax.sql.DataSource;
@Resource
private DataSource dataSource;
但是這里還沒完,我們首先要講下JdbcClientDetailsService是如何從數據庫讀取的,我們可以點擊進入查看相應的源碼,如下所示:
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));
}
我們可以看到,他自己是有一個默認的字段的表的,里面有相應的查詢的方法,所以我們需要建立一個這樣的表,sql如下:
-- ----------------------------
-- Table structure for oauth_client_details 將請求的路徑存在數據表
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details` (
`client_id` varchar(48) NOT NULL,
`resource_ids` varchar(256) DEFAULT NULL,
`client_secret` varchar(256) DEFAULT NULL,
`scope` varchar(256) DEFAULT NULL,
`authorized_grant_types` varchar(256) DEFAULT NULL,
`web_server_redirect_uri` varchar(256) DEFAULT NULL,
`authorities` varchar(256) DEFAULT NULL,
`access_token_validity` int(11) DEFAULT NULL,
`refresh_token_validity` int(11) DEFAULT NULL,
`additional_information` varchar(4096) DEFAULT NULL,
`autoapprove` varchar(256) DEFAULT NULL,
PRIMARY KEY (`client_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
這個是默認的類的表,一般用它默認的即可,我們這邊就需要根據以上的字段配置相關的內容,如下:
這里配置好了之后我們的訪問路徑為:
//步驟:客戶端向認證服務器申請令牌
http://localhost:8787/oauth/token?client_id=normal-app&grant_type=authorization_code&code=1oCj8e&redirect_uri=http://localhost:8787/resources/user
然后令牌的訪問路徑為:
//拿到令牌后訪問資源:
http://localhost:8787/resources/user?access_token=9d62c7b0-780e-4c6a-ad5a-56d79a089342
記得code要換成上一步生成的code
3.掉坑回顧:
之前我們用的jwt來存儲令牌token,后來我發現怎么也不出現jwttoken,經過多次檢查發現了錯誤,代碼如下:
package urity.demo.oauth2;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.security.SecurityProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.AccessTokenConverter;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import urity.demo.service.RedisAuthenticationCodeServices;
import urity.demo.support.MyUserDetailService;
import javax.annotation.Resource;
import javax.sql.DataSource;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Resource
private DataSource dataSource;
@Value("${resource.id:spring-boot-application}")//默認是spring-boot-application
private String resourceId;
@Value("${access_token.validity_period:36000}")
private int accessTokenValiditySeconds = 36000;
//認證管理 很重要 如果security版本高可能會出坑哦
@Resource
private AuthenticationManager authenticationManager;
@Resource
private RedisAuthenticationCodeServices redisAuthenticationCodeServices;
@Resource
private MyUserDetailService myUserDetailService;
//security
//定義令牌端點上的安全約束。
@Override
public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {
oauthServer.tokenKeyAccess("isAnonymous() || hasAuthority('ROLE_TRUSTED_CLIENT')");
oauthServer.checkTokenAccess("hasAuthority('ROLE_TRUSTED_CLIENT')");
}
//這個是定義授權的請求的路徑的Bean
@Bean
public ClientDetailsService clientDetails() {
return new JdbcClientDetailsService(dataSource);
}
// @Bean // 聲明TokenStore實現
// public JdbcTokenStore jdbcTokenStore() {
// return new JdbcTokenStore(dataSource);
// }
//將ClientDetailsServiceConfigurer(從您的回調AuthorizationServerConfigurer)可以用來在內存或JDBC實現客戶的細節服務來定義的。客戶端的重要屬性是
//clientId:(必填)客戶端ID。
//secret:(可信客戶端需要)客戶機密碼(如果有)。沒有可不填
//scope:客戶受限的范圍。如果范圍未定義或為空(默認值),客戶端不受范圍限制。read write all
//authorizedGrantTypes:授予客戶端使用授權的類型。默認值為空。
//authorities授予客戶的授權機構(普通的Spring Security權威機構)。
//客戶端的詳細信息可以通過直接訪問底層商店(例如,在數據庫表中JdbcClientDetailsService)或通過ClientDetailsManager接口(這兩種實現ClientDetailsService也實現)來更新運行的應用程序。
//注意:JDBC服務的架構未與庫一起打包(因為在實踐中可能需要使用太多變體)
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
//默認值InMemoryTokenStore對於單個服務器是完全正常的(即,在發生故障的情況下,低流量和熱備份備份服務器)。大多數項目可以從這里開始,也可以在開發模式下運行,以便輕松啟動沒有依賴關系的服務器。
//這JdbcTokenStore是同一件事的JDBC版本,它將令牌數據存儲在關系數據庫中。如果您可以在服務器之間共享數據庫,則可以使用JDBC版本,如果只有一個,則擴展同一服務器的實例,或者如果有多個組件,則授權和資源服務器。要使用JdbcTokenStore你需要“spring-jdbc”的類路徑。
// /**
// * inMemory是存儲到內存中 並未到數據庫
// */
// clients.inMemory()
// //client Id
// .withClient("normal-app")
// .authorizedGrantTypes("authorization_code", "implicit")
// .authorities("ROLE_CLIENT")
// .scopes("read","write")
// .resourceIds(resourceId)
// .accessTokenValiditySeconds(accessTokenValiditySeconds)//授權碼存活時間
// .and()
// .withClient("trusted-app")
// .authorizedGrantTypes("client_credentials", "password")
// .authorities("ROLE_TRUSTED_CLIENT")
// .scopes("read", "write")
// .resourceIds(resourceId)
// .accessTokenValiditySeconds(accessTokenValiditySeconds)
// .secret("secret");
//這個地方指的是從jdbc查出數據來存儲
clients.withClientDetails(clientDetails());
}
//AuthorizationEndpoint可以通過以下方式配置支持的授權類型AuthorizationServerEndpointsConfigurer。默認情況下,所有授權類型均受支持,除了密碼(有關如何切換它的詳細信息,請參見下文)。以下屬性會影響授權類型:
//authenticationManager:通過注入密碼授權被打開AuthenticationManager。
//userDetailsService:如果您注入UserDetailsService或者全局配置(例如a GlobalAuthenticationManagerConfigurer),則刷新令牌授權將包含對用戶詳細信息的檢查,以確保該帳戶仍然活動
//authorizationCodeServices:定義AuthorizationCodeServices授權代碼授權的授權代碼服務(實例)。
//implicitGrantService:在批准期間管理狀態。
//tokenGranter:(TokenGranter完全控制授予和忽略上述其他屬性)
//在XML授予類型中包含作為子元素authorization-server。
/**
* /oauth/authorize您可以從該請求中獲取所有數據,
* 然后根據需要進行渲染,
* 然后所有用戶需要執行的操作都是回復有關批准或拒絕授權的信息。
* 請求參數直接傳遞給您UserApprovalHandler,
* AuthorizationEndpoint所以您可以隨便解釋數據
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints.authenticationManager(this.authenticationManager);
endpoints.accessTokenConverter(accessTokenConverter());//jwt
//從數據庫查請求的路徑
// endpoints.tokenStore(jdbcTokenStore());
//從jwt來數據
endpoints.tokenStore(jwtStore());
//授權碼存儲
endpoints.authorizationCodeServices(redisAuthenticationCodeServices);
endpoints.userDetailsService(myUserDetailService);
// 配置TokenServices參數 注意這個是默認的uuid的存儲設置 與jwt無關 如果要用jwt請注釋掉
// DefaultTokenServices tokenServices = new DefaultTokenServices();
//獲取令牌的是否從jdbc查 顯然 這里是的
// tokenServices.setTokenStore(endpoints.getTokenStore());
//我們可以用jwt來存放token
// tokenServices.setTokenStore(jwtStore());
// tokenServices.setSupportRefreshToken(false);
// tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
// tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
// endpoints.tokenServices(tokenServices);
// @Bean
// RedisTokenStore redisTokenStore(){
// return new RedisTokenStore(redisConnectionFactory);
// }
// endpoints.tokenStore(redisTokenStore());
}
//定義jwttoken的某些屬性
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter() {
/**
* 重寫增強token的方法
* 自定義返回相應的信息
*
*/
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String userName = authentication.getUserAuthentication().getName();
// 與登錄時候放進去的UserDetail實現類一直查看link{SecurityConfiguration}
User user = (User) authentication.getUserAuthentication().getPrincipal();
/** 自定義一些token屬性 ***/
final Map<String, Object> additionalInformation = new HashMap<>();
additionalInformation.put("userName", userName);
additionalInformation.put("roles", user.getAuthorities());
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInformation);
OAuth2AccessToken enhancedToken = super.enhance(accessToken, authentication);
return enhancedToken;
}
};
// 測試用,資源服務使用相同的字符達到一個對稱加密的效果,生產時候使用RSA非對稱加密方式
accessTokenConverter.setSigningKey("123");
return accessTokenConverter;
}
@Bean
public TokenStore jwtStore() {
TokenStore tokenStore = new JwtTokenStore(accessTokenConverter());
return tokenStore;
}
/**
* 創建一個默認的資源服務token
*
* @return
*/
@Bean
public ResourceServerTokenServices defaultTokenServices() {
final DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setTokenEnhancer(accessTokenConverter());
defaultTokenServices.setTokenStore(jwtStore());
return defaultTokenServices;
}
}
請注意:DefaultTokenServices與jwttoken的配置不能都存在,否則系統只找DefaultTokenServices的配置, 也就是生成的token會一直是默認的UUID,這里我們只能兩者選其一配置在代碼中
/ 配置TokenServices參數 注意這個是默認的uuid的存儲設置 與jwt無關 如果要用jwt請注釋掉
// DefaultTokenServices tokenServices = new DefaultTokenServices();
//獲取令牌的是否從jdbc查 顯然 這里是的
// tokenServices.setTokenStore(endpoints.getTokenStore());
//我們可以用jwt來存放token
// tokenServices.setTokenStore(jwtStore());
// tokenServices.setSupportRefreshToken(false);
// tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
// tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
// tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(30)); // 30天
// endpoints.tokenServices(tokenServices);
4.小技巧:
我們從路徑授權后獲得了code,就可以用code請求相應對的路徑換取jwttoken,我們用postman來進行測試:
這里我們用basic Auth的方式 只需要填寫name:normal-app即可,密碼可以不填
http://localhost:8787/oauth/token?client_id=normal-app&grant_type=authorization_code&code=csTjhK&redirect_uri=http://localhost:8787/resources/user
我們可以看下獲得的json:
這里生成的jwttoken中攜帶了相應這個是jwt的信息,這個一段字符串實際上是Header和Payload加密后拼接而成的,相應的可以查看下一篇jwt的相關解析.
我們可以訪問:https://www.jsonwebtoken.io/ 來解析下這個token里面的信息:
這里可以看到Header和Payload的信息,Header主要存儲的是type和加密算法,這里是HS256,我們主要看Payload的信息:
{
"aud": [
"resourceId"
],
"user_name": "test",
"scope": [
"read"
],
"roles": [
{
"authority": "ROLE_USER"
},
{
"authority": "admin"
}
],
"exp": 1532662701,
"userName": "test",
"authorities": [
"admin",
"ROLE_USER"
],
"jti": "066cefa0-0a7a-40da-87a0-133c5a9c64d3",
"client_id": "normal-app",
"iat": 1532659101
}
這里可以看到登錄的用戶名,token的生命周期等.我們就可以更清晰了解生成的jwttoken攜帶的信息有哪些了.