Browse Source

Merge branch 'pull/86' into dev

# Conflicts:
#	spring-boot-demo-oauth/pom.xml
#	spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
#	spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
#	spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
pull/1/head
Yangkai.Shen 4 years ago
parent
commit
85a5cf3afc
13 changed files with 499 additions and 28 deletions
  1. +1
    -26
      spring-boot-demo-oauth/pom.xml
  2. +29
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
  3. +1
    -1
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
  4. +1
    -1
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
  5. +59
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc
  6. +31
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml
  7. +21
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java
  8. +43
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java
  9. +102
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java
  10. +60
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java
  11. +30
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml
  12. +38
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java
  13. +83
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java

+ 1
- 26
spring-boot-demo-oauth/pom.xml View File

@@ -7,6 +7,7 @@
<version>1.0.0-SNAPSHOT</version> <version>1.0.0-SNAPSHOT</version>
<modules> <modules>
<module>spring-boot-demo-oauth-authorization-server</module> <module>spring-boot-demo-oauth-authorization-server</module>
<module>spring-boot-demo-oauth-resource-server</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>


@@ -26,32 +27,6 @@
</properties> </properties>


<dependencies> <dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<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-data-jpa</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency> <dependency>
<groupId>mysql</groupId> <groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId> <artifactId>mysql-connector-java</artifactId>


+ 29
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml View File

@@ -11,5 +11,34 @@


<artifactId>spring-boot-demo-oauth-authorization-server</artifactId> <artifactId>spring-boot-demo-oauth-authorization-server</artifactId>


<dependencies>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

</dependencies>


</project> </project>

+ 1
- 1
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml View File

@@ -3,7 +3,7 @@ server:


spring: spring:
datasource: datasource:
url: jdbc:mysql://localhost:3306/oauth
url: jdbc:mysql://localhost:3306/oauth?allowPublicKeyRetrieval=true
username: root username: root
password: 123456 password: 123456
hikari: hikari:


+ 1
- 1
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html View File

@@ -43,7 +43,7 @@
<v-btn outlined color="info" @click="previous">{{previousText}}</v-btn> <v-btn outlined color="info" @click="previous">{{previousText}}</v-btn>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-btn color="info" type="button" @click="next" v-show="window === 0">下一步</v-btn> <v-btn color="info" type="button" @click="next" v-show="window === 0">下一步</v-btn>
<v-btn color="info" type="submit" v-show="window === 1">登录</v-btn>
<v-btn color="info" type="submit" @click="next" v-show="window === 1">登录</v-btn>
</v-card-actions> </v-card-actions>
</v-form> </v-form>
</v-card> </v-card>


+ 59
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc View File

@@ -0,0 +1,59 @@
= spring-boot-demo-oauth-resource-server
Doc Writer <lzy@echocow.cn>
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` 去访问资源即可。

+ 31
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml View File

@@ -0,0 +1,31 @@
<?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">
<parent>
<artifactId>spring-boot-demo-oauth</artifactId>
<groupId>com.xkcoding</groupId>
<version>1.0.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>spring-boot-demo-oauth-resource-server</artifactId>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>

<dependency>
<groupId>org.springframework.security.oauth.boot</groupId>
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
<version>${spring.boot.version}</version>
</dependency>
</dependencies>
</project>

+ 21
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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);
}

}

+ 43
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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();
}

}

+ 102
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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<String> 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());
}
}

+ 60
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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";
}

}

+ 30
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml View File

@@ -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

+ 38
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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<String> 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());
}
}

+ 83
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java View File

@@ -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 <a href="https://echocow.cn">EchoCow</a>
* @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<String> 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<String> 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<String> 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<String> 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<String> writeResponse = template.exchange(URL + "/write", GET, null, String.class);
assertEquals(HttpStatus.OK, writeResponse.getStatusCode());
assertEquals("WRITE", writeResponse.getBody());
ResponseEntity<String> readResponse = template.exchange(URL + "/read", GET, null, String.class);
assertEquals(HttpStatus.OK, readResponse.getStatusCode());
assertEquals("READ", readResponse.getBody());
}
}

Loading…
Cancel
Save