Browse Source

Add spring-boot-demo-oauth-authorization-server.

pull/1/head
EchoCow Yangkai.Shen 4 years ago
parent
commit
93a9b8b0d9
48 changed files with 2256 additions and 23 deletions
  1. +14
    -0
      pom.xml
  2. +51
    -1
      spring-boot-demo-oauth/pom.xml
  3. +273
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc
  4. BIN
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png
  5. BIN
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png
  6. BIN
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png
  7. BIN
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png
  8. +15
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
  9. +2
    -1
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
  10. +31
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
  11. +30
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
  12. +54
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
  13. +74
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
  14. +54
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
  15. +22
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
  16. +43
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
  17. +55
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
  18. +14
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
  19. +191
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
  20. +49
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java
  21. +55
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java
  22. +33
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java
  23. +24
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java
  24. +67
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java
  25. +59
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java
  26. +73
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java
  27. +76
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java
  28. +7
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java
  29. +22
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
  30. BIN
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks
  31. +9
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt
  32. +55
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/authorization.html
  33. +33
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html
  34. +45
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html
  35. +110
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
  36. +44
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html
  37. +155
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html
  38. +22
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java
  39. +125
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java
  40. +94
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java
  41. +39
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java
  42. +26
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java
  43. +40
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java
  44. +21
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml
  45. +10
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql
  46. +40
    -0
      spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql
  47. +0
    -4
      spring-boot-demo-oauth/src/main/resources/application.yml
  48. +0
    -17
      spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java

+ 14
- 0
pom.xml View File

@@ -86,6 +86,20 @@
<user.agent.version>1.20</user.agent.version>
</properties>

<repositories>
<repository>
<id>aliyun</id>
<name>aliyun</name>
<url>https://maven.aliyun.com/repository/public</url>
<releases>
<enabled>true</enabled>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>

<dependencyManagement>
<dependencies>
<dependency>


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

@@ -5,7 +5,10 @@

<artifactId>spring-boot-demo-oauth</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<modules>
<module>spring-boot-demo-oauth-authorization-server</module>
</modules>
<packaging>pom</packaging>

<name>spring-boot-demo-oauth</name>
<description>Demo project for Spring Boot</description>
@@ -28,10 +31,49 @@
<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>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
@@ -44,6 +86,14 @@
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>

<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.5.2</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>


+ 273
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc View File

@@ -0,0 +1,273 @@
= spring-boot-demo-oauth-authorization-server
Doc Writer <lzy@echocow.cn>
v1.0, 2019-01-07
:toc:

spring boot oauth2 授权服务器,

- 授权码模式、密码模式、刷新令牌
- 自定义 UserDetailService
- 自定义 ClientDetailService
- jwt 非对称加密
- 自定义登录授权页面

> SQL 语句
>
> - DDL: `src/test/resources/schema.sql`
> - DML: `src/test/resources/import.sql`

测试用例使用 h2 数据库,测试数据如下:

.测试客户端
|===
|客户端 id |客户端密钥 |资源服务器名称 |授权类型 | scopes| 回调地址

|oauth2
|oauth2
|oauth2
|authorization_code,password,refresh_token
|READ,WRITE
|http://example.com

|test
|oauth2
|oauth2
|authorization_code,password,refresh_token
|READ
|http://example.com


|error
|oauth2
|test
|authorization_code,password,refresh_token
|READ
|http://example.com
|===

.测试用户
|===
|用户名 |密码 |角色

|admin
|123456
|ROLE_ADMIN

|test
|123456
|ROLE_TEST

|===

== 授权码模式

> 测试用例:`com.xkcoding.oauth.oauth.AuthorizationCodeGrantTests`

=== 获取授权码

- 请求地址: http://localhost:8080/oauth/authorize?response_type=code&client_id=oauth2&redirect_uri=http://example.com&scope=READ
- 用户名:admin
- 密码:123456

image::image/Login.png[login]

=== 确认授权

登录成功以后,进入确认授权页面。已经确认过的用户,不会再次要求确认。

image::image/Confirm.png[confirm]

确认授权后,获取授权码

image::image/Code.png[code]

=== 请求 token

使用以下代码可以直接请求 token

[shell]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'grant_type=authorization_code' \
--data-urlencode 'code=GgX6QD' \
--data-urlencode 'redirect_uri=http://example.com' \
--data-urlencode 'client_id=oauth2' \
--data-urlencode 'scope=READ WRITE'
----

得到 token

[token]
----
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiZjAyMDhiNTUtYTJjYS00NjI4LTg5YjEtNzI5MzY4MzAxOWNhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.RqJpsin6bMnwI57cGpODTplLeW_gtNWHo_l4SimyRLsnxpCWm5oY1EOb4qVHpXvCbhNsUj69D462P7le13OOmexysZIQhaoGZ_CbIlEp63XsCnr5nSKeX3dgQlyTUDjOUL0WUtY2lKqLCGMeX_rpVhfmSh3b7MC0Ntxq5ao-943QMXGRIeRvJgSkvfY2HBN6-zx1H6rE0wxnUfBC1M08kUkFYlSmsFchiz-E_oTzJvE2D8lA9g-eEFU6cZ_els4Q77Vvc_O6SXUZ7o65vFyLyUjLvh9QF1825SGIUUdXTUYSZjnSAXChhRIAT5pLRHK-gthIzpOaWrgj6ebUoG02Eg",
"token_type": "bearer",
"refresh_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw",
"expires_in": 5999,
"scope": "READ",
"jti": "f0208b55-a2ca-4628-89b1-7293683019ca"
}
----

== 密码模式

> 测试用例:`com.xkcoding.oauth.oauth.ResourceOwnerPasswordGrantTests`

`test` 用户进行授权

