在上一篇《OAuth 2.0 受權碼請求》中咱們已經能夠獲取到access_token了,本節將使用客戶端來訪問遠程資源css
受權服務器負責生成併發放訪問令牌(access_token),客戶端在訪問受保護的資源時會帶上訪問令牌,資源服務器須要解析並驗證客戶端帶的這個訪問令牌。html
若是你的資源服務器同時也是一個受權服務器(資源服務器和受權服務器在一塊兒),那麼資源服務器就不須要考慮令牌解析的事情了,不然這一步是不可或缺的。java
To use the access token you need a Resource Server (which can be the same as the Authorization Server). Creating a Resource Server is easy, just add @EnableResourceServer and provide some configuration to allow the server to decode access tokens. If your application is also an Authorization Server it already knows how to decode tokens, so there is nothing else to do. If your app is a standalone service then you need to give it some more configuration.git
同時,把它們放在一塊兒的話還有一個問題須要注意,咱們知道過濾器是順序執行的,所以須要確保那些經過訪問令牌來訪問的資源路徑不能被主過濾攔下了,須要單獨摘出來。github
Note: if your Authorization Server is also a Resource Server then there is another security filter chain with lower priority controlling the API resources. Fo those requests to be protected by access tokens you need their paths not to be matched by the ones in the main user-facing filter chain, so be sure to include a request matcher that picks out only non-API resources in the WebSecurityConfigurer above.web
關於Spring Security中過濾器的順序能夠參見 redis
https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/reference/htmlsingle/#filter-orderingspring
這裏偷個懶將它們放在一塊兒:express
package com.cjs.example.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; import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; import org.springframework.security.oauth2.provider.expression.OAuth2WebSecurityExpressionHandler; @Configuration @EnableResourceServer public class ResourceServerConfig extends ResourceServerConfigurerAdapter { @Override public void configure(ResourceServerSecurityConfigurer resources) throws Exception { super.configure(resources); } /** * 用於配置對受保護的資源的訪問規則 * 默認狀況下全部不在/oauth/**下的資源都是受保護的資源 * {@link OAuth2WebSecurityExpressionHandler} */ @Override public void configure(HttpSecurity http) throws Exception { http.requestMatchers().antMatchers("/haha/**") .and() .authorizeRequests() .anyRequest().authenticated(); } }
這裏配置很簡潔,不少都用了默認的設置(好比:resourceId,accessDeniedHandler,sessionManagement等等,具體可參見源碼)apache
接下來,看看本例中咱們被保護的資源,簡單的幾個資源(都以/haha開頭),只爲測試:
package com.cjs.example.controller; import com.cjs.example.domain.UserInfo; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; import java.security.Principal; @RestController @RequestMapping("/haha") public class MainController { @GetMapping("/sayHello") public String sayHello(String name) { return "Hello, " + name; } @PreAuthorize("hasAnyRole('ADMIN')") @RequestMapping("/sayHi") public String sayHi() { return "hahaha"; } @RequestMapping("/userInfo") public UserInfo userInfo(Principal principal) { UserInfo userInfo = new UserInfo(); userInfo.setName(principal.getName()); return userInfo; } }
package com.cjs.example.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.connection.RedisConnectionFactory; 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.token.TokenStore; import org.springframework.security.oauth2.provider.token.store.redis.RedisTokenStore; @Configuration @EnableAuthorizationServer public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter { @Autowired private RedisConnectionFactory connectionFactory; @Override public void configure(AuthorizationServerSecurityConfigurer security) throws Exception { super.configure(security); } @Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient("my-client-1") .secret("$2a$10$0jyHr4rGRdQw.X9mrLkVROdQI8.qnWJ1Sl8ly.yzK0bp06aaAkL9W") .authorizedGrantTypes("authorization_code", "refresh_token") .scopes("read", "write", "execute") .redirectUris("http://localhost:8081/login/oauth2/code/callback"); // .redirectUris("http://www.baidu.com"); } @Override public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception { endpoints.tokenStore(tokenStore()); } @Bean public TokenStore tokenStore() { return new RedisTokenStore(connectionFactory); } public static void main(String[] args) { System.out.println(new org.apache.tomcat.util.codec.binary.Base64().encodeAsString("my-client-1:12345678".getBytes())); System.out.println(java.util.Base64.getEncoder().encodeToString("my-client-1:12345678".getBytes())); } }
和以前相比,咱們增長了TokenStore,將Token存儲到Redis中。不然默認放在內存中的話每次重啓的話token都丟了。下面是一個例子:
server: port: 8080 spring: redis: host: 127.0.0.1 port: 6379 logging: level: root: debug org.springframework.web: debug org.springframework.security: debug
咱們有了資源,有了受權,咱們還缺乏用戶。WebSecurity主要是配置我們這個項目的一些安全配置,好比用戶、認證、受權等等。
package com.cjs.example.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.builders.WebSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; @Configuration @EnableWebSecurity @EnableGlobalMethodSecurity(prePostEnabled = true, proxyTargetClass = true) public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser("zhangsan") .password("$2a$10$qsJ/Oy1RmUxFA.YtDT8RJ.Y2kU3U4z0jvd35YmiMOAPpD.nZUIRMC") .roles("USER") .and() .withUser("lisi") .password("$2a$10$qsJ/Oy1RmUxFA.YtDT8RJ.Y2kU3U4z0jvd35YmiMOAPpD.nZUIRMC") .roles("USER", "ADMIN"); } @Override public void configure(WebSecurity web) throws Exception { web.ignoring().antMatchers("/css/**", "/js/**", "/plugins/**", "/favicon.ico"); } @Override protected void configure(HttpSecurity http) throws Exception { super.configure(http); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } public static void main(String[] args) { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); System.out.println(bCryptPasswordEncoder.encode("123456")); System.out.println(bCryptPasswordEncoder.encode("12345678")); } }
這裏多說兩句,關於Endpoint和HttpSecurity
有不少端點咱們是能夠重寫的,好比:/login,/oauth/token等等
不少初學者可能會不知道怎麼配置HttpSecurity,這個時候其實最好的方法就是看代碼或者API文檔
下面一塊兒看一下常見的幾個配置
咱們先來看一下,當咱們繼承WebSecurityConfigurerAdapter以後它的默認的HttpSecurity是怎麼配置的:
// @formatter:off protected void configure(HttpSecurity http) throws Exception { logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().and() .httpBasic(); } // @formatter:on
能夠看到,全部的請求都須要受權,而且指定登陸的uri是/login,同時支持Basic認證。
這個方法是用於限定只有特定的HttpServletRequest實例纔會致使該HttpSecurity被調用,固然是經過請求uri進行限定的了。它後面能夠接多個匹配規則。例如:
@Configuration @EnableWebSecurity public class RequestMatchersSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .httpBasic(); } /* 與上面那段等價 @Override protected void configure(HttpSecurity http) throws Exception { http .requestMatchers() .antMatchers("/api/**") .and() .requestMatchers() .antMatchers("/oauth/**") .and() .authorizeRequests() .antMatchers("/**").hasRole("USER") .and() .httpBasic(); } */ @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth .inMemoryAuthentication() .withUser("user").password("password").roles("USER"); } }
該方法是用於配置登陸相關的設置的。例如:
@Configuration @EnableWebSecurity public class FormLoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/**").hasRole("USER").and().formLogin() .usernameParameter("username") // default is username .passwordParameter("password") // default is password .loginPage("/authentication/login") // default is /login with an HTTP get .failureUrl("/authentication/login?failed") // default is /login?error .loginProcessingUrl("/authentication/login/process"); // default is /login } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("password").roles("USER"); } }
當咱們沒有配置登陸的時候,會用默認的登陸,有默認的登陸頁面,還有好多默認的登陸配置。具體可參見 FormLoginConfigurer.loginPage(String)方法
該方法容許基於HttpServletRequest進行訪問限制,好比角色、權限。例如:
@Configuration @EnableWebSecurity public class AuthorizeUrlsSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http.authorizeRequests().antMatchers("/admin/**").hasRole("ADMIN") .antMatchers("/**").hasRole("USER").and().formLogin(); } @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().withUser("user").password("password").roles("USER") .and().withUser("admin").password("password").roles("ADMIN", "USER"); } }
anyRequest()表示匹配任意請求
authenticated()表示只有認證經過的用戶才能夠訪問
更多能夠參見API文檔:https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/api/
在瀏覽器中輸入http://localhost:8080/oauth/authorize?response_type=code&client_id=my-client-1&redirect_uri=http://www.baidu.com&scope=read
而後跳到登陸頁面,輸入用戶名和密碼登陸,而後從重定向url中拿到code
http://localhost:8080/haha/sayHi?access_token=9f908b8f-06d6-4987-b105-665ca5a4522a { "error": "access_denied", "error_description": "不容許訪問" } 這裏不容許訪問是由於我用zhangsan登陸的,他不在ADMIN角色中 http://localhost:8080/haha/userInfo?access_token=9f908b8f-06d6-4987-b105-665ca5a4522a { "name": "zhangsan" } http://localhost:8080/haha/sayHello?name=jack&access_token=9f908b8f-06d6-4987-b105-665ca5a4522a Hello, jack
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples/spring-boot-sample-oauth2-client
https://github.com/spring-projects
https://github.com/spring-projects/spring-boot
能夠將代碼拷下來 https://github.com/spring-projects/spring-boot.git
GitHub的OAuth文檔在這裏 https://developer.github.com/apps/building-oauth-apps/authorizing-oauth-apps/
受權之後就能夠重定向到咱們的localhost:8080/了
APP-CLIENT-ID: 7e304109d91ed8e9bf72
APP-CLIENT-SECRET: 003daa47fa0f350d181c8741d8bac6833aef568a
spring:
security:
oauth2:
client:
registration:
github-client-1:
client-id: ${APP-CLIENT-ID}
client-secret: ${APP-CLIENT-SECRET}
client-name: Github user
provider: github
scope: user
redirect-uri-template: http://cjshuashengke.6655.la:11664/login/oauth2/code/github
github-client-2:
client-id: ${APP-CLIENT-ID}
client-secret: ${APP-CLIENT-SECRET}
client-name: Github email
provider: github
scope: user:email
redirect-uri-template: http://cjshuashengke.6655.la:11664/login/oauth2/code/github
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>sample.oauth2.client</groupId> <artifactId>spring-boot-sample-oauth2-client</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.3.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <!-- Compile --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <!-- Test --> <dependency> <groupId>org.apache.httpcomponents</groupId> <artifactId>httpclient</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
package sample.oauth2.client; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @SpringBootApplication public class SampleOAuth2ClientApplication { public static void main(String[] args) { SpringApplication.run(SampleOAuth2ClientApplication.class); } }
package sample.oauth2.client; import java.security.Principal; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class ExampleController { @RequestMapping("/") public String email(Principal principal) { return "Hello " + principal.getName(); } }
在這裏我遇到一些問題,將provider換成本身的localhost:8080就老是報錯,不知道什麼緣由。
想一想仍是寫出來吧,但願有大神看到幫我指點迷津。。。
首先須要明確一點:Spring Security OAuth2提供了一套客戶端實現,Spring Boot也有它本身的方式。
這裏我是按照Spring Boot的那一套來的
Spring Boot最大的優勢莫過於自動配置了
只要在你的classpath下有spring-security-oauth2-client,那麼將會自動配置OAuth2 Client。
配置的屬性用的是OAuth2ClientProperties
例如:
默認狀況下,Spring Security的OAuth2LoginAuthenticationFilter只處理URL匹配/login/oauth2/code/*的請求。
若是你想自定義redirect-uri-template能夠在WebSecurityConfigurerAdapter中配置。例如:
public class OAuth2LoginSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.oauth2Login()
.redirectionEndpoint()
.baseUri("/custom-callback");
}
}
其實,不只僅能夠自定義重定向端點,其它的好比受權端點也是能夠自定義的,可參見源碼
下面重點說下個人配置
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.cjs.example</groupId> <artifactId>cjs-oauth2-code-client</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>cjs-oauth2-code-client</name> <description></description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.0.2.RELEASE</version> <relativePath/> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-config</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.security</groupId> <artifactId>spring-security-oauth2-jose</artifactId> </dependency> <dependency> <groupId>org.springframework.security.oauth</groupId> <artifactId>spring-security-oauth2</artifactId> <version>2.3.3.RELEASE</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project>
server: port: 8081 spring: security: oauth2: client: registration: my-client-1: client-id: my-client-1 client-secret: 12345678 client-name: ABC provider: my-oauth-provider scope: read redirect-uri-template: http://localhost:8081/login/oauth2/code/callback client-authentication-method: basic authorization-grant-type: authorization_code provider: my-oauth-provider: authorization-uri: http://localhost:8080/oauth/authorize token-uri: http://localhost:8080/oauth/token logging: level: root: debug
分別啓動兩個項目
查看控制檯錯誤信息
org.springframework.security.oauth2.core.OAuth2AuthenticationException: [authorization_request_not_found] at org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter.attemptAuthentication(OAuth2LoginAuthenticationFilter.java:146) at org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter.doFilter(AbstractAuthenticationProcessingFilter.java:212) at org.springframework.security.web.FilterChainProxy$VirtualFilterChain.doFilter(FilterChainProxy.java:334) [spring-security-web-5.0.5.RELEASE.jar:5.0.5.RELEASE] at org.springframework.security.oauth2.client.web.OAuth2AuthorizationRequestRedirectFilter.doFilterInternal(OAuth2AuthorizationRequestRedirectFilter.java:128) at org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:107) [spring-web-5.0.6.RELEASE.jar:5.0.6.RELEASE]
打端點,發現重定向過來的HttpServletRequest中session=null
正常狀況下session不該該是null,並且屬性中還應該有一個key爲org.springframework.security.oauth2.client.web.HttpSessionOAuth2AuthorizationRequestRepository.AUTHORIZATION_REQUEST的屬性的
後來看根據Spring Security那一套說用@EnableOAuth2Client
利用OAuth2RestTemplate來訪問資源,具體參見《Spring Security OAuth 2.0》
可是我發現加了這些問題依舊,根本問題是服務端帶着code重定向到客戶端的時候就失敗了
在客戶端收到的重定向請求中session爲null
不知道你們有沒有遇到這種問題,求路過的大神們支招。。。
下面貼出客戶端完整代碼
package com.cjs.example.controller; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import java.security.Principal; @Controller public class MainController { @Autowired private OAuth2RestTemplate oAuth2RestTemplate; @GetMapping("/test") public String test(Principal principal) { return "Hello, " + principal.getName(); } @GetMapping("/hello") public String hello() { return oAuth2RestTemplate.getForObject("http://localhost:8080/sayHi", String.class); } }
package com.cjs.example; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.security.oauth2.client.OAuth2ClientContext; import org.springframework.security.oauth2.client.OAuth2RestTemplate; import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails; import org.springframework.context.annotation.Bean; import org.springframework.security.oauth2.config.annotation.web.configuration.EnableOAuth2Client; @EnableOAuth2Client @SpringBootApplication public class CjsOauth2CodeClientApplication { public static void main(String[] args) { SpringApplication.run(CjsOauth2CodeClientApplication.class, args); } @Autowired private OAuth2ClientContext oauth2ClientContext; @Bean public OAuth2RestTemplate restTemplate() { return new OAuth2RestTemplate(new AuthorizationCodeResourceDetails(), oauth2ClientContext); } }
代碼上傳至 https://github.com/chengjiansheng/cjs-oauth2-example.git
https://docs.spring.io/spring-security/site/docs/5.0.6.RELEASE/api/
https://github.com/spring-projects/spring-boot/tree/master/spring-boot-samples
http://www.javashuo.com/article/p-myfdxwel-mh.html
https://www.jianshu.com/p/bf5dd33aea6d?utm_source=oschina-app
https://www.aliyun.com/jiaocheng/800606.html
https://blog.csdn.net/peter1220/article/details/52413250
https://blog.csdn.net/weixin_42033269/article/details/80086422