@@ -7,6 +7,7 @@ | |||
<version>1.0.0-SNAPSHOT</version> | |||
<modules> | |||
<module>spring-boot-demo-oauth-authorization-server</module> | |||
<module>spring-boot-demo-oauth-resource-server</module> | |||
</modules> | |||
<packaging>pom</packaging> | |||
@@ -26,31 +27,6 @@ | |||
</properties> | |||
<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> | |||
<groupId>mysql</groupId> | |||
@@ -11,5 +11,34 @@ | |||
<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> |
@@ -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: | |||
@@ -43,7 +43,7 @@ | |||
<v-btn outlined color="info" @click="previous">{{previousText}}</v-btn> | |||
<v-spacer></v-spacer> | |||
<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-form> | |||
</v-card> | |||
@@ -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` 去访问资源即可。 |
@@ -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> |
@@ -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); | |||
} | |||
} |
@@ -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(); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |
@@ -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"; | |||
} | |||
} |
@@ -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 |
@@ -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()); | |||
} | |||
} |
@@ -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()); | |||
} | |||
} |