[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'password=123456' \
--data-urlencode 'username=test' \
--data-urlencode 'grant_type=password' \
--data-urlencode 'scope=READ WRITE'
----

== 刷新令牌

携带 `refresh_token` 去请求

[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'grant_type=refresh_token' \
--data-urlencode 'refresh_token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsib2F1dGgyIl0sInVzZXJfbmFtZSI6ImFkbWluIiwic2NvcGUiOlsiUkVBRCJdLCJhdGkiOiJmMDIwOGI1NS1hMmNhLTQ2MjgtODliMS03MjkzNjgzMDE5Y2EiLCJleHAiOjE1NzgzODY4MTYsImF1dGhvcml0aWVzIjpbIlJPTEVfQURNSU4iXSwianRpIjoiMGViNTU2MTQtYjgxYS00MTFmLTg1MTAtZThkMjZmODJmMjJhIiwiY2xpZW50X2lkIjoib2F1dGgyIn0.CBGcjirkf-3187SgbZr0ikauiCS8U9YLaoR4sNlRQjd-gaIeF5PChnIs_yAmG_VpqPFlPRdSl8DA05S2QnFpT3TkRjyP-LPDZgsVAPfczMAdVywU1zOKYZeq-gM6p9bmGEabbZoBlIxOImsjeyFSCui6UtRTZjNlj3AhGIzvs52T8bDqC796iHPDZvJ97MMgsEiRyu-mxDm1o1LMuBX9RHCx9rAkBVf52q36bqWMcYAlDOu1wYjpmhalSLZyWcmraQvClEitXGJI4eTFapTnuXQuWFIL-973V_5Shw98-bk65zZQOEheazHrUf-n4h-sYT4akehnYSVxX2UIg9XsCw'
----

== 解析令牌

携带令牌解析

[source]
----
curl --location --request POST 'http://127.0.0.1:8080/oauth/check_token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg==' \
--data-urlencode 'token='
----

解析结果

[source]
----
{
"aud": [
"oauth2"
],
"user_name": "admin",
"scope": [
"READ",
"WRITE"
],
"active": true,
"exp": 1578389936,
"authorities": [
"ROLE_ADMIN"
],
"jti": "fe59fce9-6764-435e-8fa7-7320e11af811",
"client_id": "oauth2"
}
----

== 退出登录

授权码模式登陆是在授权服务器上登录的,所以退出也要在授权服务器上退出。

携带回调地址进行退出,退出完成后跳转到回调地址:

image::image/Logout.png[logout]

退出以后自动跳转到回调地址(要加 `http` 或 `https`)

== 获取公钥

通过访问 '/oauth/token_key' 获取 JWT 公钥

[source]
----
curl --location --request GET 'http://127.0.0.1:8080/oauth/token_key' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--header 'Authorization: Basic b2F1dGgyOm9hdXRoMg=='
----

获取后

[source]
----
{
"alg": "SHA256withRSA",
"value": "-----BEGIN PUBLIC KEY-----\n......\n-----END PUBLIC KEY-----"
}
----

== 核心配置

=== 授权服务器配置

[Oauth2AuthorizationServerConfig]
----
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
// 自定义用户
.userDetailsService(sysUserService)
// 内存存储
.tokenStore(tokenStore)
// jwt 令牌转换
.accessTokenConverter(jwtAccessTokenConverter);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库读取我们自定义的客户端信息
clients.withClientDetails(sysClientDetailsService);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
// 获取 token key 需要进行 basic 认证客户端信息
.tokenKeyAccess("isAuthenticated()")
// 获取 token 信息同样需要 basic 认证客户端信息
.checkTokenAccess("isAuthenticated()");
}
----

=== 安全配置

[WebSecurityConfig]
----
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 开启表单登录,授权码模式的时候进行登录
.formLogin()
// 路径等
.loginPage("/oauth/login")
.loginProcessingUrl("/authorization/form")
// 失败以后携带错误信息进行再次跳转登录页面
.failureHandler(clientLoginFailureHandler)
.and()
// 退出登录相关
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(clientLogoutSuccessHandler)
.and()
// 授权服务器安全配置
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest()
.authenticated();
}
----

== 参考

- https://echocow.cn/articles/2019/07/14/1563096109754.html[Spring Security Oauth2 从零到一完整实践(三)授权服务器 ]

BIN
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png View File

Before After
Width: 1381  |  Height: 403  |  Size: 30 kB

BIN
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png View File

Before After
Width: 1111  |  Height: 794  |  Size: 23 kB

BIN
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png View File

Before After
Width: 1111  |  Height: 794  |  Size: 22 kB

BIN
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png View File

Before After
Width: 1515  |  Height: 705  |  Size: 24 kB

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

@@ -0,0 +1,15 @@
<?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-authorization-server</artifactId>


</project>

spring-boot-demo-oauth/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java → spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java View File

@@ -2,7 +2,6 @@ package com.xkcoding.oauth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.GetMapping;

/**
* <p>
@@ -16,6 +15,8 @@ import org.springframework.web.bind.annotation.GetMapping;
* @copyright: Copyright (c) 2019
* @version: V1.0
* @modified: yangkai.shen
* @modified: EchoCow
* @date: Modified in 2020-01-6 21:12
*/
@SpringBootApplication
public class SpringBootDemoOauthApplication {

+ 31
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java View File

@@ -0,0 +1,31 @@
package com.xkcoding.oauth.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.URLEncoder;

/**
* 登录失败处理器,失败后携带失败信息重定向到登录地址重新登录.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/7 下午1:01
*/
@Slf4j
@Component
public class ClientLoginFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException {
log.debug("Login failed!");
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.sendRedirect("/oauth/login?error="
+ URLEncoder.encode(exception.getLocalizedMessage(), "UTF-8"));
}
}

+ 30
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java View File

@@ -0,0 +1,30 @@
package com.xkcoding.oauth.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.logout.LogoutSuccessHandler;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
* 客户团退出登录成功处理器.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午22:11
*/
@Slf4j
@Component
public class ClientLogoutSuccessHandler implements LogoutSuccessHandler {

@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException {
response.setStatus(HttpStatus.FOUND.value());
// 跳转到客户端的回调地址
response.sendRedirect(request.getParameter("redirectUrl"));
}

}

+ 54
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java View File

@@ -0,0 +1,54 @@
package com.xkcoding.oauth.config;

import com.xkcoding.oauth.service.SysClientDetailsService;
import com.xkcoding.oauth.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.JwtAccessTokenConverter;

/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:32
*/
@Configuration
@RequiredArgsConstructor
@EnableAuthorizationServer
public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
private final SysClientDetailsService sysClientDetailsService;
private final SysUserService sysUserService;
private final TokenStore tokenStore;
private final AuthenticationManager authenticationManager;
private final JwtAccessTokenConverter jwtAccessTokenConverter;

@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
endpoints.authenticationManager(authenticationManager)
.userDetailsService(sysUserService)
.tokenStore(tokenStore)
.accessTokenConverter(jwtAccessTokenConverter);
}

@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
// 从数据库读取我们自定义的客户端信息
clients.withClientDetails(sysClientDetailsService);
}

@Override
public void configure(AuthorizationServerSecurityConfigurer security) {
security
// 获取 token key 需要进行 basic 认证客户端信息
.tokenKeyAccess("isAuthenticated()")
// 获取 token 信息同样需要 basic 认证客户端信息
.checkTokenAccess("isAuthenticated()");
}
}

+ 74
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java View File

@@ -0,0 +1,74 @@
package com.xkcoding.oauth.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
import org.springframework.core.io.ClassPathResource;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.InMemoryTokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.KeyStoreKeyFactory;

import java.security.KeyPair;

/**
* token 相关配置.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:33
*/
@Configuration
@RequiredArgsConstructor
public class Oauth2AuthorizationTokenConfig {

/**
* 声明 内存 TokenStore 实现,用来存储 token 相关.
* 默认实现有 mysql、redis
*
* @return InMemoryTokenStore
*/
@Bean
@Primary
public TokenStore tokenStore() {
return new InMemoryTokenStore();
}

/**
* jwt 令牌 配置,非对称加密
*
* @return 转换器
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter() {
final JwtAccessTokenConverter accessTokenConverter = new JwtAccessTokenConverter();
accessTokenConverter.setKeyPair(keyPair());
return accessTokenConverter;
}

/**
* 密钥 keyPair.
* 可用于生成 jwt / jwk.
*
* @return keyPair
*/
@Bean
public KeyPair keyPair() {
KeyStoreKeyFactory keyStoreKeyFactory = new KeyStoreKeyFactory(new ClassPathResource("oauth2.jks"), "123456".toCharArray());
return keyStoreKeyFactory.getKeyPair("oauth2");
}

/**
* 加密方式,使用 BCrypt.
* 参数越大加密次数越多,时间越久.
* 默认为 10.
*
* @return PasswordEncoder
*/
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

+ 54
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java View File

@@ -0,0 +1,54 @@
package com.xkcoding.oauth.config;

import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Bean;
import org.springframework.security.authentication.AuthenticationManager;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;

/**
* 安全配置.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:27
*/
@EnableWebSecurity
@RequiredArgsConstructor
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

private final ClientLogoutSuccessHandler clientLogoutSuccessHandler;
private final ClientLoginFailureHandler clientLoginFailureHandler;

@Override
protected void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginPage("/oauth/login")
.failureHandler(clientLoginFailureHandler)
.loginProcessingUrl("/authorization/form")
.and()
.logout()
.logoutUrl("/oauth/logout")
.logoutSuccessHandler(clientLogoutSuccessHandler)
.and()
.authorizeRequests()
.antMatchers("/oauth/**").permitAll()
.anyRequest()
.authenticated();
}

