+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 从零到一完整实践(三)授权服务器 ]
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png
new file mode 100644
index 0000000..f9de1c6
Binary files /dev/null and b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png differ
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png
new file mode 100644
index 0000000..b418dfe
Binary files /dev/null and b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png differ
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png
new file mode 100644
index 0000000..b830990
Binary files /dev/null and b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png differ
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png
new file mode 100644
index 0000000..001dd8a
Binary files /dev/null and b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png differ
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
new file mode 100644
index 0000000..cfc942f
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml
@@ -0,0 +1,15 @@
+
+
+
+ spring-boot-demo-oauth
+ com.xkcoding
+ 1.0.0-SNAPSHOT
+
+ 4.0.0
+
+ spring-boot-demo-oauth-authorization-server
+
+
+
diff --git a/spring-boot-demo-oauth/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
similarity index 90%
rename from spring-boot-demo-oauth/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
rename to spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
index 382a8b1..ed73b61 100644
--- a/spring-boot-demo-oauth/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java
@@ -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;
/**
*
@@ -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 {
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
new file mode 100644
index 0000000..816ab07
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java
@@ -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 EchoCow
+ * @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"));
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
new file mode 100644
index 0000000..1737a63
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java
@@ -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 EchoCow
+ * @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"));
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
new file mode 100644
index 0000000..787c9f3
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java
@@ -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 EchoCow
+ * @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()");
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
new file mode 100644
index 0000000..39ac779
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java
@@ -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 EchoCow
+ * @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();
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
new file mode 100644
index 0000000..d6071cb
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java
@@ -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 EchoCow
+ * @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();
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
new file mode 100644
index 0000000..11cfadb
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java
@@ -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 EchoCow
+ * @date 2020/1/7 上午9:16
+ */
+package com.xkcoding.oauth.config;
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
new file mode 100644
index 0000000..8175467
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java
@@ -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 EchoCow
+ * @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 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;
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
new file mode 100644
index 0000000..5d7aa5d
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java
@@ -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 EchoCow
+ * @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;
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
new file mode 100644
index 0000000..453b76c
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java
@@ -0,0 +1,14 @@
+/**
+ * 控制器。除了业务逻辑的以外,提供两个控制器来帮助完成自定义:
+ * {@link com.xkcoding.oauth.controller.AuthorizationController}
+ * 自定义的授权控制器,重新设置到我们的界面中去,不使用他的默认实现
+ *
+ * {@link com.xkcoding.oauth.controller.Oauth2Controller}
+ * 页面跳转的控制器,这里拿出来是因为真的可以做很多事。比如登录的时候携带点什么
+ * 或者退出的时候携带什么标识,都可以。
+ *
+ * @author EchoCow
+ * @date 2020/1/7 上午11:25
+ * @see org.springframework.security.oauth2.provider.endpoint.AuthorizationEndpoint
+ */
+package com.xkcoding.oauth.controller;
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
new file mode 100644
index 0000000..535e366
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java
@@ -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 EchoCow
+ * @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 getScope() {
+ return stringToSet(scopes);
+ }
+
+ /**
+ * 授权类型
+ *
+ * @return 结果
+ */
+ @Override
+ public Set getAuthorizedGrantTypes() {
+ return stringToSet(grantTypes);
+ }
+
+ @Override
+ public Set getResourceIds() {
+ return stringToSet(resourceIds);
+ }
+
+
+ /**
+ * 获取回调地址
+ *
+ * @return redirectUrl
+ */
+ @Override
+ public Set getRegisteredRedirectUri() {
+ return stringToSet(redirectUrl);
+ }
+
+ /**
+ * 这里需要提一下
+ * 个人觉得这里应该是客户端所有的权限
+ * 但是已经有 scope 的存在可以很好的对客户端的权限进行认证了
+ * 那么在 oauth2 的四个角色中,这里就有可能是资源服务器的权限
+ * 但是一般资源服务器都有自己的权限管理机制,比如拿到用户信息后做 RBAC
+ * 所以在 spring security 的默认实现中直接给的是空的一个集合
+ * 这里我们也给他一个空的把
+ *
+ * @return GrantedAuthority
+ */
+ @Override
+ public Collection getAuthorities() {
+ return Collections.emptyList();
+ }
+
+ /**
+ * 判断是否自动授权
+ *
+ * @param scope scope
+ * @return 结果
+ */
+ @Override
+ public boolean isAutoApprove(String scope) {
+ if (autoApproveScopes == null || autoApproveScopes.isEmpty()) {
+ return false;
+ }
+ Set 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 getAdditionalInformation() {
+ return Collections.emptyMap();
+ }
+
+ private Set stringToSet(String s) {
+ return Arrays.stream(s.split(",")).collect(Collectors.toSet());
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java
new file mode 100644
index 0000000..e6e4f69
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java
@@ -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 EchoCow
+ * @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 users;
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java
new file mode 100644
index 0000000..84a9641
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java
@@ -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 EchoCow
+ * @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 roles;
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java
new file mode 100644
index 0000000..1184aca
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java
@@ -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 EchoCow
+ * @date 2020/1/6 下午1:09
+ */
+public interface SysClientDetailsRepository extends JpaRepository {
+
+ /**
+ * 通过 clientId 查找客户端信息.
+ *
+ * @param clientId clientId
+ * @return 结果
+ */
+ Optional findFirstByClientId(String clientId);
+
+ /**
+ * 根据客户端 id 删除客户端
+ *
+ * @param clientId 客户端id
+ */
+ @Modifying
+ void deleteByClientId(String clientId);
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java
new file mode 100644
index 0000000..a5aaff9
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java
@@ -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 EchoCow
+ * @date 2020/1/6 下午1:08
+ */
+public interface SysUserRepository extends JpaRepository {
+
+ /**
+ * 通过用户名查找用户.
+ *
+ * @param username 用户名
+ * @return 结果
+ */
+ Optional findFirstByUsername(String username);
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java
new file mode 100644
index 0000000..408414a
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java
@@ -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 EchoCow
+ * @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 findAll();
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java
new file mode 100644
index 0000000..6604a54
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java
@@ -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 EchoCow
+ * @date 2020/1/6 下午3:44
+ */
+public interface SysUserService extends UserDetailsService {
+ /**
+ * 查询所有用户
+ *
+ * @return 用户
+ */
+ List 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);
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java
new file mode 100644
index 0000000..00e3662
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java
@@ -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 EchoCow
+ * @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 findAll() {
+ return sysClientDetailsRepository.findAll();
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java
new file mode 100644
index 0000000..307af4d
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java
@@ -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 EchoCow
+ * @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 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 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);
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java
new file mode 100644
index 0000000..45f57f5
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java
@@ -0,0 +1,7 @@
+/**
+ * service 层,继承并实现 spring 接口.
+ *
+ * @author EchoCow
+ * @date 2020/1/7 上午9:16
+ */
+package com.xkcoding.oauth.service;
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
new file mode 100644
index 0000000..d68c1b2
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml
@@ -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
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks
new file mode 100644
index 0000000..af97322
Binary files /dev/null and b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks differ
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt
new file mode 100644
index 0000000..099f4e2
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt
@@ -0,0 +1,9 @@
+-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/
+xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ
+b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ
+pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI
+0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H
+5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA
+mwIDAQAB
+-----END PUBLIC KEY-----
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/authorization.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/authorization.html
new file mode 100644
index 0000000..db29716
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/authorization.html
@@ -0,0 +1,55 @@
+
+
+
+ 确认您的授权信息
+
+
+
+
+
+
+
+
+
+
+
+ 确认应用的授权信息
+
+
+
+
+
+
+ 当前应用将会获取您的以下权限:
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 确认授权
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html
new file mode 100644
index 0000000..7cc71de
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html
@@ -0,0 +1,33 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html
new file mode 100644
index 0000000..df4c1bc
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html
@@ -0,0 +1,45 @@
+
+
+
+ 发送了点小错误
+
+
+
+
+
+
+
+
+
+
+
+ 404 找不到页面
+ ~~~
+ 点击返回
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
new file mode 100644
index 0000000..5355e7e
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html
@@ -0,0 +1,110 @@
+
+
+
+ 欢迎登录
+
+
+
+
+
+
+
+
+
+
+
+ 欢迎登录
+
+
+ {{nameText}}
+
+
+
+ {{infoText}}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{previousText}}
+
+ 下一步
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html
new file mode 100644
index 0000000..1ea0a0c
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html
@@ -0,0 +1,44 @@
+
+
+
+ 确认退出吗?
+
+
+
+
+
+
+
+
+
+
+
+ 确认退出当前应用吗?
+
+
+
+
+
+
+
+
+
+ 确认退出
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html
new file mode 100644
index 0000000..3fae0b9
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html
@@ -0,0 +1,155 @@
+
+
+
+
+
+
+
+
+
+
+
+
云课程考试平台
+
+
亲爱的用户,你好!
+
+
+
+
+ 欢迎您注册 云课程考试平台
+
+
+ 你的邮件的验证码:
+ 验证码
(请输入该验证码完成 验证,验证码
+
+ 10 分钟内有效!)
+
如果您未申请云课程学习平台
+ $(type) 服务,请忽略该邮件。
+
+
+
+
+
+
+
+
+
+
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java
new file mode 100644
index 0000000..3dc8233
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java
@@ -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 EchoCow
+ * @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"));
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java
new file mode 100644
index 0000000..01e0d44
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java
@@ -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 EchoCow
+ * @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 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 form = new LinkedMultiValueMap<>();
+ form.add("username", "admin");
+ form.add("password", "123456");
+ form.add("_csrf", matcher.group(1));
+
+ // 3. 登录授权并获取登录成功的 cookie
+ ResponseEntity 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 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]);
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java
new file mode 100644
index 0000000..0c22919
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java
@@ -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 EchoCow
+ * @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 getForString(String path, final HttpHeaders headers) {
+ return client.exchange(getUrl(path), HttpMethod.GET, new HttpEntity<>(null, headers), String.class);
+ }
+
+ public ResponseEntity getForString(String path) {
+ return getForString(path, new HttpHeaders());
+ }
+
+ public ResponseEntity postForStatus(String path, HttpHeaders headers, MultiValueMap 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) null);
+ }
+
+
+ public static String getUrl(String path) {
+ return HOST + path;
+ }
+
+ public HttpHeaders postForHeaders(String path, MultiValueMap 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) {
+ }
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java
new file mode 100644
index 0000000..38d8d1d
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java
@@ -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 EchoCow
+ * @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();
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java
new file mode 100644
index 0000000..c0126bc
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java
@@ -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 EchoCow
+ * @date 2020/1/6 下午1:10
+ */
+@DataJpaTest
+public class SysClientDetailsTest {
+ @Autowired
+ private SysClientDetailsRepository sysClientDetailsRepository;
+
+ @Test
+ public void autowiredSuccessWhenPassed() {
+ assertNotNull(sysClientDetailsRepository);
+ }
+
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java
new file mode 100644
index 0000000..7df0679
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java
@@ -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 EchoCow
+ * @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 admin = sysUserRepository.findFirstByUsername("admin");
+ assertTrue(admin.isPresent());
+ SysUser sysUser = admin.orElseGet(SysUser::new);
+ assertNotNull(sysUser.getRoles());
+ assertEquals(1, sysUser.getRoles().size());
+ }
+}
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml
new file mode 100644
index 0000000..0324e25
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml
@@ -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
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql
new file mode 100644
index 0000000..4dee7e7
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql
@@ -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);
diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql
new file mode 100644
index 0000000..1bb2156
--- /dev/null
+++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql
@@ -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)
+);
+
diff --git a/spring-boot-demo-oauth/src/main/resources/application.yml b/spring-boot-demo-oauth/src/main/resources/application.yml
deleted file mode 100644
index a02fbde..0000000
--- a/spring-boot-demo-oauth/src/main/resources/application.yml
+++ /dev/null
@@ -1,4 +0,0 @@
-server:
- port: 8080
- servlet:
- context-path: /demo
\ No newline at end of file
diff --git a/spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java b/spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java
deleted file mode 100644
index 9b53df2..0000000
--- a/spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java
+++ /dev/null
@@ -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() {
- }
-
-}
-