配置
基礎包依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
================================== 在spring-boot中 ==================================
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.3.5.RELEASE</version>
</dependency>
================================== 或者在spring-cloud中 ==================================
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>
配置三大核心
認證服務器的配置需要繼承 AuthorizationServerConfigurerAdapter 類,然後重寫內部的方法來獲得自己邏輯的token,其源碼如下:
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {
// 配置安全約束,主要是對默認的6個端點的開啓與關閉配置
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
}
// 客戶端信息配置
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
}
// 認證服務器令牌訪問端點配置和令牌服務配置,可以替換默認的端點url,配置支持的授權模式
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
}
}
- 配置客戶端詳細信息
ClientDetailsServiceConfigurer能夠使用內存或者JDBC來實現客戶端詳情服務
(ClientDetailsService ) , ClientDetailsService負責查找ClientDetails ,而ClientDetails有幾個重要的屬性如下列表:
- clientld: (必須的)用來標識客戶的Id。
- secret : ( 需要值得信任的客戶端)客戶端安全碼,如果有的話。
- scope :用來限制客戶端的訪問範圍,如果爲空(默認)的話,那麼客戶端擁有全部的訪問範圍。
- authorizedGrantTypes :此客戶端可以使用的授權類型,默認爲空。
- authorities :此客戶端可以使用的權限(基於Spring Security authorities )。
客戶端詳情( ClientDetails )能夠在應用程序運行的時候進行更新,可以通過訪問底層的存儲服務(例如
將客戶端詳情存儲在一個關係數據庫的表中,就可以使用JdbcClientDetailsService或者通過自己實現
ClientRegistrationService接口(同時你也可以實現ClientDetailsService接口)來進行管理。
- 令牌訪問端點和令牌服務配置
AuthorizationServerEndpointsConfigurer這個對象的實例可以完成令牌服務以及令牌endpoint配置。
配置授權類型( Grant Types )
AuthorizationServerEndpointsConfigurer通過設定以下屬性決定支持的授權類型( Grant Types ) :
- authenticationManager:認證管理器,當你選擇了資源所有者密碼(password)授權類型的時候,請設置這個屬性注入一個AuthenticationManager對象。
- userDetailsService :如果你設置了這個屬性的話,那說明你有一個自己的UserDetailsService接口的實現,或者你可以把這個東西設置到全局域上面去(例如GlobalAuthenticationManagerConfigurer這個配置對象),當你設置了這個之後,那麼"refresh_token"即刷新令牌授權類型模式的流程中就會包含一個檢查,用來確保這個賬號是否仍然有效,假如說你禁用了這個賬戶的話。
- authorizationCodeServices:這個屬性是用來設置授權碼服務的(即AuthorizationCodeServices的實例對象) ,主要用於"authorization_ code" 授權碼類型模式。
- implicitGrantService :這個屬性用於設置隱式授權模式,用來管理隱式授權模式的狀態。
- tokenGranter :當你設置了這個東西(即TokenGranter接口實現),那麼授權將會交由你來完全掌控,並且會忽略掉上面的這幾個屬性,這個屬性一般是用作拓展用途的,即標準的四種授權模式已經滿足不了你的需求的時候,纔會考慮使用這個。
配置授權端點的URL
默認的端點URL有以下6個:
- /oauth/authorize : 授權端點。
- /oauth/token : 令牌端點。
- /oauth/confirm _access : 用戶確認授權提交端點。
- /oauth/error : 授權服務錯誤信息端點。
- /oauth/check_token : 用於資源服務訪問的令牌解析端點。
- /oauth/token_key : 提供公有密匙的端點,如果你使用JWT令牌的話。
這些端點都可以通過配置的方式更改其路徑,在AuthorizationServerEndpointsConfigurer中有一個叫pathMapping()的方法用來配置端點URL鏈接,他有兩個參數:
- 第一個參數:string類型,默認的URL
- 第二個參數:string類型,進行替代的URL
以上的參數都是以"/"開始的字符串
- 配置令牌端點的安全約束
AuthorizationServerSecurityConfigurer : 用來配置令牌端點(Token Endpoint)的安全約束,如配置資源服務器需要驗證token的/oauth/check_token
總結
授權服務配置分成三大塊,可以關聯記憶。
既然要完成認證,它首先得知道客戶端信息從哪兒讀取,因此要進行客戶端詳情配置。
既然要頒發token,那必須得定義token的相關endpoint,以及token如何存取,以及客戶端支持哪些類型的token。
既然暴露除了一些endpoint,那對這些endpoint可以定義一些安全上的約束等。
詳細代碼如下:
- 認證服務器配置類
/**
* @author zhongyj <[email protected]><br/>
* @date 2020/6/1
*/
@Configuration
@EnableAuthorizationServer
public class DimplesAuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
private PasswordEncoder passwordEncoder;
private TokenStore tokenStore;
private ClientDetailsService clientDetailsService;
private AuthenticationManager authenticationManager;
@Autowired
public DimplesAuthorizationServerConfiguration(PasswordEncoder passwordEncoder, TokenStore tokenStore, ClientDetailsService clientDetailsService, AuthenticationManager authenticationManager) {
this.passwordEncoder = passwordEncoder;
this.tokenStore = tokenStore;
this.clientDetailsService = clientDetailsService;
this.authenticationManager = authenticationManager;
}
/**
* 令牌端點的安全約束
*
* @param security AuthorizationServerSecurityConfigurer
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
.tokenKeyAccess("permitAll()")
.checkTokenAccess("permitAll()")
.allowFormAuthenticationForClients();
}
/**
* 客戶端詳情服務,暫時配置在內存中,後期改爲存在數據庫
*
* @param clients ClientDetailsServiceConfigurer
* @throws Exception Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
clients.inMemory()
.withClient("dimples")
.secret(this.passwordEncoder.encode("123456"))
// 爲了測試,所以開啓所有的方式,實際業務根據需要選擇
.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
.accessTokenValiditySeconds(3600)
.refreshTokenValiditySeconds(864000)
.scopes("select")
// false跳轉到授權頁面,在授權碼模式中會使用到
.autoApprove(false)
// 驗證回調地址
.redirectUris("http://www.baidu.com");
}
/**
* 令牌訪問端點和令牌訪問服務
*
* @param endpoints AuthorizationServerEndpointsConfigurer
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints
// /oauth/token_key公開
.authenticationManager(authenticationManager)
// /oauth/check_token公開
.authorizationCodeServices(authorizationCodeServices())
// 允許表單獲取token
.tokenServices(tokenServices());
}
/**
* 令牌管理服務
*
* @return TokenServices
*/
@Bean
public AuthorizationServerTokenServices tokenServices() {
DefaultTokenServices services = new DefaultTokenServices();
// 客戶端詳情服務
services.setClientDetailsService(clientDetailsService);
// 支持令牌刷新
services.setSupportRefreshToken(true);
// 令牌存儲策略
services.setTokenStore(tokenStore);
// 令牌默認有效期2小時
services.setAccessTokenValiditySeconds(7200);
// 刷新令牌默認有效期2天
services.setRefreshTokenValiditySeconds(259200);
return services;
}
/**
* 設置授權碼模式的授權碼如何存儲,暫時採用內存
*
* @return AuthorizationCodeServices
*/
@Bean
public AuthorizationCodeServices authorizationCodeServices() {
return new InMemoryAuthorizationCodeServices();
}
}
- Token存儲配置類
/**
* @author zhongyj <[email protected]><br/>
* @date 2020/6/1
*/
@Configuration
public class TokenConfigure {
@Bean
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}
}
- Security 安全配置
/**
* @author zhongyj <[email protected]><br/>
* @date 2020/6/1
*/
@Configuration
public class DimplesWebSecurityConfiguration extends WebSecurityConfigurerAdapter {
private DimplesUserDetailServiceImpl userDetailsService;
private PasswordEncoder passwordEncoder;
@Autowired
public DimplesWebSecurityConfiguration(DimplesUserDetailServiceImpl userDetailsService, PasswordEncoder passwordEncoder) {
this.userDetailsService = userDetailsService;
this.passwordEncoder = passwordEncoder;
}
/**
* 安全攔截機制(最重要)
*/
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest().authenticated()
.and().formLogin();
}
/**
* 非必須配置,可以不配
* 認證管理配置
* 連接數據查詢用戶信息,與數據庫中密碼比對
*
* @param auth AuthenticationManagerBuilder
* @throws Exception Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
}
/**
* 認證管理區配置,密碼模式需要用到
*
* @return AuthenticationManager
* @throws Exception Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}
- 配置密碼
@Configuration
public class DimplesConfigure {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
- 用戶信息獲取類
@Configuration
public class DimplesUserDetailServiceImpl implements UserDetailsService {
private PasswordEncoder passwordEncoder;
@Autowired
public DimplesUserDetailServiceImpl(PasswordEncoder passwordEncoder) {
this.passwordEncoder = passwordEncoder;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// 模擬一個用戶,替代數據庫獲取邏輯
DimplesUser user = new DimplesUser();
user.setUserName(username);
user.setPassword(this.passwordEncoder.encode("123456"));
// 輸出加密後的密碼
System.out.println(user.getPassword());
return new User(username, user.getPassword(), user.isEnabled(),
user.isAccountNonExpired(), user.isCredentialsNonExpired(),
user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("admin"));
}
}
@Data
public class DimplesUser implements Serializable {
private static final long serialVersionUID = 3497935890426858541L;
private String userName;
private String password;
private boolean accountNonExpired = true;
private boolean accountNonLocked= true;
private boolean credentialsNonExpired= true;
private boolean enabled= true;
}
可能出現的問題
- 最可能出現的問題,項目啓動時報循環依賴的錯誤,要注意代碼中在當前配置的類中寫的Bean不能再當前類中去使用@Autowired注入
- 在登錄時控制檯報錯堆棧溢出
錯誤追蹤:
是由於配置密碼模式下的AuthenticationManager時,方法名稱爲authenticationManager,更改爲authenticationManagerBean即可
測試
授權碼模式下獲取token
- 首先獲取授權碼
在瀏覽器輸入 http://127.0.0.1:8080/oauth/authorize?client_id=dimples&response_type=code&redirect_uri=http://www.baidu.com
跳轉到登錄界面,然後進行用戶登錄,登錄成功以後選擇用戶授權,獲取相應的授權碼,將會顯示在重定向URL的鏈接後面。如下圖:
- 獲取code
- 驗證用戶
- 授權
- 得到code
- 拿着這個獲取的code的值去/oauth/token端點獲取token,如下圖:
這個code有錯將不能獲取到token,因爲這個將會保存在程序內存中,同時這個code只能獲取一次token
密碼模式
/oauth/token?client_id=dimples&client_secret=123456&grant_type=password&username=dimples&password=123456
這種模式十分簡單,但是也意味着直接將用戶的敏感信息泄露給了client端,因此這種模式一般只用於client是我們自己開發的情況下。
客戶端模式
/oauth/token?client_id=dimples&client_secret=123456&grant_type=client_credentials
簡化模式使用較少,也比較簡單,此處不做測試。