/**
* 授权管理.
*
* @return 认证管理对象
* @throws Exception 认证异常信息
*/
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
}

+ 22
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java View File

@@ -0,0 +1,22 @@
/**
* spring security oauth2 的相关配置。
* 使用 spring boot oauth2 自动配置。
* {@link com.xkcoding.oauth.config.Oauth2AuthorizationServerConfig}
* 授权服务器相关的配置,主要设置授权服务器如何读取客户端、用户信息和一些端点配置
* 可以在这里配置更多的东西,例如端点映射,token 增强等
*
* {@link com.xkcoding.oauth.config.Oauth2AuthorizationTokenConfig}
* 授权服务器 token 相关的配置,主要设置 jwt、加密方式等信息
*
* {@link com.xkcoding.oauth.config.ClientLogoutSuccessHandler}
* 资源服务器退出以后的处理。在授权码模式中,所有的客户端都需要跳转到授权服务器进行登录
* 当登录成功以后跳转到回调地址,如果用户需要登出,也要跳转到授权服务器这里进行登出
* 但是 spring security oauth2 似乎并没有这个逻辑。
* 所以自己给登出端点加了一个 redirect_url 参数,表示登出成功以后要跳转的地址
* 这个处理器就是来完成登出成功以后的跳转操作的。
*
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/7 上午9:16
*/
package com.xkcoding.oauth.config;

+ 43
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java View File

@@ -0,0 +1,43 @@
package com.xkcoding.oauth.controller;

import org.springframework.security.oauth2.provider.AuthorizationRequest;
import org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.SessionAttributes;
import org.springframework.web.servlet.ModelAndView;

import java.util.Map;

/**
* 自定义确认授权页面.
* 需要注意的是: 不能在代码中 setComplete,因为整个授权流程并没有结束
* 我们只是在中途修改了它确认的一些信息而已。
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午4:42
*/
@Controller
@SessionAttributes("authorizationRequest")
public class AuthorizationController {

/**
* 自定义确认授权页面
* 当然你也可以使用 {@link AuthorizationEndpoint#setUserApprovalPage(String)} 方法
* 进行设置,但是 model 就没有那么灵活了
*
* @param model model
* @return ModelAndView
*/
@GetMapping("/oauth/confirm_access")
public ModelAndView getAccessConfirmation(Map<String, Object> model) {
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
ModelAndView view = new ModelAndView();
view.setViewName("authorization");
view.addObject("clientId", authorizationRequest.getClientId());
// 传递 scope 过去,Set 集合
view.addObject("scopes", authorizationRequest.getScope());
return view;
}

}

+ 55
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java View File

@@ -0,0 +1,55 @@
package com.xkcoding.oauth.controller;

import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.ResourceAccessException;
import org.springframework.web.servlet.ModelAndView;

import java.security.Principal;
import java.util.Objects;

/**
* 页面控制器.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午4:30
*/
@Controller
@RequestMapping("/oauth")
@RequiredArgsConstructor
public class Oauth2Controller {

/**
* 授权码模式跳转到登录页面
*
* @return view
*/
@GetMapping("/login")
public String loginView() {
return "login";
}

/**
* 退出登录
*
* @param redirectUrl 退出完成后的回调地址
* @param principal 用户信息
* @return 结果
*/
@GetMapping("/logout")
public ModelAndView logoutView(
@RequestParam("redirect_url") String redirectUrl, Principal principal) {
if (Objects.isNull(principal)) {
throw new ResourceAccessException("请求错误,用户尚未登录");
}
ModelAndView view = new ModelAndView();
view.setViewName("logout");
view.addObject("user", principal.getName());
view.addObject("redirectUrl", redirectUrl);
return view;
}

}

+ 14
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java View File

@@ -0,0 +1,14 @@
/**
* 控制器。除了业务逻辑的以外,提供两个控制器来帮助完成自定义:
* {@link com.xkcoding.oauth.controller.AuthorizationController}
* 自定义的授权控制器,重新设置到我们的界面中去,不使用他的默认实现
*
* {@link com.xkcoding.oauth.controller.Oauth2Controller}
* 页面跳转的控制器,这里拿出来是因为真的可以做很多事。比如登录的时候携带点什么
* 或者退出的时候携带什么标识,都可以。
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/7 上午11:25
* @see org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
*/
package com.xkcoding.oauth.controller;

+ 191
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java View File

@@ -0,0 +1,191 @@
package com.xkcoding.oauth.entity;

import lombok.Data;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.client.BaseClientDetails;

import javax.persistence.*;
import java.util.*;
import java.util.stream.Collectors;

/**
* 客户端信息.
* 这里实现了 ClientDetails 接口
* 个人建议不应该在实体类里面写任何逻辑代码
* 而为了避免实体类耦合严重不应该去实现这个接口的
* 但是这里为了演示和 {@link SysUser} 不同的方式,所以就选择实现这个接口了
* 另一种方式是写一个方法将它转化为默认实现 {@link BaseClientDetails} 比较好一点并且简单很多
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午12:54
*/
@Data
@Table
@Entity
public class SysClientDetails implements ClientDetails {

/**
* 主键
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* client id
*/
private String clientId;

/**
* client 密钥
*/
private String clientSecret;

/**
* 资源服务器名称
*/
private String resourceIds;

/**
* 授权域
*/
private String scopes;

/**
* 授权类型
*/
private String grantTypes;

/**
* 重定向地址,授权码时必填
*/
private String redirectUrl;

/**
* 授权信息
*/
private String authorizations;

/**
* 授权令牌有效时间
*/
private Integer accessTokenValiditySeconds;

/**
* 刷新令牌有效时间
*/
private Integer refreshTokenValiditySeconds;

/**
* 自动授权请求域
*/
private String autoApproveScopes;

/**
* 是否安全
*
* @return 结果
*/
@Override
public boolean isSecretRequired() {
return this.clientSecret != null;
}

/**
* 是否有 scopes
*
* @return 结果
*/
@Override
public boolean isScoped() {
return this.scopes != null && !this.scopes.isEmpty();
}

/**
* scopes
*
* @return scopes
*/
@Override
public Set<String> getScope() {
return stringToSet(scopes);
}

/**
* 授权类型
*
* @return 结果
*/
@Override
public Set<String> getAuthorizedGrantTypes() {
return stringToSet(grantTypes);
}

@Override
public Set<String> getResourceIds() {
return stringToSet(resourceIds);
}


/**
* 获取回调地址
*
* @return redirectUrl
*/
@Override
public Set<String> getRegisteredRedirectUri() {
return stringToSet(redirectUrl);
}

/**
* 这里需要提一下
* 个人觉得这里应该是客户端所有的权限
* 但是已经有 scope 的存在可以很好的对客户端的权限进行认证了
* 那么在 oauth2 的四个角色中,这里就有可能是资源服务器的权限
* 但是一般资源服务器都有自己的权限管理机制,比如拿到用户信息后做 RBAC
* 所以在 spring security 的默认实现中直接给的是空的一个集合
* 这里我们也给他一个空的把
*
* @return GrantedAuthority
*/
@Override
public Collection<GrantedAuthority> getAuthorities() {
return Collections.emptyList();
}

/**
* 判断是否自动授权
*
* @param scope scope
* @return 结果
*/
@Override
public boolean isAutoApprove(String scope) {
if (autoApproveScopes == null || autoApproveScopes.isEmpty()) {
return false;
}
Set<String> authorizationSet = stringToSet(authorizations);
for (String auto : authorizationSet) {
if ("true".equalsIgnoreCase(auto) || scope.matches(auto)) {
return true;
}
}
return false;
}

/**
* additional information 是 spring security 的保留字段
* 暂时用不到,直接给个空的即可
*
* @return map
*/
@Override
public Map<String, Object> getAdditionalInformation() {
return Collections.emptyMap();
}

private Set<String> stringToSet(String s) {
return Arrays.stream(s.split(",")).collect(Collectors.toSet());
}
}

