diff --git a/pom.xml b/pom.xml index 64089db..8311573 100644 --- a/pom.xml +++ b/pom.xml @@ -86,6 +86,20 @@ 1.20 + + + aliyun + aliyun + https://maven.aliyun.com/repository/public + + true + + + false + + + + diff --git a/spring-boot-demo-oauth/pom.xml b/spring-boot-demo-oauth/pom.xml index 8ad7b24..724e86a 100644 --- a/spring-boot-demo-oauth/pom.xml +++ b/spring-boot-demo-oauth/pom.xml @@ -5,7 +5,10 @@ spring-boot-demo-oauth 1.0.0-SNAPSHOT - jar + + spring-boot-demo-oauth-authorization-server + + pom spring-boot-demo-oauth Demo project for Spring Boot @@ -28,10 +31,49 @@ spring-boot-starter-web + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${spring.boot.version} + + + + mysql + mysql-connector-java + runtime + + + + com.h2database + h2 + test + + org.springframework.boot spring-boot-starter-test test + + + junit + junit + + @@ -44,6 +86,14 @@ lombok true + + + org.junit.jupiter + junit-jupiter + 5.5.2 + test + + diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc new file mode 100644 index 0000000..1fee060 --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc @@ -0,0 +1,273 @@ += spring-boot-demo-oauth-authorization-server +Doc Writer +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 @@ + + + + 欢迎登录 + + +
+ + + + + + + + + 欢迎登录 + + + + +

{{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) 服务,请忽略该邮件。 +
+
+
+ +

如果仍有问题,请联系我们的管理员: 000-00000000 +

+
+
+
+ + 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() { - } - -} -