diff --git a/spring-boot-demo-oauth/pom.xml b/spring-boot-demo-oauth/pom.xml
index 724e86a..dd76db3 100644
--- a/spring-boot-demo-oauth/pom.xml
+++ b/spring-boot-demo-oauth/pom.xml
@@ -7,6 +7,7 @@
1.0.0-SNAPSHOT
spring-boot-demo-oauth-authorization-server
+ spring-boot-demo-oauth-resource-server
pom
@@ -26,32 +27,6 @@
-
- org.springframework.boot
- spring-boot-starter-web
-
-
-
- org.springframework.boot
- spring-boot-starter-security
-
-
-
- org.springframework.boot
- spring-boot-starter-thymeleaf
-
-
-
- org.springframework.boot
- spring-boot-starter-data-jpa
-
-
-
- org.springframework.security.oauth.boot
- spring-security-oauth2-autoconfigure
- ${spring.boot.version}
-
-
mysql
mysql-connector-java
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
index cfc942f..d4fff86 100644
--- a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
@@ -11,5 +11,34 @@
spring-boot-demo-oauth-authorization-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.security.oauth.boot
+ spring-security-oauth2-autoconfigure
+ ${spring.boot.version}
+
+
+
+ org.springframework.boot
+ spring-boot-starter-thymeleaf
+
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
index d68c1b2..edbe405 100644
--- a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
@@ -3,7 +3,7 @@ server:
spring:
datasource:
- url: jdbc:mysql://localhost:3306/oauth
+ url: jdbc:mysql://localhost:3306/oauth?allowPublicKeyRetrieval=true
username: root
password: 123456
hikari:
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
index 5355e7e..896327e 100644
--- a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
@@ -43,7 +43,7 @@
{{previousText}}
下一步
- 登录
+ 登录
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc
new file mode 100644
index 0000000..4083136
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc
@@ -0,0 +1,59 @@
+= spring-boot-demo-oauth-resource-server
+Doc Writer
+v1.0, 2019-01-09
+:toc:
+
+spring boot oauth2 资源服务器,同 授权服务器 一起使用。
+
+> 使用 `spring security oauth`
+
+- JWT 解密,远程公钥获取
+- 基于角色访问控制
+- 基于应用授权域访问控制
+
+== jwt 解密
+
+要先获取 jwt 公钥
+
+[source,java]
+.OauthResourceTokenConfig
+----
+public class OauthResourceTokenConfig {
+ // ......
+ private String getPubKey() {
+ // 如果本地没有密钥,就从授权服务器中获取
+ return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue())
+ ? getKeyFromAuthorizationServer()
+ : resourceServerProperties.getJwt().getKeyValue();
+ }
+ // ......
+}
+----
+
+然后配置进去
+
+[source, java]
+.OauthResourceServerConfig
+----
+public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter {
+ @Override
+ public void configure(ResourceServerSecurityConfigurer resources) {
+ resources
+ .tokenStore(tokenStore)
+ .resourceId(resourceServerProperties.getResourceId());
+ }
+}
+----
+
+== 访问控制
+
+通过 `@EnableGlobalMethodSecurity(prePostEnabled = true)` 注解开启 `spring security` 的全局方法安全控制
+
+- `@PreAuthorize("hasRole('ADMIN')")` 校验角色
+- `@PreAuthorize("#oauth2.hasScope('READ')")` 校验令牌授权域
+
+== 测试
+
+测试用例: `com.xkcoding.oauth.controller.TestControllerTest`
+
+先获取 `token`,携带 `token` 去访问资源即可。
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml
new file mode 100644
index 0000000..b19d74c
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml
@@ -0,0 +1,31 @@
+
+
+
+ spring-boot-demo-oauth
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-boot-demo-oauth-resource-server
+
+
+
+ org.springframework.boot
+ spring-boot-starter-web
+
+
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+
+ org.springframework.security.oauth.boot
+ spring-security-oauth2-autoconfigure
+ ${spring.boot.version}
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java
new file mode 100644
index 0000000..33b7bd9
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java
@@ -0,0 +1,21 @@
+package com.xkcoding.oauth;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
+
+/**
+ * 启动器.
+ *
+ * @author EchoCow
+ * @date 2020/1/9 上午11:38
+ * @version V1.0
+ */
+@EnableResourceServer
+@SpringBootApplication
+public class SpringBootDemoResourceApplication {
+ public static void main(String[] args) {
+ SpringApplication.run(SpringBootDemoResourceApplication.class, args);
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java
new file mode 100644
index 0000000..2d3243e
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java
@@ -0,0 +1,43 @@
+package com.xkcoding.oauth.config;
+
+import lombok.AllArgsConstructor;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+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.token.TokenStore;
+
+/**
+ * 资源服务器配置.
+ * 我们自己实现了它的配置,所以它的自动装配不会生效
+ *
+ * @author EchoCow
+ * @date 2020/1/9 下午2:20
+ */
+@Configuration
+@AllArgsConstructor
+@EnableResourceServer
+@EnableGlobalMethodSecurity(prePostEnabled = true)
+public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter {
+
+ private final ResourceServerProperties resourceServerProperties;
+ private final TokenStore tokenStore;
+
+ @Override
+ public void configure(ResourceServerSecurityConfigurer resources) {
+ resources
+ .tokenStore(tokenStore)
+ .resourceId(resourceServerProperties.getResourceId());
+ }
+
+ @Override
+ public void configure(HttpSecurity http) throws Exception {
+ super.configure(http);
+ // 前后端分离下,可以关闭 csrf
+ http.csrf().disable();
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java
new file mode 100644
index 0000000..c28c72c
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java
@@ -0,0 +1,102 @@
+package com.xkcoding.oauth.config;
+
+import cn.hutool.json.JSONObject;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.security.oauth2.provider.token.TokenStore;
+import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
+import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.util.Base64;
+
+/**
+ * token 相关配置,jwt 相关.
+ *
+ * @author EchoCow
+ * @date 2020/1/9 下午2:39
+ */
+@Slf4j
+@Configuration
+@AllArgsConstructor
+public class OauthResourceTokenConfig {
+
+ private final ResourceServerProperties resourceServerProperties;
+
+ /**
+ * 这里并不是对令牌的存储,他将访问令牌与身份验证进行转换
+ * 在需要 {@link TokenStore} 的任何地方可以使用此方法
+ *
+ * @return TokenStore
+ */
+ @Bean
+ public TokenStore tokenStore() {
+ return new JwtTokenStore(jwtAccessTokenConverter());
+ }
+
+ /**
+ * jwt 令牌转换
+ *
+ * @return jwt
+ */
+ @Bean
+ public JwtAccessTokenConverter jwtAccessTokenConverter() {
+ JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
+ converter.setVerifierKey(getPubKey());
+ return converter;
+ }
+
+ /**
+ * 非对称密钥加密,获取 public key。
+ * 自动选择加载方式。
+ *
+ * @return public key
+ */
+ private String getPubKey() {
+ // 如果本地没有密钥,就从授权服务器中获取
+ return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue())
+ ? getKeyFromAuthorizationServer()
+ : resourceServerProperties.getJwt().getKeyValue();
+ }
+
+ /**
+ * 本地没有公钥的时候,从服务器上获取
+ * 需要进行 Basic 认证
+ *
+ * @return public key
+ */
+ private String getKeyFromAuthorizationServer() {
+ ObjectMapper objectMapper = new ObjectMapper();
+ HttpHeaders httpHeaders = new HttpHeaders();
+ httpHeaders.add(HttpHeaders.AUTHORIZATION, encodeClient());
+ HttpEntity requestEntity = new HttpEntity<>(null, httpHeaders);
+ String pubKey = new RestTemplate()
+ .getForObject(resourceServerProperties.getJwt().getKeyUri(), String.class, requestEntity);
+ try {
+ JSONObject body = objectMapper.readValue(pubKey, JSONObject.class);
+ log.info("Get Key From Authorization Server.");
+ return body.getStr("value");
+ } catch (IOException e) {
+ log.error("Get public key error: {}", e.getMessage());
+ }
+ return null;
+ }
+
+ /**
+ * 客户端信息
+ *
+ * @return basic
+ */
+ private String encodeClient() {
+ return "Basic " + Base64.getEncoder().encodeToString((resourceServerProperties.getClientId()
+ + ":" + resourceServerProperties.getClientSecret()).getBytes());
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java
new file mode 100644
index 0000000..9c6ed62
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java
@@ -0,0 +1,60 @@
+package com.xkcoding.oauth.controller;
+
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 测试接口.
+ *
+ * @author EchoCow
+ * @date 2020/1/9 下午2:37
+ */
+@RestController
+public class TestController {
+
+ /**
+ * 拥有 ROLE_ADMIN 的用户才能访问的资源
+ *
+ * @return ADMIN
+ */
+ @PreAuthorize("hasRole('ADMIN')")
+ @GetMapping("/admin")
+ public String admin() {
+ return "ADMIN";
+ }
+
+ /**
+ * 拥有 ROLE_TEST 的用户才能访问的资源
+ *
+ * @return TEST
+ */
+ @PreAuthorize("hasRole('TEST')")
+ @GetMapping("/test")
+ public String test() {
+ return "TEST";
+ }
+
+ /**
+ * scope 有 READ 的用户资源才能访问
+ *
+ * @return READ
+ */
+ @PreAuthorize("#oauth2.hasScope('READ')")
+ @GetMapping("/read")
+ public String read() {
+ return "READ";
+ }
+
+ /**
+ * scope 有 WRITE 的用户资源才能访问
+ *
+ * @return WRITE
+ */
+ @PreAuthorize("#oauth2.hasScope('WRITE')")
+ @GetMapping("/write")
+ public String write() {
+ return "WRITE";
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml
new file mode 100644
index 0000000..9d6558a
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml
@@ -0,0 +1,30 @@
+server:
+ port: 8081
+security:
+ oauth2:
+ resource:
+ token-info-uri: http://localhost:8080/oauth/check_token
+ jwt:
+ key-alias: oauth2
+ # 如果没有此项会去请求授权服务器获取
+ key-value: |
+ -----BEGIN PUBLIC KEY-----
+ MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/
+ xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ
+ b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ
+ pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI
+ 0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H
+ 5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA
+ mwIDAQAB
+ -----END PUBLIC KEY-----
+ key-uri: http://localhost:8080/oauth/token_key
+ id: oauth2
+ client:
+ client-id: oauth2
+ client-secret: oauth2
+ access-token-uri: http://localhost:8080/oauth/token
+ scope: READ
+
+logging:
+ level:
+ org.springframework.security: debug
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java
new file mode 100644
index 0000000..774a4ec
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java
@@ -0,0 +1,38 @@
+package com.xkcoding.oauth;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails;
+
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+/**
+ * .
+ *
+ * @author EchoCow
+ * @date 2020/1/9 下午3:44
+ */
+public class AuthorizationTest {
+ public static final String AUTHORIZATION_SERVER = "http://127.0.0.1:8080";
+
+ protected OAuth2RestTemplate oauth2RestTemplate(String username, String password, List scope) {
+ ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
+ resource.setAccessTokenUri(AUTHORIZATION_SERVER + "/oauth/token");
+ resource.setClientId("oauth2");
+ resource.setClientSecret("oauth2");
+ resource.setId("oauth2");
+ resource.setScope(scope);
+ resource.setUsername(username);
+ resource.setPassword(password);
+ return new OAuth2RestTemplate(resource);
+ }
+
+ @Test
+ void testAccessTokenWhenPassed() {
+ assertNotNull(oauth2RestTemplate("admin", "123456", Collections.singletonList("READ"))
+ .getAccessToken());
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java
new file mode 100644
index 0000000..ea0a432
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java
@@ -0,0 +1,83 @@
+package com.xkcoding.oauth.controller;
+
+import com.xkcoding.oauth.AuthorizationTest;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.oauth2.client.OAuth2RestTemplate;
+import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.springframework.http.HttpMethod.GET;
+
+/**
+ * .
+ *
+ * @author EchoCow
+ * @date 2020/1/9 下午3:46
+ */
+public class TestControllerTest extends AuthorizationTest {
+
+ private static final String URL = "http://127.0.0.1:8081";
+
+ @Test
+ @DisplayName("ROLE_ADMIN 角色测试")
+ void testAdminRoleSucceedAndTestRoleFailedWhenPassed() {
+ OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ"));
+ ResponseEntity response = template.exchange(URL + "/admin", GET, null, String.class);
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals("ADMIN", response.getBody());
+ assertThrows(OAuth2AccessDeniedException.class,
+ () -> template.exchange(URL + "/test", GET, null, String.class));
+ }
+
+ @Test
+ @DisplayName("ROLE_Test 角色测试")
+ void testTestRoleSucceedWhenPassed() {
+ OAuth2RestTemplate template = oauth2RestTemplate("test", "123456", Collections.singletonList("READ"));
+ ResponseEntity response = template.exchange(URL + "/test", GET, null, String.class);
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals("TEST", response.getBody());
+ assertThrows(OAuth2AccessDeniedException.class,
+ () -> template.exchange(URL + "/admin", GET, null, String.class));
+ }
+
+ @Test
+ @DisplayName("SCOPE_READ 授权域测试")
+ void testScopeReadWhenPassed() {
+ OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ"));
+ ResponseEntity response = template.exchange(URL + "/read", GET, null, String.class);
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals("READ", response.getBody());
+ assertThrows(OAuth2AccessDeniedException.class,
+ () -> template.exchange(URL + "/write", GET, null, String.class));
+ }
+
+ @Test
+ @DisplayName("SCOPE_WRITE 授权域测试")
+ void testScopeWriteWhenPassed() {
+ OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("WRITE"));
+ ResponseEntity response = template.exchange(URL + "/write", GET, null, String.class);
+ assertEquals(HttpStatus.OK, response.getStatusCode());
+ assertEquals("WRITE", response.getBody());
+ assertThrows(OAuth2AccessDeniedException.class,
+ () -> template.exchange(URL + "/read", GET, null, String.class));
+ }
+
+ @Test
+ @DisplayName("SCOPE 测试")
+ void testScopeWhenPassed() {
+ OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Arrays.asList("READ", "WRITE"));
+ ResponseEntity writeResponse = template.exchange(URL + "/write", GET, null, String.class);
+ assertEquals(HttpStatus.OK, writeResponse.getStatusCode());
+ assertEquals("WRITE", writeResponse.getBody());
+ ResponseEntity readResponse = template.exchange(URL + "/read", GET, null, String.class);
+ assertEquals(HttpStatus.OK, readResponse.getStatusCode());
+ assertEquals("READ", readResponse.getBody());
+ }
+}