+ 49
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java View File

@@ -0,0 +1,49 @@
package com.xkcoding.oauth.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.codehaus.jackson.annotate.JsonIgnore;

import javax.persistence.*;
import java.util.Set;

/**
* 这里完全可以只用一个字段代替的
* 但是想了想还是模拟实际的情况来把
* 角色信息.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午12:44
*/
@Data
@Table
@Entity
@EqualsAndHashCode(exclude = {"users"})
@ToString(exclude = "users")
public class SysRole {

/**
* 主键.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* 角色名称,按照 spring security 规范
* 需要以 ROLE_ 开头.
*/
private String name;

/**
* 角色描述.
*/
private String description;

/**
* 当前角色所有用户.
*/
@ManyToMany(mappedBy = "roles", fetch = FetchType.EAGER)
private Set<SysUser> users;
}

+ 55
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java View File

@@ -0,0 +1,55 @@
package com.xkcoding.oauth.entity;

import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;

import javax.persistence.*;
import java.util.Set;

/**
* 用户实体.
* 避免实体类耦合,所以不去实现 {@link UserDetails} 接口
* 因为有且只有登录加载用户的时候才会需要这个接口
* 我们就手动构建一个 {@link User} 的默认实现就可以了
* 实现接口的方式可以参考 {@link SysClientDetails}
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午12:41
*/
@Data
@Table
@Entity
@EqualsAndHashCode(exclude = "roles")
@ToString(exclude = "roles")
public class SysUser {

/**
* 主键.
*/
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;

/**
* 用户名.
*/
private String username;

/**
* 密码.
*/
private String password;

/**
* 当前用户所有角色.
*/
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "sys_user_role",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<SysRole> roles;
}

+ 33
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java View File

@@ -0,0 +1,33 @@
package com.xkcoding.oauth.repostiory;

import com.xkcoding.oauth.entity.SysClientDetails;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;

import java.util.Optional;

/**
* 客户端信息.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:09
*/
public interface SysClientDetailsRepository extends JpaRepository<SysClientDetails, Long> {

/**
* 通过 clientId 查找客户端信息.
*
* @param clientId clientId
* @return 结果
*/
Optional<SysClientDetails> findFirstByClientId(String clientId);

/**
* 根据客户端 id 删除客户端
*
* @param clientId 客户端id
*/
@Modifying
void deleteByClientId(String clientId);

}

+ 24
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java View File

@@ -0,0 +1,24 @@
package com.xkcoding.oauth.repostiory;

import com.xkcoding.oauth.entity.SysUser;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

/**
* 用户信息仓库.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:08
*/
public interface SysUserRepository extends JpaRepository<SysUser, Long> {

/**
* 通过用户名查找用户.
*
* @param username 用户名
* @return 结果
*/
Optional<SysUser> findFirstByUsername(String username);

}

+ 67
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java View File

@@ -0,0 +1,67 @@
package com.xkcoding.oauth.service;

import com.xkcoding.oauth.entity.SysClientDetails;
import org.springframework.security.oauth2.provider.ClientAlreadyExistsException;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationService;
import org.springframework.security.oauth2.provider.NoSuchClientException;

import java.util.List;

/**
* 声明自己的实现.
* 参见 {@link ClientRegistrationService}
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:39
*/
public interface SysClientDetailsService extends ClientDetailsService {

/**
* 通过客户端 id 查询
*
* @param clientId 客户端 id
* @return 结果
*/
SysClientDetails findByClientId(String clientId);

/**
* 添加客户端信息.
*
* @param clientDetails 客户端信息
* @throws ClientAlreadyExistsException 客户端已存在
*/
void addClientDetails(SysClientDetails clientDetails) throws ClientAlreadyExistsException;

/**
* 更新客户端信息,不包括 clientSecret.
*
* @param clientDetails 客户端信息
* @throws NoSuchClientException 找不到客户端异常
*/
void updateClientDetails(SysClientDetails clientDetails) throws NoSuchClientException;

/**
* 更新客户端密钥.
*
* @param clientId 客户端 id
* @param clientSecret 客户端密钥
* @throws NoSuchClientException 找不到客户端异常
*/
void updateClientSecret(String clientId, String clientSecret) throws NoSuchClientException;

/**
* 删除客户端信息.
*
* @param clientId 客户端 id
* @throws NoSuchClientException 找不到客户端异常
*/
void removeClientDetails(String clientId) throws NoSuchClientException;

/**
* 查询所有
*
* @return 结果
*/
List<SysClientDetails> findAll();
}

+ 59
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java View File

@@ -0,0 +1,59 @@
package com.xkcoding.oauth.service;

import com.xkcoding.oauth.entity.SysUser;
import org.springframework.security.core.userdetails.UserDetailsService;

import java.util.List;


/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午3:44
*/
public interface SysUserService extends UserDetailsService {
/**
* 查询所有用户
*
* @return 用户
*/
List<SysUser> findAll();

/**
* 通过 id 查询用户
*
* @param id id
* @return 用户
*/
SysUser findById(Long id);

/**
* 创建用户
*
* @param sysUser 用户
*/
void createUser(SysUser sysUser);

/**
* 更新用户
*
* @param sysUser 用户
*/
void updateUser(SysUser sysUser);

/**
* 更新用户 密码
*
* @param id 用户 id
* @param password 用户密码
*/
void updatePassword(Long id, String password);

/**
* 删除用户.
*
* @param id id
*/
void deleteUser(Long id);
}

+ 73
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java View File

@@ -0,0 +1,73 @@
package com.xkcoding.oauth.service.impl;

import com.xkcoding.oauth.entity.SysClientDetails;
import com.xkcoding.oauth.repostiory.SysClientDetailsRepository;
import com.xkcoding.oauth.service.SysClientDetailsService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.provider.*;
import org.springframework.stereotype.Service;

import java.util.List;

