OAuth是一個關於受權(authorization)的開放網絡標準,在全世界獲得普遍應用,目前的版本是2.0版。本文重點講解Spring Boot項目對OAuth2進行的實現,若是你對OAuth2不是很瞭解,你能夠先理解 OAuth 2.0 - 阮一峯,這是一篇對於oauth2很好的科普文章。html
oauth2根據使用場景不一樣,分紅了4種模式java
在項目中咱們一般使用受權碼模式,也是四種模式中最複雜的,一般網站中常常出現的微博,qq第三方登陸,都會採用這個形式。git
Oauth2受權主要由兩部分組成:程序員
在實際項目中以上兩個服務能夠在一個服務器上,也能夠分開部署。下面結合spring boot來講明如何使用。github
以前的文章已經對 Spring Security 進行了講解,這一節對涉及到 Spring Security 的配置不詳細講解。若不瞭解 Spring Security 先移步到 Spring Boot Security 詳解。web
客戶端信息能夠存儲在內存、redis和數據庫。在實際項目中一般使用redis和數據庫存儲。本文采用數據庫。Spring 0Auth2 己經設計好了數據庫的表,且不可變。表及字段說明參照:Oauth2數據庫表說明 。redis
建立0Auth2數據庫的腳本以下:spring
DROP TABLE IF EXISTS `clientdetails`; DROP TABLE IF EXISTS `oauth_access_token`; DROP TABLE IF EXISTS `oauth_approvals`; DROP TABLE IF EXISTS `oauth_client_details`; DROP TABLE IF EXISTS `oauth_client_token`; DROP TABLE IF EXISTS `oauth_refresh_token`; 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` datetime DEFAULT NULL, `lastModifiedAt` datetime 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; DROP TABLE IF EXISTS `oauth_code`; 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;
爲了測試方便,咱們先插入一條客戶端信息。sql
INSERT INTO `oauth_client_details` VALUES ('dev', '', 'dev', 'app', 'password,client_credentials,authorization_code,refresh_token', 'http://www.baidu.com', '', 3600, 3600, '{\"country\":\"CN\",\"country_code\":\"086\"}', 'false');
用戶、權限、角色用到的表以下:shell
DROP TABLE IF EXISTS `user`; DROP TABLE IF EXISTS `role`; DROP TABLE IF EXISTS `user_role`; DROP TABLE IF EXISTS `role_permission`; DROP TABLE IF EXISTS `permission`; CREATE TABLE `user` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `username` varchar(255) NOT NULL, `password` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `role` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `name` varchar(255) NOT NULL, PRIMARY KEY (`id`) ); CREATE TABLE `user_role` ( `user_id` bigint(11) NOT NULL, `role_id` bigint(11) NOT NULL ); CREATE TABLE `role_permission` ( `role_id` bigint(11) NOT NULL, `permission_id` bigint(11) NOT NULL ); CREATE TABLE `permission` ( `id` bigint(11) NOT NULL AUTO_INCREMENT, `url` varchar(255) NOT NULL, `name` varchar(255) NOT NULL, `description` varchar(255) NULL, `pid` bigint(11) NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO user (id, username, password) VALUES (1,'user','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO user (id, username , password) VALUES (2,'admin','e10adc3949ba59abbe56e057f20f883e'); INSERT INTO role (id, name) VALUES (1,'USER'); INSERT INTO role (id, name) VALUES (2,'ADMIN'); INSERT INTO permission (id, url, name, pid) VALUES (1,'/**','',0); INSERT INTO permission (id, url, name, pid) VALUES (2,'/**','',0); INSERT INTO user_role (user_id, role_id) VALUES (1, 1); INSERT INTO user_role (user_id, role_id) VALUES (2, 2); INSERT INTO role_permission (role_id, permission_id) VALUES (1, 1); INSERT INTO role_permission (role_id, permission_id) VALUES (2, 2);
resources |____templates | |____login.html | |____application.yml java |____com | |____gf | | |____SpringbootSecurityApplication.java | | |____config | | | |____SecurityConfig.java | | | |____MyFilterSecurityInterceptor.java | | | |____MyInvocationSecurityMetadataSourceService.java | | | |____ResourceServerConfig.java | | | |____WebResponseExceptionTranslateConfig.java | | | |____AuthorizationServerConfiguration.java | | | |____MyAccessDecisionManager.java | | |____entity | | | |____User.java | | | |____RolePermisson.java | | | |____Role.java | | |____mapper | | | |____PermissionMapper.java | | | |____UserMapper.java | | | |____RoleMapper.java | | |____controller | | | |____HelloController.java | | | |____MainController.java | | |____service | | | |____MyUserDetailsService.java
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth.boot</groupId> <artifactId>spring-security-oauth2-autoconfigure</artifactId> <version>2.1.3.RELEASE</version> </dependency>
支持password模式要配置AuthenticationManager
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private MyUserDetailsService userService; @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { //校驗用戶 auth.userDetailsService( userService ).passwordEncoder( new PasswordEncoder() { //對密碼進行加密 @Override public String encode(CharSequence charSequence) { System.out.println(charSequence.toString()); return DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); } //對密碼進行判斷匹配 @Override public boolean matches(CharSequence charSequence, String s) { String encode = DigestUtils.md5DigestAsHex(charSequence.toString().getBytes()); boolean res = s.equals( encode ); return res; } } ); } @Override protected void configure(HttpSecurity http) throws Exception { http.csrf().disable(); http.requestMatchers() .antMatchers("/oauth/**","/login","/login-error") .and() .authorizeRequests() .antMatchers("/oauth/**").authenticated() .and() .formLogin().loginPage( "/login" ).failureUrl( "/login-error" ); } @Override @Bean public AuthenticationManager authenticationManagerBean() throws Exception{ return super.authenticationManager(); } @Bean public PasswordEncoder passwordEncoder() { return new PasswordEncoder() { @Override public String encode(CharSequence charSequence) { return charSequence.toString(); } @Override public boolean matches(CharSequence charSequence, String s) { return Objects.equals(charSequence.toString(),s); } }; } }
/** * 認證服務器配置 */ @Configuration @EnableAuthorizationServer public class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter { /** * 注入權限驗證控制器 來支持 password grant type */ @Autowired private AuthenticationManager authenticationManager; /** * 注入userDetailsService,開啓refresh_token須要用到 */ @Autowired private MyUserDetailsService userDetailsService; /** * 數據源 */ @Autowired private DataSource dataSource; /** * 設置保存token的方式,一共有五種,這裏採用數據庫的方式 */ @Autowired private TokenStore tokenStore; @Autowired private WebResponseExceptionTranslator webResponseExceptionTranslator; @Bean public TokenStore tokenStore() { return new JdbcTokenStore( dataSource ); } @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { /** * 配置oauth2服務跨域 */ CorsConfigurationSource source = new CorsConfigurationSource() { @Override public CorsConfiguration getCorsConfiguration(HttpServletRequest request) { CorsConfiguration corsConfiguration = new CorsConfiguration(); corsConfiguration.addAllowedHeader("*"); corsConfiguration.addAllowedOrigin(request.getHeader( HttpHeaders.ORIGIN)); corsConfiguration.addAllowedMethod("*"); corsConfiguration.setAllowCredentials(true); corsConfiguration.setMaxAge(3600L); return corsConfiguration; } }; security.tokenKeyAccess("permitAll()") .checkTokenAccess("permitAll()") .allowFormAuthenticationForClients() .addTokenEndpointAuthenticationFilter(new CorsFilter(source)); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.jdbc(dataSource); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { //開啓密碼受權類型 endpoints.authenticationManager(authenticationManager); //配置token存儲方式 endpoints.tokenStore(tokenStore); //自定義登陸或者鑑權失敗時的返回信息 endpoints.exceptionTranslator(webResponseExceptionTranslator); //要使用refresh_token的話,須要額外配置userDetailsService endpoints.userDetailsService( userDetailsService ); } }
/** * 資源提供端的配置 */ @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { /** * 這裏設置須要token驗證的url * 這些url能夠在WebSecurityConfigurerAdapter中排除掉, * 對於相同的url,若是兩者都配置了驗證 * 則優先進入ResourceServerConfigurerAdapter,進行token驗證。而不會進行 * WebSecurityConfigurerAdapter 的 basic auth或表單認證。 */ @Override public void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/hi") .and() .authorizeRequests() .antMatchers("/hi").authenticated(); } }
關鍵代碼就是這些,其餘類代碼參照後面提供的源碼地址。
[ 密碼模式須要參數:username , password , grant_type , client_id , client_secret ]
請求token
curl -X POST -d "username=admin&password=123456&grant_type=password&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
返回
{ "access_token": "d94ec0aa-47ee-4578-b4a0-8cf47f0e8639", "token_type": "bearer", "refresh_token": "23503bc7-4494-4795-a047-98db75053374", "expires_in": 3475, "scope": "app" }
不攜帶token訪問資源,
curl http://localhost:8080/hi\?name\=zhangsan
返回提示未受權
{ "error": "unauthorized", "error_description": "Full authentication is required to access this resource" }
攜帶token訪問資源
curl http://localhost:8080/hi\?name\=zhangsan\&access_token\=164471f7-6fc6-4890-b5d2-eb43bda3328a
返回正確
hi , zhangsan
刷新token
curl -X POST -d 'grant_type=refresh_token&refresh_token=23503bc7-4494-4795-a047-98db75053374&client_id=dev&client_secret=dev' http://localhost:8080/oauth/token
返回
{ "access_token": "ef53eb01-eb9b-46d8-bd58-7a0f9f44e30b", "token_type": "bearer", "refresh_token": "23503bc7-4494-4795-a047-98db75053374", "expires_in": 3599, "scope": "app" }
[ 客戶端模式須要參數:grant_type , client_id , client_secret ]
請求token
curl -X POST -d "grant_type=client_credentials&client_id=dev&client_secret=dev" http://localhost:8080/oauth/token
返回
{ "access_token": "a7be47b3-9dc8-473e-967a-c7267682dc66", "token_type": "bearer", "expires_in": 3564, "scope": "app" }
獲取code
瀏覽器中訪問以下地址:
http://localhost:8080/oauth/authorize?response_type=code&client_id=dev&redirect_uri=http://www.baidu.com
跳轉到登陸頁面,輸入帳號和密碼進行認證:
認證後會跳轉到受權確認頁面(oauth_client_details 表中 「autoapprove」 字段設置爲true 時,不會出受權確認頁面):
確認後,會跳轉到百度,而且地址欄中會帶上咱們想獲得的code參數:
經過code換token
curl -X POST -d "grant_type=authorization_code&code=qS03iu&client_id=dev&client_secret=dev&redirect_uri=http://www.baidu.com" http://localhost:8080/oauth/token
返回
{ "access_token": "90a246fa-a9ee-4117-8401-ca9c869c5be9", "token_type": "bearer", "refresh_token": "23503bc7-4494-4795-a047-98db75053374", "expires_in": 3319, "scope": "app" }
http://www.javashuo.com/article/p-vssqefqx-mn.html
https://stackoverflow.com/questions/28537181/spring-security-oauth2-which-decides-security
https://github.com/gf-huanchupk/SpringBootLearning/tree/master/springboot-security-oauth2
歡迎關注個人公衆號《程序員果果》,關注有驚喜~~