什么是 oAuth
oAuth 協議為用戶資源的授權提供了一個安全的、開放而又簡易的標准。與以往的授權方式不同之處是 oAuth 的授權不會使第三方觸及到用戶的帳號信息(如用戶名與密碼),即第三方無需使用用戶的用戶名與密碼就可以申請獲得該用戶資源的授權,因此 oAuth 是安全的。
什么是 Spring Security
Spring Security 是一個安全框架,前身是 Acegi Security,能夠為 Spring 企業應用系統提供聲明式的安全訪問控制。Spring Security 基於 Servlet 過濾器、IOC 和 AOP,為 Web 請求和方法調用提供身份確認和授權處理,避免了代碼耦合,減少了大量重復代碼工作。
客戶端授權模式
客戶端必須得到用戶的授權(authorization grant),才能獲得令牌(access token)。oAuth 2.0 定義了四種授權方式。
- implicit:簡化模式,不推薦使用
- authorization code:授權碼模式(本文章采用這種模式)
- resource owner password credentials:密碼模式
- client credentials:客戶端模式
個人對oAuth2認證過程的理解

編碼部分
- 創建項目工程,本文章采用多module方式
主要是配置一個pom,文章結束后代碼會發到github,這里不復制了 - 創建統一依賴
統一依賴中配置org.springframework.cloud
springboot2.1.x需要使用Greenwich版本 - 創建認證服務器模塊(內存模擬版本)
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
// 注入 WebSecurityConfiguration 中配置的 BCryptPasswordEncoder
@Autowired
private BCryptPasswordEncoder passwordEncoder;
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 配置客戶端
clients
// 使用內存設置
.inMemory()
// client_id
.withClient("client")
// client_secret
.secret(passwordEncoder.encode("secret"))
// 授權類型
.authorizedGrantTypes("authorization_code")
// 授權范圍
.scopes("app")
// 注冊回調地址
.redirectUris("http://blog.yhhu.xyz");
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true, jsr250Enabled = true)
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder passwordEncoder() {
// 設置默認的加密方式
return new BCryptPasswordEncoder();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
// 在內存中創建用戶並為密碼加密
.withUser("user").password(passwordEncoder().encode("123456")).roles("USER")
.and()
.withUser("admin").password(passwordEncoder().encode("123456")).roles("ADMIN");
}
}
SpringSecurity提供默認的登錄界面,並且提供RestfulAPI
spring:
application:
name: oauth2-server
server:
port: 8080
必須遵循這樣的請求url
獲取code:在瀏覽器直接訪問http://localhost:8080/oauth/authorize?client_id=client&response_type=code
通過code獲取access_token:http://client:secret@localhost:8080/oauth/token
這里需要帶兩個參數,采用x-www-form-urlencoded方式,grant_type為authorization_code,code為上一步獲取到的內容
url說明:client:secret不是固定的,這取決於你在AuthorizationServerConfiguration中withClient和secret的設置