/**
* 客户端 相关操作.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:37
*/
@Service
@RequiredArgsConstructor
public class SysClientDetailsServiceImpl implements SysClientDetailsService {

private final SysClientDetailsRepository sysClientDetailsRepository;
private final PasswordEncoder passwordEncoder;

@Override
public ClientDetails loadClientByClientId(String id) throws ClientRegistrationException {
return sysClientDetailsRepository.findFirstByClientId(id)
.orElseThrow(() -> new ClientRegistrationException("Loading client exception."));
}

@Override
public SysClientDetails findByClientId(String clientId) {
return sysClientDetailsRepository.findFirstByClientId(clientId)
.orElseThrow(() -> new ClientRegistrationException("Loading client exception."));
}

@Override
public void addClientDetails(SysClientDetails clientDetails) throws ClientAlreadyExistsException {
clientDetails.setId(null);
if (sysClientDetailsRepository.findFirstByClientId(clientDetails.getClientId()).isPresent()) {
throw new ClientAlreadyExistsException(String.format("Client id %s already exist.", clientDetails.getClientId()));
}
sysClientDetailsRepository.save(clientDetails);
}

@Override
public void updateClientDetails(SysClientDetails clientDetails) throws NoSuchClientException {
SysClientDetails exist = sysClientDetailsRepository.findFirstByClientId(clientDetails.getClientId())
.orElseThrow(() -> new NoSuchClientException("No such client!"));
clientDetails.setClientSecret(exist.getClientSecret());
sysClientDetailsRepository.save(clientDetails);
}

@Override
public void updateClientSecret(String clientId, String clientSecret) throws NoSuchClientException {
SysClientDetails exist = sysClientDetailsRepository.findFirstByClientId(clientId)
.orElseThrow(() -> new NoSuchClientException("No such client!"));
exist.setClientSecret(passwordEncoder.encode(clientSecret));
sysClientDetailsRepository.save(exist);
}

@Override
public void removeClientDetails(String clientId) throws NoSuchClientException {
sysClientDetailsRepository.deleteByClientId(clientId);
}

@Override
public List<SysClientDetails> findAll() {
return sysClientDetailsRepository.findAll();
}

}

+ 76
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java View File

@@ -0,0 +1,76 @@
package com.xkcoding.oauth.service.impl;

import com.xkcoding.oauth.entity.SysUser;
import com.xkcoding.oauth.repostiory.SysUserRepository;
import com.xkcoding.oauth.service.SysUserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;

import java.util.List;
import java.util.stream.Collectors;

/**
* 用户相关操作.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午3:06
*/
@Service
@RequiredArgsConstructor
public class SysUserServiceImpl implements SysUserService {

private final SysUserRepository sysUserRepository;
private final PasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
SysUser sysUser = sysUserRepository.findFirstByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found!"));
List<SimpleGrantedAuthority> roles = sysUser.getRoles().stream()
.map(sysRole -> new SimpleGrantedAuthority(sysRole.getName()))
.collect(Collectors.toList());
// 在这里手动构建 UserDetails 的默认实现
return new User(sysUser.getUsername(), sysUser.getPassword(), roles);
}

@Override
public List<SysUser> findAll() {
return sysUserRepository.findAll();
}

@Override
public SysUser findById(Long id) {
return sysUserRepository.findById(id)
.orElseThrow(() -> new RuntimeException("找不到用户"));
}

@Override
public void createUser(SysUser sysUser) {
sysUser.setId(null);
sysUserRepository.save(sysUser);
}

@Override
public void updateUser(SysUser sysUser) {
sysUser.setPassword(null);
sysUserRepository.save(sysUser);
}

@Override
public void updatePassword(Long id, String password) {
SysUser exist = findById(id);
exist.setPassword(passwordEncoder.encode(password));
sysUserRepository.save(exist);
}

@Override
public void deleteUser(Long id) {
sysUserRepository.deleteById(id);
}

}

+ 7
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java View File

@@ -0,0 +1,7 @@
/**
* service 层,继承并实现 spring 接口.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/7 上午9:16
*/
package com.xkcoding.oauth.service;

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

@@ -0,0 +1,22 @@
server:
port: 8080

spring:
datasource:
url: jdbc:mysql://localhost:3306/oauth
username: root
password: 123456
hikari:
data-source-properties:
useSSL: false
serverTimezone: GMT+8
useUnicode: true
characterEncoding: utf8
jpa:
hibernate:
ddl-auto: update
show-sql: true

logging:
level:
org.springframework.security: debug

BIN
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks View File


+ 9
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt View File

@@ -0,0 +1,9 @@
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/
xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ
b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ
pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI
0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H
5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA
mwIDAQAB
-----END PUBLIC KEY-----

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

@@ -0,0 +1,55 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/common::_header">
<title>确认您的授权信息</title>
</head>
<body>
<div id="app">
<v-app>
<v-content>
<v-row class="fill-height align-sm-center" justify="center">
<v-col class="pa-0">
<v-card id="form-card" class="px-6 pb-7 px-sm-10 pb-sm-9 mx-auto" outlined>
<v-form ref="auth" id="auth" th:action="@{/oauth/authorize}" method="post">
<v-spacer class="pt-6 pt-sm-12"></v-spacer>
<v-card-title class="justify-center headline">
确认应用的授权信息
</v-card-title>
<div class="text-center" style="height:44px">
<v-btn outlined rounded th:text="'客户端:' + ${#strings.toUpperCase(clientId)}"></v-btn>
</div>
<v-spacer></v-spacer>
<v-list shaped>
<v-subheader>当前应用将会获取您的以下权限:</v-subheader>
<v-list-item-group color="primary">
<v-list-item th:each="scope : ${scopes}">
<v-list-item-content>
<input type="hidden" th:name="'scope.' + ${scope}" value="true">
<v-list-item-title th:text="${#strings.toUpperCase(scope)}"></v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-item-group>
</v-list>
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
<input type="hidden" name="user_oauth_approval" value="true">
<v-card-actions class="mt-6">
<v-spacer></v-spacer>
<v-btn color="info" type="submit">确认授权</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-col>
</v-row>
</v-content>
</v-app>
</div>

<div th:include="common/common::_footer"></div>
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(),
})
</script>
</body>
</html>

+ 33
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html View File

@@ -0,0 +1,33 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:fragment="_header">
<!--/*@thymesVar id="title" type="java.lang.String"*/-->
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
<link href="https://fonts.googleapis.com/css?family=Roboto:100,300,400,500,700,900" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/@mdi/font@4.x/css/materialdesignicons.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.min.css" rel="stylesheet">
<style>
#form-card {
width: 30rem;
}
@media screen and (max-width: 450px) {
#form-card{
width: 100%;
height: 100%;
padding: 2rem !important;
}
}
.fill-height {
height: 100%;
}
</style>
</head>
<body>

<footer th:fragment="_footer">
<script src="https://cdn.jsdelivr.net/npm/vue@2.x/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vuetify@2.x/dist/vuetify.js"></script>
</footer>
</body>
</html>

+ 45
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html View File

@@ -0,0 +1,45 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/common::_header">
<title>发送了点小错误</title>
</head>
<body>
<div id="app">
<v-app>
<v-content>
<v-responsive class="fill-height">
<v-container class="fill-height">
<v-layout align-center justify-center>
<v-flex class="text-center">
<!--/*@thymesVar id="status" type="java.lang.String"*/-->
<!--/*@thymesVar id="message" type="java.lang.String"*/-->
<h1 class="display-1 blue--text" th:text="${status} + ' ' + ${message}">404 找不到页面</h1>
<p class="mt-2">~~~</p>
<v-btn outlined color="info" @click="handleBack">点击返回</v-btn>
</v-flex>
</v-layout>
</v-container>
</v-responsive>
</v-content>
</v-app>
</div>


