概述:Spring OAuth2.0 是Spring Security中的模塊,提供項目安全認證(包括身份,權限,角色認證等)。其作用和 shiro 差不多,同屬於安全框架。但在使用角度個人覺得 shiro更易理解更易上手,更多差別還需了解。
其中OAuth2為我們提供了四種授權方式:
1、授權碼模式(authorization code)
2、簡化模式(implicit)
3、密碼模式(resource owner password credentials)
4、客戶端模式(client credentials)
而較常用的則為密碼模式和授權碼模式,而授權碼模式又是最為安全的模式,復雜程度成正比。完整的項目結構分為:客戶端服務,認證服務,資源服務。客戶端需要訪問資源服務的資源時,則需要得到認證服務的認證。
一、密碼模式
(1)單項目集成(即認證服務即為資源服務)其項目結構如下:
此項目屬於Maven項目。
• 配置認證服務器
首先需要配置認證服務所必須的配置,為 AuthorizationServerConfig 和 WebSecurityConfig 兩個配置文件,其詳情如下:
package com.liuzj.oauth2server.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; 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.client.JdbcClientDetailsService; import org.springframework.security.oauth2.provider.token.DefaultTokenServices; import org.springframework.security.oauth2.provider.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore; import javax.sql.DataSource; import java.util.concurrent.TimeUnit; /** * 這個注解告訴 Spring 這個應用是 OAuth2 的授權服務器, * 提供/oauth/authorize,/oauth/token,/oauth/check_token,/oauth/confirm_access,/oauth/error * * @author liuzj * @date 2019-01-15 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { /** * webSecurityConfig 中配置的AuthenticationManager */ @Autowired @Qualifier("authenticationManagerBean") private AuthenticationManager authenticationManager; /** * 此項目使用數據庫保存 token 等信息所以要配置數據源 */ @Autowired private DataSource dataSource; /** * webSecurityConfig 中配置的 userDetailsService */ @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; /** * webSecurityConfig 中配置的 passwordEncoder(使用MD5加密) */ @Autowired PasswordEncoder passwordEncoder; @Bean public TokenStore tokenStore() { //使用內存中的 token store // return new InMemoryTokenStore(); //使用Jdbctoken store return new JdbcTokenStore(dataSource); } /** * 對 oauth_client_details 表的一些操作 * * @return ClientDetailsService */ @Bean public ClientDetailsService clientDetails() { return new JdbcClientDetailsService(dataSource); } @Autowired TokenStore tokenStore; @Autowired ClientDetailsService clientDetailsService; @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource);
// 請求token的時候會將client_id,client_secret等信息保存到 oauth_client_details 表中,所以需要手動創建該表
// 注意:以下注釋的代碼在請求了一次 token 之后則可以注釋掉,否則如果不換 client 名字的話會因為主鍵沖突無法插入 client 信息。也可以一開始就注釋,手動添加記錄到數據庫 // .withClient("client") // .secret(passwordEncoder.encode("123456")) // .authorizedGrantTypes("authorization_code", "refresh_token", // "password", "implicit") // 四種認證模式 // .scopes("all") // .authorities("ROLE_admin","ROLE_user") // .redirectUris("http://www.baidu.com") // .accessTokenValiditySeconds(120000) // .refreshTokenValiditySeconds(50000); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { security.tokenKeyAccess("permitAll()") //允許check_token訪問 .checkTokenAccess("permitAll()") //允許表單登錄 .allowFormAuthenticationForClients(); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.authenticationManager(authenticationManager); endpoints.tokenStore(tokenStore()); endpoints.userDetailsService(userDetailsService); endpoints.setClientDetailsService(clientDetailsService); //配置TokenServices參數 DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setTokenStore(endpoints.getTokenStore()); tokenServices.setSupportRefreshToken(true); tokenServices.setClientDetailsService(endpoints.getClientDetailsService()); tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer()); // access_token 過期時間:5s tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.DAYS.toSeconds(1)); // refresh_token 過期時間,默認不過期 // tokenServices.setReuseRefreshToken(true); // tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.SECONDS.toSeconds(20)); endpoints.tokenServices(tokenServices); } @Bean @Primary public DefaultTokenServices tokenServices() { DefaultTokenServices tokenServices = new DefaultTokenServices(); tokenServices.setSupportRefreshToken(true); tokenServices.setTokenStore(tokenStore); return tokenServices; } }
package com.liuzj.oauth2server.config; import com.liuzj.oauth2server.utils.MD5Util; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; /** * 安全配置 * * @author liuzj * @date 2019-01-15 */ @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { /** * userDetailsService 獲取token的時候對用戶進行一些自定義過濾,並將保存用戶信息(用戶名,密碼,角色等) */ @Autowired @Qualifier("userDetailsServiceImpl") private UserDetailsService userDetailsService; /** * 使用MD5對client_secreat進行加密,可以使用默認的加密方式也可以自定義,這里使用MD5加密方式 * * @return PasswordEncoder */ @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return MD5Util.encodeMD5(String.valueOf(charSequence)); } @Override public boolean matches(CharSequence charSequence, String s) { return s.equals(MD5Util.encodeMD5(String.valueOf(charSequence))); } }; } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception { return super.authenticationManagerBean(); } /** * 配置用戶簽名服務 主要是user-details 機制, * * @param auth 簽名管理器構造器,用於構建用戶具體權限控制 * @throws Exception */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.userDetailsService(userDetailsService) .passwordEncoder(passwordEncoder()); } @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http .requestMatchers().antMatchers("/oauth/**","/login/**","/logout/**") .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .formLogin().permitAll(); //新增login form支持用戶登錄及授權 } }
以上有一個 UserDetailsService 這個接口是提供出來我們自己實現的,在實現代碼中可以自定義一下過濾規則,比如判斷用戶的合法性,具體如下:
package com.liuzj.oauth2server.config.selfauthor; import com.liuzj.oauth2server.domain.User; import com.liuzj.oauth2server.repositories.UserRepository; import com.liuzj.oauth2server.utils.MD5Util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import org.springframework.stereotype.Service; /** * 進行登錄用戶自定義過濾 * * @author liuzj * @date 2019-01-15 */ @Service public class UserDetailsServiceImpl implements UserDetailsService { protected final Log logger = LogFactory.getLog(getClass()); @Autowired private UserRepository userRepository; @Override public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException { User user = userRepository.findByUserName(username); logger.info("loadUserByUsername username=" + username); // 如果用戶不存在則認證失敗 if(user == null){ throw new UsernameNotFoundException(username + " not found"); } // 注意:此處的密碼記得要進行加密,因為在前面配置的時候是使用了 MD5 加密,所以這里也要進行加密 return new UserInfo(username, MD5Util.encodeMD5(user.getPassword()),user.getRole()); } }
以上只是將主要的配置附上,還有 user 表以及 集成 mybatis 等步驟沒有寫出,請自行倒騰。。。注意:user 對象需要實現 Serializable 接口(可序列化),因為在認證的時候該對象是需要進行IO操作的。
配置好認證服務之后跑如下 SQL 創建幾張必須的表(存儲 token 以及 client 等信息),因為本案例是使用數據庫存儲 token 的,當然也可以存在內存以及通過 jwt 方式:
-- ---------------------------- -- Table structure for oauth_access_token -- ---------------------------- DROP TABLE IF EXISTS `oauth_access_token`; CREATE TABLE `oauth_access_token` ( `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `token` blob NULL, `authentication_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `user_name` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authentication` blob NULL, `refresh_token` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`authentication_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for oauth_client_details -- ---------------------------- DROP TABLE IF EXISTS `oauth_client_details`; CREATE TABLE `oauth_client_details` ( `client_id` varchar(250) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL, `resource_ids` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `client_secret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorized_grant_types` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `web_server_redirect_uri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `access_token_validity` int(11) NULL DEFAULT NULL, `refresh_token_validity` int(11) NULL DEFAULT NULL, `additional_information` varchar(4096) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `autoapprove` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, PRIMARY KEY (`client_id`) USING BTREE ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic; -- ---------------------------- -- Table structure for oauth_refresh_token -- ---------------------------- DROP TABLE IF EXISTS `oauth_refresh_token`; CREATE TABLE `oauth_refresh_token` ( `token_id` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL, `token` blob NULL, `authentication` blob NULL ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;
-- ---------------------------- -- Table structure for user -- ----------------------------
CREATE TABLE `user` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '用戶ID',
`name` varchar(255) NOT NULL DEFAULT '' COMMENT '名稱',
`password` varchar(255) NOT NULL DEFAULT '0' COMMENT '密碼',
`role` varchar(255) NOT NULL COMMENT '角色',
PRIMARY KEY (`id`)
) ENGINE=MyISAM AUTO_INCREMENT=5 DEFAULT CHARSET=utf8;
到此,啟動項目,訪問連接:http://localhost:8001/oauth/token?username=liuzj&password=123&grant_type=password&client_id=client&client_secret=123456 (POST)
正常情況下如下返回:
這樣就成功通過 賬號和密碼獲取了 token。還可以嘗試如下操作:
檢查 token :http://localhost:8001/oauth/check_token?token=f0cb83c3-6dd7-4c63-ab9f-2bdf3d492b46 (get)
刷新 token: http://localhost:8001/oauth/token?grant_type=refresh_token&refresh_token=cd08de58-b91f-45c6-b165-6b8bd4b7bfdc&client_id=client&client_secret=123456 (post)
• 配置資源服務器
首先配置資源服務器專有配置,如下:
package com.liuzj.oauth2server.config; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; /** * 這個類表明了此應用是OAuth2 的資源服務器,此處主要指定受資源服務器保護的資源鏈接 * 默認情況下spring security oauth2的http配置會被WebSecurityConfigurerAdapter的配置覆蓋 * * @author liuzj * @date 2019-01-15 */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(HttpSecurity http) throws Exception { http.csrf().disable()//禁用了csrf(跨站請求偽造)功能 .authorizeRequests()//限定簽名成功的請求 //必須認證過后才可以訪問;注意:hasAnyRole 會默認加上ROLE_前綴,而hasAuthority不會加前綴 .antMatchers("/decision/**","/govern/**").hasAnyRole("user") // 在角色過濾的時候需要注意user角色需要加角色前綴 .antMatchers("/admin/**").hasRole("admin") .antMatchers("/test/**").authenticated() // 免驗證請求 .antMatchers("/oauth/**").permitAll(); } }
配置好如上配置就搞定了。重啟項目測試訪問項目接口(接口自行去碼):
如果直接訪問它便會提示:此資源訪問需要認證。所以得先獲取 token(上面有講)帶上 token 即可訪問接口,如下:
到此密碼認證告一段落。