- 創建認證服務器模塊(JDBC版本)
- 創建一個oauth數據庫,官方給的表示H2的,mysql有點改動,百度找了一個
CREATE TABLE `clientdetails` ( `appId` varchar(128) NOT NULL, `resourceIds` varchar(256) DEFAULT NULL, `appSecret` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `grantTypes` varchar(256) DEFAULT NULL, `redirectUrl` varchar(256) DEFAULT NULL, `authorities` varchar(256) DEFAULT NULL, `access_token_validity` int(11) DEFAULT NULL, `refresh_token_validity` int(11) DEFAULT NULL, `additionalInformation` varchar(4096) DEFAULT NULL, `autoApproveScopes` varchar(256) DEFAULT NULL, PRIMARY KEY (`appId`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, `authentication` blob, `refresh_token` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_approvals` ( `userId` varchar(256) DEFAULT NULL, `clientId` varchar(256) DEFAULT NULL, `scope` varchar(256) DEFAULT NULL, `status` varchar(10) DEFAULT NULL, `expiresAt` timestamp NULL DEFAULT NULL, `lastModifiedAt` timestamp NULL DEFAULT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_client_details` ( `client_id` varchar(128) 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; CREATE TABLE `oauth_client_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication_id` varchar(128) NOT NULL, `user_name` varchar(256) DEFAULT NULL, `client_id` varchar(256) DEFAULT NULL, PRIMARY KEY (`authentication_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_code` ( `code` varchar(256) DEFAULT NULL, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) DEFAULT NULL, `token` blob, `authentication` blob ) ENGINE=InnoDB DEFAULT CHARSET=utf8;- 操作一下oauth_client_details表
需要設置的內容為client_id,client_secret,scope,authorized_grant_types,web_server_redirect_uri,注意client_secret是經過加密的

至此完成驗證功能,其他操作和內存版都是一樣的,完成后可以在數據庫中看到看到token相關信息
RBAC(Role-Based Access Control,基於角色的訪問控制)
增加了幾個表,模型如下

可以做的簡單一點,可是這樣做功能更加完備,單單在user表中插入role列的話不便於管理
下面開始具體實現
- 在認證服務器端排除token檢查的權限判斷
@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/oauth/check_token");
}
-
創建一個service,查詢用戶使用(代碼略)
-
將授權模式由內存模式改為userDetailService方式
@Autowired
private DataSource dataSource;
@Bean
public TokenStore tokenStore() {
// 基於 JDBC 實現,令牌保存到數據
return new JdbcTokenStore(dataSource);
}
@Bean
public ClientDetailsService jdbcClientDetails() {
// 基於 JDBC 實現,需要事先在數據庫配置客戶端信息
return new JdbcClientDetailsService(dataSource);
}
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception
{
// 設置令牌
endpoints.tokenStore(tokenStore());
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService());
}
- 認證中心UserDetail權限判斷邏輯
public class UserDetailServiceImpl implements UserDetailsService {
@Autowired
private TbUserService tbUserService;
@Autowired
private TbPermissionService tbPermissionService;
@Override
public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
TbUser tbUser = tbUserService.getByUsername(s);
List<GrantedAuthority> grantedAuthorities= Lists.newArrayList();
if (tbUser != null) {
List<TbPermission> tbPermissions = tbPermissionService.selectByUserId(tbUser.getId());
tbPermissions.forEach(item -> {
GrantedAuthority grantedAuthority = new SimpleGrantedAuthority(item.getEnname());
grantedAuthorities.add(grantedAuthority);
});
return new User(tbUser.getUsername(),tbUser.getPassword(),grantedAuthorities);
}
return null;
}
}
- 如何自定義一個權限控制方法?(通過permission中url對應的方式來判斷權限)
定義權限判斷類
@Component("permission")
public class PermissionCheck {
private AntPathMatcher antPathMatcher = new AntPathMatcher();
public boolean hasPermission(HttpServletRequest request, Authentication authentication) {
boolean hasPermission = false;
//取出當前token對應用戶所擁有的權限
Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
for (GrantedAuthority authority : authorities) {
if (antPathMatcher.match(authority.getAuthority(), request.getRequestURI())) {
hasPermission = true;
break;
}
}
return hasPermission;
}
}
在資源服務器中配置access("")方法來調用hasPermission方法
public class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.exceptionHandling()
.and()
//管理session的創建策略永遠不會創建HttpSession,它不會使用HttpSession來獲取SecurityContext
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
//ALWAYS總是創建HttpSession
//IF_REQUIREDSpring Security只會在需要時創建一個HttpSession
//NEVERSpring Security不會創建HttpSession,但如果它已經存在,將可以使用HttpSession
.and()
.authorizeRequests()
.anyRequest().access("@permission.hasPermission(request,authentication)");
// 以下為配置所需保護的資源路徑及權限,需要與認證服務器配置的授權部分對應
// .antMatchers("/**").hasAuthority("SystemContent");
}
}
補充一個錯誤:Spring security oauth2 throwing "no bean resolver registered" for custom bean
在ResourceServerConfiguration類下補充以下配置
/**
* 以下為采坑地方,如果不使用自己的動態權限控制,下面無需配置也能運行
* 原因暫時不明,解決方案為參考以下issues地址
* https://github.com/spring-projects/spring-security-oauth/issues/730#issuecomment-219480394
*/
@Autowired
private OAuth2WebSecurityExpressionHandler expressionHandler;
@Bean
public OAuth2WebSecurityExpressionHandler oAuth2WebSecurityExpressionHandler(ApplicationContext applicationContext) {
OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
expressionHandler.setApplicationContext(applicationContext);
return expressionHandler;
}
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
resources.expressionHandler(expressionHandler);
}
完整代碼地址
github:oauth2