<div th:include="common/common::_footer"></div>
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: () => ({
text: '未知错误',
code: 500
}),
methods: {
handleBack () {
window.history.go(-1)
}
}
})
</script>
</body>
</html>

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

@@ -0,0 +1,110 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/common::_header">
<title>欢迎登录</title>
</head>
<body>
<div id="app">
<v-app>
<v-content>
<v-row class="fill-height align-sm-center" justify="center">
<v-col class="pa-0">
<v-card id="form-card" class="px-6 pb-7 px-sm-10 pb-sm-9 mx-auto" outlined>
<v-form ref="login" id="login" th:action="@{/authorization/form}" method="post" @submit.native.prevent>
<v-spacer class="pt-6 pt-sm-12"></v-spacer>
<v-card-title class="justify-center headline">
欢迎登录
</v-card-title>
<div class="login-user pb-2 text-center" style="height:44px">
<v-btn outlined rounded>{{nameText}}</v-btn>
</div>
<v-spacer></v-spacer>
<v-card-subtitle class="text-center subtitle-1 pt-0">
<p th:if="${param.error == null}">{{infoText}}</p>
<p th:unless="${param.error == null}" th:text="${param.error}" style="color: red;"></p>
</v-card-subtitle>
<v-spacer></v-spacer>
<v-card-text>
<v-window v-model="window" style="min-height:180px">
<v-window-item :key="0">
<v-text-field label="用户名/手机号/邮箱" name="username" type="text" clearable :rules="usernameRules"
outlined v-model='user.username' @keyup.enter="next" ref="username" autofocus :counter="55">
</v-text-field>
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
</v-window-item>
<v-window-item :key="1">
<v-text-field label="账户密码" name="password" type="password" required clearable
outlined v-model='user.password' @keyup.enter="next" ref="password" autofocus>
</v-text-field>
</v-window-item>
</v-window>
</v-card-text>
<v-card-actions>
<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-card-actions>
</v-form>
</v-card>
</v-col>
</v-row>
</v-content>
</v-app>
</div>

<div th:include="common/common::_footer"></div>
<script >
new Vue({
el: '#app',
vuetify: new Vuetify(),
data: function () {
return {
window: 0,
previousText: '忘记密码',
infoText: '使用您的帐号进行登录',
nameText: 'DEMO',
user: {
username: null,
password: null
},
usernameRules:[
v => !!v || '请输入用户名/手机号/邮箱',
v => !!v && v.length <= 55 || '长度不合法'
],
passwordRules:[ v => !!v || '请输入密码' ]
}
},
watch: {
window: function (val) {
if (val === 0) {
this.infoText = '使用您的帐号进行登录'
this.previousText = '忘记密码'
this.nameText = 'DEMO'
} else if (val === 1) {
this.infoText = '要继续操作,请首先验证登录者是您本人'
this.previousText = '上一步'
this.nameText = this.user.username
}
}
},
created () {
this.window = 0
},
methods: {
previous() {
if (this.window === 0) {

} else {
this.window -= 1
}
},
next () {
if (this.window === 0) this.$refs.username.validate(true) && (this.window += 1)
else this.$refs.password.validate(true) && document.getElementById("login").submit()
}
}
})
</script>
</body>
</html>

+ 44
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html View File

@@ -0,0 +1,44 @@
<!DOCTYPE html>
<html lang="zh" xmlns:th="http://www.thymeleaf.org">
<head th:replace="common/common::_header">
<title>确认退出吗?</title>
</head>
<body>
<div id="app">
<v-app>
<v-content>
<v-row class="fill-height align-sm-center" justify="center">
<v-col class="pa-0">
<v-card id="form-card" class="px-6 pb-7 px-sm-10 pb-sm-9 mx-auto" outlined>
<v-form ref="auth" id="auth" th:action="@{/oauth/logout}" method="post">
<v-spacer class="pt-6 pt-sm-12"></v-spacer>
<v-card-title class="justify-center headline">
确认退出当前应用吗?
</v-card-title>
<div class="text-center" style="height:44px">
<v-btn outlined rounded th:text="'用户:' + ${#strings.toUpperCase(user)}"></v-btn>
</div>
<v-spacer></v-spacer>
<input type="hidden" name="_csrf" th:value="${_csrf.token}"/>
<input type="hidden" name="redirectUrl" th:value="${redirectUrl}"/>
<v-card-actions class="mt-6">
<v-spacer></v-spacer>
<v-btn color="info" type="submit">确认退出</v-btn>
</v-card-actions>
</v-form>
</v-card>
</v-col>
</v-row>
</v-content>
</v-app>
</div>

<div th:include="common/common::_footer"></div>
<script>
new Vue({
el: '#app',
vuetify: new Vuetify(),
})
</script>
</body>
</html>

+ 155
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html View File

@@ -0,0 +1,155 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<title></title>
<meta charset="utf-8"/>
<style type="text/css">
.lesson body {
margin: 0;
padding: 0;
background: #fff;
font-family: Verdana, Arial, Helvetica, sans-serif, serif;
font-size: 14px;
line-height: 24px;
}

.lesson div, .lesson p, .lesson span, .lesson img {
margin: 0;
padding: 0;
}

.lesson img {
border: none;
}

.lesson .container {
margin: 0 auto;
}

.lesson .title {
margin: 0 auto;
background: #efefef repeat-x;
height: 30px;
text-align: center;
font-weight: bold;
padding-top: 12px;
font-size: 16px;
color: #2d2d2d;
}

.lesson .content {
margin: 4px;
}

.lesson .headline {
padding: 6px;
color: #2d2d2d;
}

.lesson .top, .lesson .bottom {
display: block;
font-size: 1px;
}

.lesson .xb1, .lesson .xb2, .lesson .xb3, .lesson .xb4 {
display: block;
overflow: hidden;
}

.lesson .xb1, .lesson .xb2, .lesson .xb3 {
height: 1px;
}

.lesson .xb2, .lesson .xb3, .lesson .xb4 {
border-left: 1px solid #BCBCBC;
border-right: 1px solid #BCBCBC;
}

.lesson .xb1 {
margin: 0 5px;
background: #BCBCBC;
}

.lesson .xb2 {
margin: 0 3px;
border-width: 0 2px;
}

.lesson .xb3 {
margin: 0 2px;
}

.lesson .xb4 {
height: 2px;
margin: 0 1px;
}

.lesson .lesson-content {
display: block;
}

.lesson .line {
margin-top: 6px;
border-top: 1px dashed #B9B9B9;
padding: 4px;
}

.lesson .content {
padding: 6px;
color: #666666;
}

.lesson .foot {
padding: 6px;
color: #777;
}

.lesson .font-darkblue {
color: #006699;
font-weight: bold;
}

.lesson .font-lightblue {
color: #008BD1;
font-weight: bold;
}

.lesson .font-gray {
color: #888;
font-size: 12px;
}
</style>
</head>
<body>
<div class="lesson">
<div class="container">
<div class="title">云课程考试平台</div>
<div class="content">
<p class="headline"><b>亲爱的用户,你好!</b></p>
<b class="top"><b class="xb1"></b><b class="xb2"></b><b class="xb3"></b><b class="xb4"></b></b>
<div class="lesson-content">
<div class="content">
<p><!--/*@thymesVar id="type" type="java.lang.String"*/-->
<b th:text="'欢迎您' + ${type}">欢迎您注册</b><span id="userName" class="font-darkblue"> 云课程考试平台</span>
</p>
<p><b><!--/*@thymesVar id="type" type="java.lang.String"*/-->
<b th:text="${type}">你的邮件</b>的验证码:</b><span class="font-lightblue"><span style="border-bottom: 1px dashed rgb(204, 204, 204); z-index: 1; position: static;"> <!--/*@thymesVar id="code" type="java.lang.String"*/-->
<b th:text="${code}">验证码</b></span></span><br><span
class="font-gray">(请输入该验证码完成 <span></span> 验证,验证码
<!--/*@thymesVar id="time" type="java.lang.Integer"*/-->
<b th:text="${time}">10</b> 分钟内有效!)</span></p>
<div class="line">如果您未申请云课程学习平台 <!--/*@thymesVar id="type" type="java.lang.String"*/-->
<span th:text="${type}">$(type)</span> 服务,请忽略该邮件。
</div>
</div>
</div>
<b class="bottom"><b class="xb4"></b><b class="xb3"></b><b class="xb2"></b><b class="xb1"></b></b>
<p class="foot">如果仍有问题,请联系我们的管理员: <span
style="border-bottom: 1px dashed rgb(204, 204, 204); z-index: 1; position: static;">000-00000000
</span></p>
</div>
</div>
</div>
</body>
</html>

+ 22
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java View File

@@ -0,0 +1,22 @@
package com.xkcoding.oauth;

import org.junit.jupiter.api.Test;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午3:51
*/
public class PasswordEncodeTest {

private PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();

@Test
public void getPasswordWhenPassed() {
System.out.println(passwordEncoder.encode("oauth2"));
System.out.println(passwordEncoder.encode("123456"));
}
}

+ 125
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java View File

@@ -0,0 +1,125 @@
package com.xkcoding.oauth.oauth;

import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.oauth2.client.OAuth2RestTemplate;
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeResourceDetails;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;

import java.net.URI;
import java.util.Arrays;
import java.util.Collections;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import static com.xkcoding.oauth.oauth.AuthorizationServerInfo.getUrl;
import static org.junit.jupiter.api.Assertions.*;

/**
* 授权码模式测试.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午8:43
*/
public class AuthorizationCodeGrantTests {

private AuthorizationCodeResourceDetails resource = new AuthorizationCodeResourceDetails();
private AuthorizationServerInfo authorizationServerInfo = new AuthorizationServerInfo();

@BeforeEach
void setUp() {
resource.setAccessTokenUri(getUrl("/oauth/token"));
resource.setClientId("oauth2");
resource.setId("oauth2");
resource.setScope(Arrays.asList("READ", "WRITE"));
resource.setAccessTokenUri(getUrl("/oauth/token"));
resource.setUserAuthorizationUri(getUrl("/oauth/authorize"));
}

@Test
void testCannotConnectWithoutToken() {
OAuth2RestTemplate template = new OAuth2RestTemplate(resource);
assertThrows(UserRedirectRequiredException.class,
() -> template.getForObject(getUrl("/oauth/me"), String.class));
}

@Test
void testAttemptedTokenAcquisitionWithNoRedirect() {
AuthorizationCodeAccessTokenProvider provider = new AuthorizationCodeAccessTokenProvider();
assertThrows(UserRedirectRequiredException.class,
() -> provider.obtainAccessToken(resource, new DefaultAccessTokenRequest()));
}

/**
* 这里不使用他提供的是因为很多地方不符合我们的需要
* 比如 csrf,比如许多有些是自己自定义的端点这些
* 所以只有我们一步一步的来进行测试拿到授权码
*/
@Test
void testCodeAcquisitionWithCorrectContext() {
// 1. 请求登录页面获取 _csrf 的 value 以及 cookie
ResponseEntity<String> page = authorizationServerInfo.getForString("/oauth/login");
assertNotNull(page.getBody());
String cookie = page.getHeaders().getFirst("Set-Cookie");
HttpHeaders headers = new HttpHeaders();
headers.set("Cookie", cookie);
Matcher matcher = Pattern.compile("(?s).*name=\"_csrf\".*?value=\"([^\"]+).*").matcher(page.getBody());
assertTrue(matcher.find());

// 2. 添加表单数据
MultiValueMap<String, String> form = new LinkedMultiValueMap<>();
form.add("username", "admin");
form.add("password", "123456");
form.add("_csrf", matcher.group(1));

// 3. 登录授权并获取登录成功的 cookie
ResponseEntity<Void> response = authorizationServerInfo
.postForStatus("/authorization/form", headers, form);
assertNotNull(response);
cookie = response.getHeaders().getFirst("Set-Cookie");
headers = new HttpHeaders();
headers.set("Cookie", cookie);
headers.setAccept(Collections.singletonList(MediaType.ALL));

// 4. 请求到 确认授权页面 ,获取确认授权页面的 _csrf 的 value
ResponseEntity<String> confirm = authorizationServerInfo
.getForString("/oauth/authorize?response_type=code&client_id=oauth2&redirect_uri=http://example.com&scope=READ", headers);

headers = confirm.getHeaders();
// 确认过一次后,后面都会自动确认了,这里判断下是不是重定向请求
// 如果不是,就表示是第一次,需要确认授权
if (!confirm.getStatusCode().is3xxRedirection()) {
assertNotNull(confirm.getBody());
Matcher matcherConfirm = Pattern.compile("(?s).*name=\"_csrf\".*?value=\"([^\"]+).*").matcher(confirm.getBody());
assertTrue(matcherConfirm.find());
headers = new HttpHeaders();
headers.set("Cookie", cookie);
headers.setAccept(Collections.singletonList(MediaType.ALL));

// 5. 构建 同意授权 的表单
form = new LinkedMultiValueMap<>();
form.add("user_oauth_approval", "true");
form.add("scope.READ", "true");
form.add("_csrf", matcherConfirm.group(1));

// 6. 请求授权,获取 授权码
headers = authorizationServerInfo.postForHeaders("/oauth/authorize", form, headers);
}

URI location = headers.getLocation();
assertNotNull(location);
String query = location.getQuery();
assertNotNull(query);
String[] result = query.split("=");
assertEquals(2, result.length);
System.out.println(result[1]);
}

}

+ 94
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java View File

@@ -0,0 +1,94 @@
package com.xkcoding.oauth.oauth;

import org.springframework.http.*;
import org.springframework.http.client.ClientHttpRequest;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RequestCallback;
import org.springframework.web.client.ResponseErrorHandler;
import org.springframework.web.client.RestTemplate;

import java.io.IOException;
import java.net.HttpURLConnection;

/**
* 授权服务器工具类.
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午8:44
*/
@SuppressWarnings("all")
public class AuthorizationServerInfo {
public static final String HOST = "http://127.0.0.1:8080";

private RestTemplate client;

public AuthorizationServerInfo() {
client = new RestTemplate();
client.setRequestFactory(new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) throws IOException {
super.prepareConnection(connection, httpMethod);
connection.setInstanceFollowRedirects(false);
}
});
client.setErrorHandler(new ResponseErrorHandler() {
public boolean hasError(ClientHttpResponse response) {
return false;
}

public void handleError(ClientHttpResponse response) {
}
});
}

public ResponseEntity<String> getForString(String path, final HttpHeaders headers) {
return client.exchange(getUrl(path), HttpMethod.GET, new HttpEntity<>(null, headers), String.class);
}

public ResponseEntity<String> getForString(String path) {
return getForString(path, new HttpHeaders());
}

public ResponseEntity<Void> postForStatus(String path, HttpHeaders headers, MultiValueMap<String, String> formData) {
HttpHeaders actualHeaders = new HttpHeaders();
actualHeaders.putAll(headers);
actualHeaders.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
return client.exchange(getUrl(path), HttpMethod.POST,
new HttpEntity<>(formData, actualHeaders), (Class<Void>) null);
}


public static String getUrl(String path) {
return HOST + path;
}

public HttpHeaders postForHeaders(String path, MultiValueMap<String, String> formData, final HttpHeaders headers) {
RequestCallback requestCallback = new NullRequestCallback();
if (headers != null) {
requestCallback = request -> request.getHeaders().putAll(headers);
}
StringBuilder builder = new StringBuilder(getUrl(path));
if (!path.contains("?")) {
builder.append("?");
} else {
builder.append("&");
}
for (String key : formData.keySet()) {
for (String value : formData.get(key)) {
builder.append(key).append("=").append(value);
builder.append("&");
}
}
builder.deleteCharAt(builder.length() - 1);

return client.execute(builder.toString(), HttpMethod.POST, requestCallback,
HttpMessage::getHeaders);
}

private static final class NullRequestCallback implements RequestCallback {
public void doWithRequest(ClientHttpRequest request) {
}
}
}

+ 39
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java View File

@@ -0,0 +1,39 @@
package com.xkcoding.oauth.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 org.springframework.security.oauth2.common.OAuth2AccessToken;

import java.util.Arrays;

import static com.xkcoding.oauth.oauth.AuthorizationServerInfo.getUrl;
import static org.junit.jupiter.api.Assertions.*;

/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午9:14
*/
public class ResourceOwnerPasswordGrantTests {

@Test
void testConnectDirectlyToResourceServer() {
assertNotNull(accessToken());
}

public static String accessToken() {
ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails();
resource.setAccessTokenUri(getUrl("/oauth/token"));
resource.setClientId("oauth2");
resource.setClientSecret("oauth2");
resource.setId("oauth2");
resource.setScope(Arrays.asList("READ", "WRITE"));
resource.setUsername("admin");
resource.setPassword("123456");
OAuth2RestTemplate template = new OAuth2RestTemplate(resource);
OAuth2AccessToken accessToken = template.getAccessToken();
return accessToken.getValue();
}
}

+ 26
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java View File

@@ -0,0 +1,26 @@
package com.xkcoding.oauth.repostiory;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import static org.junit.jupiter.api.Assertions.assertNotNull;


/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:10
*/
@DataJpaTest
public class SysClientDetailsTest {
@Autowired
private SysClientDetailsRepository sysClientDetailsRepository;

@Test
public void autowiredSuccessWhenPassed() {
assertNotNull(sysClientDetailsRepository);
}

}

+ 40
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java View File

@@ -0,0 +1,40 @@
package com.xkcoding.oauth.repostiory;

import com.xkcoding.oauth.entity.SysUser;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;

import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;


/**
* .
*
* @author <a href="https://echocow.cn">EchoCow</a>
* @date 2020/1/6 下午1:25
*/
@DataJpaTest
public class SysUserRepositoryTest {

@Autowired
private SysUserRepository sysUserRepository;

@Test
public void autowiredSuccessWhenPassed() {
assertNotNull(sysUserRepository);
}

@Test
@DisplayName("测试关联查询")
public void queryUserAndRoleWhenPassed() {
Optional<SysUser> admin = sysUserRepository.findFirstByUsername("admin");
assertTrue(admin.isPresent());
SysUser sysUser = admin.orElseGet(SysUser::new);
assertNotNull(sysUser.getRoles());
assertEquals(1, sysUser.getRoles().size());
}
}

+ 21
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml View File

@@ -0,0 +1,21 @@
server:
port: 8080
servlet:
context-path: /demo

spring:
datasource:
url: jdbc:h2:mem:oauth2?options=DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE
username: root
password: 123456
jpa:
hibernate:
ddl-auto: create-drop
show-sql: true
properties:
hibernate:
format_sql: true

logging:
level:
org.springframework.security: debug

+ 10
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql View File

@@ -0,0 +1,10 @@
-- 测试数据
INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (1, 6000, null, null, 'oauth2', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'oauth2', 'READ,WRITE');
INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (2, 6000, null, null, 'test', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'test', 'READ');
INSERT INTO sys_client_details (id, access_token_validity_seconds, authorizations, auto_approve_scopes, client_id, client_secret, grant_types, redirect_url, refresh_token_validity_seconds, resource_ids, scopes) VALUES (3, 6000, null, null, 'test', '$2a$10$O8uM8kd5SbsuoITG3tBifOcarqqI8GP19vzbqDzVHP5ZV9yOfvpYS', 'authorization_code,password', 'http://example.com', 6000, 'error', 'READ');
INSERT INTO sys_role (id, name, description) VALUES (1, 'ROLE_ADMIN', '管理员');
INSERT INTO sys_role (id, name, description) VALUES (2, 'ROLE_TEST', '测试');
INSERT INTO sys_user (id, username, password) VALUES (1, 'admin', '$2a$10$xLH.pDNz3d2frOBQ6Gc.wuHY4ghwlSyFDgy0Ta.psXmm1YJjNaV1G');
INSERT INTO sys_user (id, username, password) VALUES (2, 'test', '$2a$10$xLH.pDNz3d2frOBQ6Gc.wuHY4ghwlSyFDgy0Ta.psXmm1YJjNaV1G');
INSERT INTO sys_user_role (user_id, role_id) VALUES (1, 1);
INSERT INTO sys_user_role (user_id, role_id) VALUES (2, 2);

+ 40
- 0
spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql View File

@@ -0,0 +1,40 @@
create table sys_client_details
(
id bigint auto_increment primary key,
access_token_validity_seconds int null,
authorizations varchar(255) null,
auto_approve_scopes varchar(255) null,
client_id varchar(255) null,
client_secret varchar(255) null,
grant_types varchar(255) null,
redirect_url varchar(255) null,
refresh_token_validity_seconds int null,
resource_ids varchar(255) null,
scopes varchar(255) null
);

create table sys_role
(
id bigint auto_increment primary key,
name varchar(55) not null,
description varchar(55) null
);

create table sys_user
(
id bigint auto_increment primary key,
username varchar(55) not null,
password varchar(128) not null
);

create table sys_user_role
(
id bigint auto_increment primary key,
user_id bigint not null,
role_id bigint not null,
constraint sys_user_role_sys_role_id_fk
foreign key (role_id) references sys_role (id),
constraint sys_user_role_sys_user_id_fk
foreign key (user_id) references sys_user (id)
);


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

@@ -1,4 +0,0 @@
server:
port: 8080
servlet:
context-path: /demo

+ 0
- 17
spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java View File

@@ -1,17 +0,0 @@
package com.xkcoding.oauth;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringBootDemoOauthApplicationTests {

@Test
public void contextLoads() {
}

}


Loading…
Cancel
Save