From dcfeb4f4dec08af75419aa9b8d517cc83a6c012b Mon Sep 17 00:00:00 2001 From: EchoCow Date: Tue, 7 Jan 2020 16:17:56 +0800 Subject: [PATCH 1/2] :sparkles: Add spring-boot-demo-oauth-authorization-server. --- pom.xml | 14 ++ spring-boot-demo-oauth/pom.xml | 52 +++- .../README.adoc | 273 +++++++++++++++++++++ .../image/Code.png | Bin 0 -> 29672 bytes .../image/Confirm.png | Bin 0 -> 22681 bytes .../image/Login.png | Bin 0 -> 22048 bytes .../image/Logout.png | Bin 0 -> 24467 bytes .../pom.xml | 15 ++ .../oauth/SpringBootDemoOauthApplication.java | 3 +- .../oauth/config/ClientLoginFailureHandler.java | 31 +++ .../oauth/config/ClientLogoutSuccessHandler.java | 30 +++ .../config/Oauth2AuthorizationServerConfig.java | 54 ++++ .../config/Oauth2AuthorizationTokenConfig.java | 74 ++++++ .../xkcoding/oauth/config/WebSecurityConfig.java | 54 ++++ .../com/xkcoding/oauth/config/package-info.java | 22 ++ .../oauth/controller/AuthorizationController.java | 43 ++++ .../oauth/controller/Oauth2Controller.java | 55 +++++ .../xkcoding/oauth/controller/package-info.java | 14 ++ .../xkcoding/oauth/entity/SysClientDetails.java | 191 ++++++++++++++ .../java/com/xkcoding/oauth/entity/SysRole.java | 49 ++++ .../java/com/xkcoding/oauth/entity/SysUser.java | 55 +++++ .../repostiory/SysClientDetailsRepository.java | 33 +++ .../oauth/repostiory/SysUserRepository.java | 24 ++ .../oauth/service/SysClientDetailsService.java | 67 +++++ .../com/xkcoding/oauth/service/SysUserService.java | 59 +++++ .../service/impl/SysClientDetailsServiceImpl.java | 73 ++++++ .../oauth/service/impl/SysUserServiceImpl.java | 76 ++++++ .../com/xkcoding/oauth/service/package-info.java | 7 + .../src/main/resources/application.yml | 22 ++ .../src/main/resources/oauth2.jks | Bin 0 -> 2559 bytes .../src/main/resources/public.txt | 9 + .../main/resources/templates/authorization.html | 55 +++++ .../main/resources/templates/common/common.html | 33 +++ .../src/main/resources/templates/error.html | 45 ++++ .../src/main/resources/templates/login.html | 110 +++++++++ .../src/main/resources/templates/logout.html | 44 ++++ .../main/resources/templates/registerTemplate.html | 155 ++++++++++++ .../com/xkcoding/oauth/PasswordEncodeTest.java | 22 ++ .../oauth/oauth/AuthorizationCodeGrantTests.java | 125 ++++++++++ .../oauth/oauth/AuthorizationServerInfo.java | 94 +++++++ .../oauth/ResourceOwnerPasswordGrantTests.java | 39 +++ .../oauth/repostiory/SysClientDetailsTest.java | 26 ++ .../oauth/repostiory/SysUserRepositoryTest.java | 40 +++ .../src/test/resources/application.yml | 21 ++ .../src/test/resources/import.sql | 10 + .../src/test/resources/schema.sql | 40 +++ .../src/main/resources/application.yml | 4 - .../oauth/SpringBootDemoOauthApplicationTests.java | 17 -- 48 files changed, 2256 insertions(+), 23 deletions(-) create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/README.adoc create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Code.png create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Confirm.png create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Login.png create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/image/Logout.png create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/pom.xml rename spring-boot-demo-oauth/{ => spring-boot-demo-oauth-authorization-server}/src/main/java/com/xkcoding/oauth/SpringBootDemoOauthApplication.java (90%) create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLoginFailureHandler.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/ClientLogoutSuccessHandler.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationServerConfig.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/Oauth2AuthorizationTokenConfig.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/WebSecurityConfig.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/config/package-info.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/AuthorizationController.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/Oauth2Controller.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/controller/package-info.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysClientDetails.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysRole.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/entity/SysUser.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysClientDetailsRepository.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/repostiory/SysUserRepository.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysClientDetailsService.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/SysUserService.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysClientDetailsServiceImpl.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/impl/SysUserServiceImpl.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/java/com/xkcoding/oauth/service/package-info.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/application.yml create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/oauth2.jks create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/public.txt create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/authorization.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/common/common.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/error.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/login.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/logout.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/main/resources/templates/registerTemplate.html create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/PasswordEncodeTest.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationCodeGrantTests.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/AuthorizationServerInfo.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/oauth/ResourceOwnerPasswordGrantTests.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysClientDetailsTest.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/java/com/xkcoding/oauth/repostiory/SysUserRepositoryTest.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/application.yml create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/import.sql create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-authorization-server/src/test/resources/schema.sql delete mode 100644 spring-boot-demo-oauth/src/main/resources/application.yml delete mode 100644 spring-boot-demo-oauth/src/test/java/com/xkcoding/oauth/SpringBootDemoOauthApplicationTests.java 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 @@ -30,8 +33,47 @@ 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 0000000000000000000000000000000000000000..f9de1c61140fa508fd5257bb4b0740ac945e7bcd GIT binary patch literal 29672 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 zV4u1wzCkD0k+fd|0Dz1``vL#}0Az}!ePq=yRn?%LfdlWcYSpR*&#zy<-nw+@96ap1 zdGqGhwQILz_p6Z73%=AlwN0Bg8TAx=q2N^2r1|pY3%fjf_Utxj(7nl)SDS#bWB;ul zEnd91O`iOPU3Ae!$(GeRU9ez*n@~)FtSIVu$^rlYz;_B`L;J|)$&<&14<8ZM|5TR^ z9Xi|{hVS|8v(IeEkY|JEpMU-Zd-BPrtVD?tVfU}lt68&VHgDd1d*_|e$tDRqoOJBi z$p#O8JlP@q1q&8*;of)OEeuu$twqG6YB+O#RGRX~|CWo*=_cWuIi ziPoS&gRpfPO4X|h!&TLp2wLx|@~u;+j@@+A&2GZ0Uw>WZs-R=pCThPhe*7mcY^_wO zQnKr{zbFLNG9yR6e%Ng1BVoS;000>)+qgN~4i?O9Rv_^AK*2nLpLq@jPJurdQvN#) z0000PAkw5i-sSq4G>2kz}p0WS{0Lb!G`Ke0El&UGS3O$F) zl`Feby!7FyeEIV36pJE7idemR_2Pb<%vP~tMR&?7j)eUZ003kR+7|!-000000DzAO zWdQ&H00000z(<6#000000002sBSKjK00000008h26MX>y002J9l`EGOEm|~qK7aoF znK&8%003kLLRkO+0KiA*o_nrs+O#>dhXDWpfJ{XD0ssI200000@DZUb000000002^ zh)@;)00000004YMC<_1p0000006rp=1poj5000009}&s|00000004lG2xS2P00000 z0Ki9tvH$=800000;3Gm=000000002+5uq#q00000008)iP!<3H000000DMF!3jhEB z00000J|dI_000000000V5y}Dp000000DzAOWdQ&H00000z(<6#000000002sBSKjK z00000008h2p)3FZ000000QiVd761SM00000d_*V<000000000!B9sLH000000018m z$^rlY00000fR6}e0RR910002MM})Ef00000007`4LRkO+000000Pqo^EC2uiVzPPj zW?Q{_b*iWR`}f-~zx-k+om9*I_~Va=_qA`|KKt^^FKySZT?rfCsi&T7)22;J*!`u) zZ@>L!Km71Ry6)raufMkS>({6G_V?X)uT`vA-YQo?rc9X< zJU?vMQ0vs`qI5k*X2^)w(vc+Tc21;>TKBKK^0Hle=_Luh&;J^28~vZOW5 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 z_U=3A>gnA%D^ko;woFm?_np~a+ku0J-7TE9iv~3t_tHXJ^2s_oeE5j1{C2ZFJz+th z>`fxJT8>7c`>?ww*}>n}vt{dUH(1%Mc`JkS+&y}ljTtvT;nxcQ0C1wBr7Qpd05YUf zg%WAbqf7B`yYW05`@%=|kFj$tPoCUvP_5f;I4{lTHmFm{Mvu7Eo_c=1ji0#Kg}(|F zZy(y;&TCyQ880Cs^)#TtydM6?EbBYuMK`TXsUmjo@QzlmR)tJir&_RT(5(p*m)Q7M z7FnT_@>};y>!s*Ypj_FaHn@KaduGA{cf0#f7g&cj)l)4?^C+;rW7JeTbm*{aA=|rK zBkP;s0A4FrZFK$msxRE)`SRGd9ednhV5`5|;?}FMdihsBWqL>s003}2$u;HUT}O-L zH!EDwOkGSBE^yQ|U~c|ASzR;$0Du#R2F!{zU4KiINIU$@3DnZEMDdecpR|eExqH9u z-+#zTom?oP)}9%q*1dc89!NC^S!U5|+_7`7l`UP=B6LIR96oZ?js|Xd^TxjTO|4|N zZQo<1OBIQ_tXR}dg8KCh?Js!uZ~LuC;R3Xi@cH1_2ZU&U z!$q5p#L!X}0002KDpR^hx}RH2tq6(~NPEU4Q|sJV-7_Rxfxx;>`MsNhXw*f4#Su)a zRdPn^T;X^40B)gytoT_s0002T1AfW|000000000V5y}Dp000000DzAOWdQ&H00000 zz(<6#000000002sBSKjK00000008h2p)3FZ000000QiVd761SM00000d_*V<00000 z0000!B9sLH000000018m$^rlY00000fR6}e0RR910002MM})Ef00000007`4LRkO+ z000000Pqo^EC2ui00000_=r#z000000001dL?{aY00000002HBlm!3)0000003Q*` z0ssI20001hj|gP}00000006*8gt7nt000000N^7+SpWb400000@DZUb000000002^ zh)@;)00000004YMC<_1p0000006rp=1poj5000009}&s|00000004lG2xS2P00000 z0Ki9tvH$=800000;3Gm=000000002+5uq#q00000008)iP!<3H000000DMF!3jhEB z00000J|dI_000000000V5y}Dp000000DzAOWdQ&H00000z(<6#000000002sBSKjK z00000008h2p)3FZ000000QiVd761SM00000d_*V<000000000!B9sLH000000018m z$^rlY4fHJw z004mWQeU$B_U%j8a|vschfo#(03e6??YG_O{vZti0Du#hTFCC&wafkQ1*`Y2j|e|@nISO007a1e^!k5 zvm>7|lZ#Lm001C|h!w_03TFWT002)~VJxAmzX)Xk006R)v|7uCKOX5m0000GOQL}+ zpD~kguh|cwEC2w&afT1w0000$_Le)V z3jhEB0000004EYcSpWb400000@DZUb000000002^h)@;)00000004YMC<_1p00000 z06rp=1poj5000009}&s|00000004lG2xS2P000000Ki9tvH$=800000;3Gm=00000 z0002+5uq#q00000008)iP!<3H000000DMF!3jhEB00000J|dI_000000000V5y}Dp z000000DzAOWdQ&H00000z(<6#000000002sBSKjK00000008h2p)3FZ000000QiVd z761SM00000d_*V<000000000!B9sLH000000018m$^rlY00000fR6}e0RR910002M zM})Ef00000007`4LRkO+000000Pqo^EC2ui00000_=r#z000000001dL?{aY00000 z002HBlm!3)0000003Q*`0ssI20001hj|gP}00000006*8gt7nt000000N^7+SpWb4 q00000@DZUb000000002^=>GxQ^RV9HvUD8)0000u4`X=@3q%@*0Y|qTy?+C=kvKI88vFun+*T}003iU z(vdy^0001h37K#O0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZ za0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@ z005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001> zf^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A( z0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5 zpn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_! z1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW z0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZ za0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@ z005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001> zf^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A( z0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5 zpn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_! z1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW z0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZ za0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@ z005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001> zf^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A( z0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5 zpn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_! z1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW z0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZ za0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@ z005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001> zf^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A( z0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5 zpn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_! z1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW z0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZ za0LJW0HA_!1poj5pn`A(0001>f^Y=@005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@ z005wZa0LJW0HA_!1poj5pn`A(0001>f^Y=@005xE7>o%lTY4EKmGL0)KgDA(x(GVV1z3Gm`Nxat zrkk#rWRgkBKWYe9fZ;H61zZ`9d(@ZnXNS_7({7*=zlK<@m7!KhIzH{UG( ziUt6@^)h=U|Jw^N9Ksa<004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j z0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr z!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it z004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h z3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD z0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJL zfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@ z6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?it004jr!W94j z0Dua@6#xJLfC|DD0001h3c?it004jr!W94j0Dua@6#xJLfC|DD0001h3c?itV7&8x z|M!2*GtWFT(x(9cFeJhi0GP-ns2DYB)UY0N=bd*pk3II-TXaSP`|p4M+kEbGpKG?- zYOChyr=K2?a~O`7Uw(Pe`;1Lcl=tlR+i!2~zyJQRIjteP?z-!mAO7%%<+_iA|NQ4a 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 zxe>2>+Gu>@g{$J?XOG42UpW=O@UJ`L@$2ew&C6EDJI)@j;z@D;bpL8a{jo(AZC-Y2 zGcJAc>bUcPZoK?6lQGqb_~!Oby!_LXanTuL@rvD-kOp8O2^s(ZlFF5O=Dg+Vsp|Zd zD>nD*NLg(1g*#fYacw;g9jnE=uAAQLwOc2$bGxS3Wga1(iWJ-P4KZe`Hs+wF!X5)n;a9{4EKvAG_#~gFaN)H1#fY=rU008@egC(p03?p#`0000WL0ka<002l3R{#J2020I% z005ZhKsk7Ie`WrFVI!^p0000ah${d9FrciuD7V!Om+#pZW!~oI5MWq{D*ylxu~!f% zA2+U=UEvWOn995j&ZEcz3dCwtc^4A#OCJMwryKXPftgu;~bXx%6yHDjm6=IA09^?byPKP zW&Y;kiogO4191fafT33Y@!#!s_xilVfmN$kRi8H+jo7eZL$q40hn_e1{9!eCu1cAs z(b3Ur&dROK-~6Z|vH%A&aRmSXcAMK4RFu^iWqZrS#6+~)?JB51poj5AVFLK0000;5LW;I000ui z6#xJLfCO;`0000WL0ka<002l3R{#J2020I%0001h1aSob001CCTmb+807wv500000 z62uh%004jlaRmSX03bnJ0RR91NDx;50000I#1#Ml0DuH>1poj5AVFLK0000;5LW;I z000ui6#xJLfCO;`0000WL0kdY&(&(R=yWjY=ytm?F)2ROip zD*ylh014KX0{{R3B#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5A zK!Uge0001xAg%xa001P2D*ylh014s>0000$g17 z002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T% z3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9 z000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2kRYxA0000a zh${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2kRYxA z0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2 zkRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc z0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$ zg17002OOxB>tG0FWTA00000B#0{j00009;tBu& z06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s> z0000$g17002OOxB>tG0FWTA00000B#0{j00009 z;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh z014s>0000$g17002OOxB>tG0FWTA00000B#0{j z00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2 zD*ylh014s>0000$g17002OOxB>tG0FWTA00000 zB#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa z001P2D*ylh014s>0000$g17002OOxB>tG0FWTA z00000B#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK*Ik4Q>~Jy TLexz4o3k;k@~;{~9oE+_(t^ z0001hsWMAn4*&oF0H8k;t^fc405l+60RR91Xh66E0002cfN%u>005u?;R*l%06+u6 z6#xJLfChvs000004G32N0000Q5Uv0K001-~Tmb+80BAtC0ssI2(136S0001>0pSV& z002M(!W94j0DuOBD*ylh01XIN000008W64k0000qAY1_e003w}xB>tG0MLMN1poj5 zpaJ0u0000$1Hu&m004jngew35000dLR{#J202&ak00000G$3370001JK)3<`007W{ za0LJW0H6Wk3IG5AKm)=R0001h281gB0000D2v-0A000^gt^fc405l+60RR91Xh66E z0002cfN%u>005u?;R*l%06+u66#xJLfChvs000004G32N0000Q5Uv0K001-~Tmb+8 z0BAtC0ssI2(136S0001>0pSV&002M(!W94j0DuOBD*ylh01XIN000008W64k0000q zAY1_e003w}xB>tG0MLMN1poj5paJ0u0000$1Hu&m004jngew35000dLR{#J202&ak z00000G$3370001JK)3<`007W{a0LJW0H6Wk3IG5AKm)=R0001h281gB0000D2v-0A z000^gt^fc405l+60RR91Xh66E0002cfN%u>005u?;R*l%06+u66#xJLfChvs00000 z4G32N0000Q5Uv0K001-~Tmb+80BAtC0ssI2(136S0001>0pSV&002M(!W94j0DuOB zD*ylh01XIN000008W64k0000qAY1_e003w}xB>tG0MLMN1poj5paJ0u0000$1Hu&m z004jngew35000dLR{#J202&ak00000G$3370001JK)3<`007W{a0LJW0H6Wk3IG5A zKm)=R0001h281gB0000D2v-0A000^gt^fc405l+60RR91Xh66E0002cfN%u>005u? z;R*l%06+u66#xJLfChvs000004G32N0000Q5Uv0K001-~Tmb+80BAtC0ssI2(136S z0001>0pSV&002M(!W94j0DuOBD*ylh01XIN000008W64k0000qAY1_e003w}xB>tG z0MLMN1poj5paJ0u0000$1Hu&m004jngew35000dLR{#J202&ak00000G$3370001J zK)3<`007W{a0LJW0H6Wk3IG5AKm)=R0001h281gB0000D2v-0A000^gt^fc405l+6 z0RR91Xh66E0002cfN%u>005u?;R*l%06+u66#xJLfChvs000004G32N0000Q5Uv0K z001-~Tmb+80BAtC0ssI2(136S0001>0pSV&002M(!W94j0DuOBD*ylh01XIN00000 z8W64k0000qAY1_e003w}xB>tG0MLMN1poj5paJ0u0000$1Hu&m004jngew35000dL zR{#J202&ak00000G$3370001JK)3<`007W{a0LJW0H6Wk3IG5AKm)=R0001h281gB z0000D2v-0A000^gt^fc405l+60RR91Xh66E0002cfN%u>005u?;R*l%06+u66#xJL zfChvs000004G32N0000Q5Uv0K001-~Tmb+80BAtC0ssI2(136S0001>fq{KJ004UC zt+(DP&pr2CdE<>Y%A0S#+1JAXfSJy$v(8#(n{Bo--+c3x0Rsl~^>BcGWWcy_<0ka= zAOM)Wgegxy{dAdUo_WgLbI)D>=MMmww(|Gwi!Z)dUV7=Jvd}^cm04y{=nwRaa0Q?r zNX5#aL4#_z0su^3FTC(V`Pb*9GGxe*z8(tDPl#dx`holp-r&K5`+67v@Q#qbb6005u?;R*l%06+u66#xJLfChvs000004G32N0000Q z5Uv0K001-~Tmb+80BAtC0ssI2(136S0001>0pSV&002M(!W94j0DuOBD*ylh01XIN z000008W64k0000qAY1_e003w}xB>tG0MLMN1poj5paJ0u0000$1Hu&m004jngew35 z000dLR{#J202&ak00000G$3370001JK)3<`007W{a0LJW0H6Wk3IG5AKm)=R0001h z281gB0000D2v-0A000^gt^fc405l+60RR91Xh66E0002cfN%u>005u?;R*l%06+u6 z6#xJLfChvs000004G32N0000Q5Uv0K001-~Tmb+80BAtC0ssI2(136S0001>0pSV& z002M(!W94j0DuOBD*ylh01XIN000008W64k0000qAY1_e003w}xB>tG0MLMN1poj5 zpaJ0u0000$1Hu&m004jngew35|MJ#bZtoB+Vo8Q9kY005Yl z_T6{iGRGWql%tM1s>kn{ZMNCUl1naGuDkBK@})0*sT_a&@nyh(0iE`_<(6B@p@$w? z?!No(^8EA9mxB*JxYuL*=tndv2M3{`n_){DeNMtg=eI{@G`ry~pDkI&^3`=bUqTeLVoc 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 z!W94j0DuOBD*ylh01XIN000008W64k0000qAY1_e003w}xB|coVc@`lW&HT@eLV~S zrr`D0UoV3O4eIM*0L)N?D*((8h71`}#*7)$*TVo{3Z8uO$ufBG;JzLPzzjvW0>BJm z$t9O8k3ar+dE}8t>c5HtFk{K@=KPM%@9O-{2H^h;?CSvlK+G}69A&lDRx1xY@Id`n z(brykt*?g!K$}bjO2tZkSLb&&0RKn00>BI-|Fb@F005u?;R*l%06+u66#xJLfCdKk^#A|> zfd4gm^yqTSEw^;qw(YjtmH`6>bi4lp4?Ixbc;k(-+;YqH_&B{i2LMy1as>bYW)OGZ zeRp~A!3WEnbI#fEn4AkPxS-56*Id0iPH)cvz?7+60RVs*#PiQTUshgu;f2?smU~ zBYAzTwbttOae8_V0D4a43IG7iAokj8ua586Uw{2_$RUT6v17-Up+kpu+V7rw?kU5E z4=;lU5AOAGdU_53dQRmE007JY-g@h;a{vAJcRWvp$&evKYG0MEG9@U}d3t-C-kbw~ zo)fMB0Kg1jmRV*gC!BCXU0yjJd+f0~eP@wH7U{I#E3dp#9(m-Evi$PP_xdxwfQr1{wjWS@sfT=o8PtE~A?+h3> zZrp^v9s~fBH)hP3GGfGtz8(euZ-X?e%`~04=bpRU0OgD`&L|siym47zfd!`eI8!;R?_jdaeKf001-~Tmb+8 z0BAtC0ssI2(136S0001>0pSV&002M(!W94j0DuOBD*ylh|F69}0g|k)4>A80H zVv%bX5WF}Q5LtV8DTs0-;4XX3&e1*T{~LO@cXsx`-|WoJ%;!_pOm9yguU{3f=Y9MC{tqBQTmb+8 z07wv50000062uh%004jlaRmSX03bnJ0RR91NDx;50000I#1#Ml0DuH>1poj5AVFLK z0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3R{#J2020I%0001h1aSob001CC zTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91NDx;50000I#1#Ml0DuH>1poj5 zAVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3R{#J2020I%0001h1aSob z001CCTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91NDx;50000I#1#Ml0DuH> z1poj5AVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3R{#J2020I%0001h z1aSob001CCTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91NDx;50000I#1#Ml z0DuH>1poj5AVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3R{#J2020I% z0001h1aSob001CCTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91NDx;50000I z#1#Ml0DuH>1poj5AVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3R{#J2 z020I%0001h1aSob001CCTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91NDx;5 z0000I#1#Ml0DuH>1poj5AVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka<002l3 zR{#J2020I%0001h1aSob001CCTmb+807wv50000062uh%004jlaRmSX03bnJ0RR91 zNDx;50000I#1#Ml0DuH>1poj5AVFLK0000;5LW;I000ui6#xJLfCO;`0000WL0ka< z002l3R{#J2020I%0001h1aSob001CCTmb+807wv50000062uh%004jlaRmSX03bnJ z0RR91Na&vF1pokmxom7~Y^E1sTGeW`nO+2dX(z4#0KhKLjw%y>&Q%kmipT=YC*leK z08F9Im@?V*&il-EodKm>FPHM(Cf*J(*NH0t05C;5V@k8x+!R&X*E{bsSG40v`PtRg zwJEZ+uTP9E0CSPJ0>HGnd+k_!@W(@O<3o+OXYJhDxtMDEFRsNAd)4FJ&*_T;7TYO- z*`ys)8jaSrZe7ILS2g3NhnsVLyH~bbhd*vC-to+?*niRZZSBAUFxQDI08EP#R$ltm z^>Nn9KEYrol=XGq%_G%1yXdrq))&kIougDrSh;IWGfw$dBi0Vj^pZ`F5|8d#+lVXg zZN&MnsK%HDYWQN7i=17?TL zxN`PYWAi4gbSm$;y!WyWIuC~en9IZ!0H(#2_m0M?hxW|$V$3?H9@Y~#J=~b-#Q=7o zbHdsZ6h=ly;_CaGGrUwY=ayAt)jBBaqL?_LFTh+Qt^hDC$~T!y`eu45W}h;GWvo|L?G$`_FkbY;?s(U;$M+1*ykuiE$0BOAE$>^N@53i<-h0?M z8gc*nE$0TPw;uD!Ylf=FpLFn+ADg}Ph!u;*pY-mvV{y%Wji^8RwMCg?V4sDxxbogc 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 znO+P4m>0@tZu#s5*jeHVz%0<)+Z*fGub=6~0DyU6-MV$vXD`6c5?26bfu&2A#v_kB z6029Qj%Kqt(~AQDb5a?xQa)?fu3Z~TmMod+#Q|oK+VJr3*i0`306SKq%BoeXVsv!W zAwmGK3)JiN=;`T+Wy_XDtyY`q#Q|m!aRmSX03g9(K>z>%014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc0ssI2 zkRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$g17002OOxB>tG0FWTA00000B#0{j00009;tBu&06>Dc z0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s>0000$ zg17002OOxB>tG0FWTA00000B#0{j00009;tBu& z06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh014s> z0000$g17002OOxB>tG0FWTA00000B#0{j00009 z;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2D*ylh z014s>0000$g17002OOxB>tG0FWTA00000B#0{j z00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa001P2 zD*ylh014s>0000$g17002OOxB>tG0FWTA00000 zB#0{j00009;tBu&06>Dc0ssI2kRYxA0000ah${d9000T%3IG5AK!Uge0001xAg%xa z001P2D*ylh014s>0000$g17X>P)c3jixz$G2=H-N*{M@hb?F>ks;Z(_k8Z`NdSK;~N?UdB z-ZkEzGcOiYSI2?{3rl~l7v*ji0002c4wee**BLS}S33ZJGIZj3}&001Df&MhwjZC6uM6V(fAqGnM|ELyZU7B60$yGZ~5 z006);sHmujPMs>EvQuSLb*_xc%F5hLS}6$Y=@1L57smVr3xx9k00026l1Z6gO-;Hj ziUrlxMR?z}OPA1-K3y??!5WSDX0%YJpcdz05p^`vrNSFATBp@dH?_b0BBt1>}<}wdBwn9TF3PWEVD(678M^rpgcYH+#`1rpxw;5 zZFKG?0002+F_Z@OR`%!=ojMsWUM^w%O72e=T@n?gf8RLzs1DnPpdJ7K0034#bLPw` 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 zmm*aC^TmITt+(Dfw)^yUtzMU)lNFtF7ytlZ<&*~6cJI-x$$_*f%%3}FUJT!Uc-iah z(XCr_?%ei)gC8f8CQXWGpMAE-h8mOd=QN^zgAF!l)A~C!tY`W9_3zG$C!Tz|tYt^+ zv_p#%;g+klqw8Tsl&<`^`l_poOP}V>ofF$^vu%7~?=O@+>bpKcCo4MVFaQ7mIN|!RFiH8OC z00000%}eEP8qUi`^CV8Ri>l6@qoP|ytLxXdS?w&(u!5^!P6Pk|0H9rD&dz#t?-rda zSB^UM%ag7R+D)1$BGsVk)P@?~t(G>icyWu10RR91z$Z$|%rXV_DHlxnprF2OjI5_! z6_ZBv>=6s97smVr3rgP&9T=!Dk4^yq00013kz~lsDoWez+NDc$sS-CY3rCL*kv^E| zR#Q_G)eCE)W>HNnTC})h!+Tgk%(-oJ?j`^L006XE3g7biOM4L3 z)1lG_w$f!U000000L{|n0RR91005Z@>j3}&001B}VLbo<0001Fwp7jfNiV#Ts~rFU z0002+iM3Q%zs`_Bx!M5$00000pIFM^0000006=EKdH?_b0078LSPuXI0001)3F`p> z0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l002N{!g>Gz z0002UOjr*900000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005AgupR&a z0000o6V?L&0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz003krtOo!9 z004l@g!KRb0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF005Z@>j3}& z001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b z0078LSPuXI0001)3F`p>0000WGhsadJpfX1001BWNklGz0002UOjr*900000nF;Fw00000ATwb- z000000Awbt2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L&0000$X2N;^00000$V^xd z000000GSEv0RR9103b7AJpcdz003krtOo!9004l@g!KRb0001xnXn!J00000G85JV z00000KxV>v000000LV;O4*&oF005Z@>j3}&001B}VLbo<0001FCaebl0001h%!Ks- z00000keRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI0001)3F`p>0000WGhsad z00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l002N{!g>Gz0002UOjr*9 z00000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L& z0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz003krtOo!9004l@g!KRb z0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF005Z@>j3}&001B}VLbo< z0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI z0001)3F`p>0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l z002N{!g>Gz0002UOjr*900000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|> z005AgupR&a0000o6V?L&0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz z003krtOo!9004l@g!KRb0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF z005Z@>j3}&001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R3 z06=EKdH?_b0078LSPuXI0001)3F`p>0000WGhsad00000WG1W!00000fXsyT00000 z0Farm9smFU001%*)&l?l002N{!g>Gz0002UOjr*900000nF;Fw00000ATwb-00000 z0Awbt2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L&0000$X2N;^00000$V^xd00000 z0GSEv0RR9103b7AJpcdz003krtOo!9004l@g!KRb0001xnXn!J00000G85JV00000 zKxV>v000000LV;O4*&oF005Z@>j3}&001B}VLbo<0001FCaebl0001h%!Ks-00000 zkeRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI0001)3F`p>0000WGhsad00000 zWG1W!00000fXsyT000000Farm9smFU001%*)&l?l002N{!g>Gz0002UOjr*900000 znF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L&0000$ zX2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz003krtOo!9004l@g!KRb0001x znXn!J00000G85JV00000KxV>v000000LV;O4*&oF005Z@>j3}&001B}VLbo<0001F zCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI0001) z3F`p>0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l002N{ z!g>Gz0002UOjr*900000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005Ag zupR&a0000o6V?L&0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz003kr ztOo!9004l@g!KRb0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF005Z@ z>j3}&001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EK zdH?_b0078LSPuXI0001)3F`p>0000WGhsad00000WG1W!00000fXsyT000000Farm z9smFU001%*)&l?l002N{!g>Gz0002UOjr*900000nF;Fw00000ATwb-000000Awbt z2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L&0000$X2N;^00000$V^xd000000GSEv z0RR9103b7AJpcdz003krtOo!9004l@g!KRb0001xnXn!J00000G85JV00000KxV>v z000000LV;O4*&oF005Z@>j3}&001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^ z0000005TKS0{{R306=EKdH?_b0078LSPuXI0001)3F`p>0000WGhsad00000WG1W! z00000fXsyT000000Farm9smFU001%*)&l?l002N{!g>Gz0002UOjr*900000nF;Fw z00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005AgupR&a0000o6V?L&0000$X2N;^ z00000$V^xd000000GSEv0RR9103b7AJpcdz003krtOo!9004l@g!KRb0001xnXn!J z00000G85JV00000KxV>v000000LV;O4*&oF005Z@>j3}&001B}VLbo<0001FCaebl z0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI0001)3F`p> z0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l002N{!g>Gz z0002UOjr*900000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|>005AgupR&a z0000o6V?L&0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz003krtOo!9 z004l@g!KRb0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF005Z@>j3}& z001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b z0078LSPuXI0001)3F`p>0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU z001%*)&l?l002N{!g>Gz0002UtaI)L006+p&AfT@V)pFWv2fwSShQ$S?&bi%N~cq& zPSLq@=jhd|S9I&vEq8MOJ{E-a002-=3hHOfm=S&Z_ASDL%F4>zO#*v000000LV;O4*&oF005Z@>j3}&001B}VLbo< z0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R306=EKdH?_b0078LSPuXI z0001)3F`p>0000WGhsad00000WG1W!00000fXsyT000000Farm9smFU001%*)&l?l z002N{!g>Gz0002UOjr*900000nF;Fw00000ATwb-000000Awbt2LJ#70D#Pd^#A|> z005AgupR&a0000o6V?L&0000$X2N;^00000$V^xd000000GSEv0RR9103b7AJpcdz z003krtOo!9004l@g!KRb0001xnXn!J00000G85JV00000KxV>v000000LV;O4*&oF z005Z@>j3}&001B}VLbo<0001FCaebl0001h%!Ks-00000keRR^0000005TKS0{{R3 z06=EKdH?_b0078LSPuXI0001)3F`p>0000WGhsad00000WG1W!00000fXsyT00000 z0Farm9smFU001%*)&l?l002N{!g>Gz0002UOjr*900000nF;Fw00000ATwb-00000 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 z2<>BwW2g-dK=&uC2LJ#7N>c>(#chPoK~Uei$`(RJcrU02=>CND0002MN>!~DPVrb# zX)mmAU1cy|EusM%wfl68V+PQ@3F`p>0D#g|3+?F~Aw59P zAgl)f002rSa0Z~at1Wu~000000MOeB>j3}&005w5!g>Gz0000enXn!J00000N+zra z00000fRYL80RR910H9>TdH?_b001bNupR&a0000=Caebl0001hk_qbp00000pk%^& z0000004SNT9smFU002rRtOo!9004lJ3F`p>0001>WWss?00000D4DPx0000007@pT z2LJ#70DzJS>j3}&005w5!g>Gz0000enXn!J00000N+zra00000fRYL80RR910H9>T zdH?_b001bNupR&a0000=Caebl0001hk_qbp00000pk%^&0000004SNT9smFU002rR ztOo!9004lJ3F`p>0001>WWss?00000D4DPx0000007@pT2LJ#70DzJS>j40uLQPFg zTC`|U*~0*UULpR?jemOq5Y__#z*6e#>(ku1bITqE0Q3s+Z*KhC3qVSBWe)%V0REY* zvdSvyt+(Dvjg5_Iz<>d%wpM)$04#3{7cR^}{o8N9omO9c^|FTp@J~Tl4*&oa`uFdj zZ{K_Gy&T$Wi30%3UrdJA)z#&ozWBEnptlp&0{}oX5gG^%004lM$u+$I00000P%>dX z000000F+Ev4*&oF001Qu)&l?l002PAg!KRb0000`GGRRc00000luTF;0000003{RF z0{{R306@uv^#A|>002-jVLbo<0001#Ojr*900000B@@;I00000K*@yl0000008lbv zJpcdz005LsSPuXI0000b6V?L&0000$$%OR)00000P%>dX000000F+Ev4*&oF001Qu z)&l?l002PAg!KRb0000`GGRRc00000luTF;0000003{RF0{{R306@uv^#A|>002-j zVLbo<0001#Ojr*900000B@@;I00000K*@yl0000008lbvJpcdz005LsSPuXI0000b z6V?L&0000$$%OR)00000P%>dX000000F+Ev4*&oF001Qu)&l?l002PAg!KRb0000` zGGRRc00000luTF;0000003{RF0{{R306@uv^#A|>002-jVLbo<0001#Ojr*900000 zB@@;I00000K*@yl0000008lbvJpcdz005LsSPuXI0000b6V?L&0000$$%OR)00000 zP%>dX000000F+Ev4*&oF001Qu)&l?l002PAg!KRb0000`GGRRc00000luTF;00000 z03{RF0{{R306@uv^#A|>002-jVLbo<0001#Ojr*900000B@@;I00000K*@yl00000 l08lbvJpcdz005Ni{{uCCpTWRrzq0@U002ovPDHLkV1f*!X1)Lb literal 0 KcmV+b0RR6000031 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 0000000000000000000000000000000000000000..af97322428bc077838fc24774dafc3a40db84e1f GIT binary patch literal 2559 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 V*dd;)7$5KV + + + 确认您的授权信息 + + +

    + + + + + + + + + 确认应用的授权信息 + +
    + +
    + + + 当前应用将会获取您的以下权限: + + + + + + + + + + + + + + 确认授权 + +
    +
    +
    +
    +
    +
    +
    + +
    + + + 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() { - } - -} - From a989d01e45887c5debf1f797bc6fed56fed939e6 Mon Sep 17 00:00:00 2001 From: EchoCow Date: Thu, 9 Jan 2020 17:07:14 +0800 Subject: [PATCH 2/2] :sparkles: Add spring-boot-demo-oauth-resource-server. --- spring-boot-demo-oauth/pom.xml | 26 +----- .../pom.xml | 29 ++++++ .../src/main/resources/application.yml | 2 +- .../src/main/resources/templates/login.html | 2 +- .../README.adoc | 59 ++++++++++++ .../spring-boot-demo-oauth-resource-server/pom.xml | 31 +++++++ .../oauth/SpringBootDemoResourceApplication.java | 21 +++++ .../oauth/config/OauthResourceServerConfig.java | 43 +++++++++ .../oauth/config/OauthResourceTokenConfig.java | 102 +++++++++++++++++++++ .../xkcoding/oauth/controller/TestController.java | 60 ++++++++++++ .../src/main/resources/application.yml | 30 ++++++ .../java/com/xkcoding/oauth/AuthorizationTest.java | 38 ++++++++ .../oauth/controller/TestControllerTest.java | 83 +++++++++++++++++ 13 files changed, 499 insertions(+), 27 deletions(-) create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java create mode 100644 spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java diff --git a/spring-boot-demo-oauth/pom.xml b/spring-boot-demo-oauth/pom.xml index 724e86a..a403df2 100644 --- a/spring-boot-demo-oauth/pom.xml +++ b/spring-boot-demo-oauth/pom.xml @@ -7,6 +7,7 @@ 1.0.0-SNAPSHOT spring-boot-demo-oauth-authorization-server + spring-boot-demo-oauth-resource-server pom @@ -26,31 +27,6 @@ - - org.springframework.boot - 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 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 index cfc942f..d4fff86 100644 --- 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 @@ -11,5 +11,34 @@ spring-boot-demo-oauth-authorization-server + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${spring.boot.version} + + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + 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 index d68c1b2..edbe405 100644 --- 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 @@ -3,7 +3,7 @@ server: spring: datasource: - url: jdbc:mysql://localhost:3306/oauth + url: jdbc:mysql://localhost:3306/oauth?allowPublicKeyRetrieval=true username: root password: 123456 hikari: 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 index 5355e7e..896327e 100644 --- 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 @@ -43,7 +43,7 @@ {{previousText}} 下一步 - 登录 + 登录 diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc new file mode 100644 index 0000000..4083136 --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/README.adoc @@ -0,0 +1,59 @@ += spring-boot-demo-oauth-resource-server +Doc Writer +v1.0, 2019-01-09 +:toc: + +spring boot oauth2 资源服务器,同 授权服务器 一起使用。 + +> 使用 `spring security oauth` + +- JWT 解密,远程公钥获取 +- 基于角色访问控制 +- 基于应用授权域访问控制 + +== jwt 解密 + +要先获取 jwt 公钥 + +[source,java] +.OauthResourceTokenConfig +---- +public class OauthResourceTokenConfig { + // ...... + private String getPubKey() { + // 如果本地没有密钥,就从授权服务器中获取 + return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue()) + ? getKeyFromAuthorizationServer() + : resourceServerProperties.getJwt().getKeyValue(); + } + // ...... +} +---- + +然后配置进去 + +[source, java] +.OauthResourceServerConfig +---- +public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter { + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources + .tokenStore(tokenStore) + .resourceId(resourceServerProperties.getResourceId()); + } +} +---- + +== 访问控制 + +通过 `@EnableGlobalMethodSecurity(prePostEnabled = true)` 注解开启 `spring security` 的全局方法安全控制 + +- `@PreAuthorize("hasRole('ADMIN')")` 校验角色 +- `@PreAuthorize("#oauth2.hasScope('READ')")` 校验令牌授权域 + +== 测试 + +测试用例: `com.xkcoding.oauth.controller.TestControllerTest` + +先获取 `token`,携带 `token` 去访问资源即可。 diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml new file mode 100644 index 0000000..b19d74c --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/pom.xml @@ -0,0 +1,31 @@ + + + + spring-boot-demo-oauth + com.xkcoding + 1.0.0-SNAPSHOT + + 4.0.0 + + spring-boot-demo-oauth-resource-server + + + + org.springframework.boot + spring-boot-starter-web + + + + org.springframework.boot + spring-boot-starter-security + + + + org.springframework.security.oauth.boot + spring-security-oauth2-autoconfigure + ${spring.boot.version} + + + diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java new file mode 100644 index 0000000..33b7bd9 --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/SpringBootDemoResourceApplication.java @@ -0,0 +1,21 @@ +package com.xkcoding.oauth; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; + +/** + * 启动器. + * + * @author EchoCow + * @date 2020/1/9 上午11:38 + * @version V1.0 + */ +@EnableResourceServer +@SpringBootApplication +public class SpringBootDemoResourceApplication { + public static void main(String[] args) { + SpringApplication.run(SpringBootDemoResourceApplication.class, args); + } + +} diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java new file mode 100644 index 0000000..2d3243e --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceServerConfig.java @@ -0,0 +1,43 @@ +package com.xkcoding.oauth.config; + +import lombok.AllArgsConstructor; +import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer; +import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter; +import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer; +import org.springframework.security.oauth2.provider.token.TokenStore; + +/** + * 资源服务器配置. + * 我们自己实现了它的配置,所以它的自动装配不会生效 + * + * @author EchoCow + * @date 2020/1/9 下午2:20 + */ +@Configuration +@AllArgsConstructor +@EnableResourceServer +@EnableGlobalMethodSecurity(prePostEnabled = true) +public class OauthResourceServerConfig extends ResourceServerConfigurerAdapter { + + private final ResourceServerProperties resourceServerProperties; + private final TokenStore tokenStore; + + @Override + public void configure(ResourceServerSecurityConfigurer resources) { + resources + .tokenStore(tokenStore) + .resourceId(resourceServerProperties.getResourceId()); + } + + @Override + public void configure(HttpSecurity http) throws Exception { + super.configure(http); + // 前后端分离下,可以关闭 csrf + http.csrf().disable(); + } + +} diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java new file mode 100644 index 0000000..c28c72c --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/config/OauthResourceTokenConfig.java @@ -0,0 +1,102 @@ +package com.xkcoding.oauth.config; + +import cn.hutool.json.JSONObject; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.boot.autoconfigure.security.oauth2.resource.ResourceServerProperties; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.security.oauth2.provider.token.TokenStore; +import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter; +import org.springframework.security.oauth2.provider.token.store.JwtTokenStore; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestTemplate; + +import java.io.IOException; +import java.util.Base64; + +/** + * token 相关配置,jwt 相关. + * + * @author EchoCow + * @date 2020/1/9 下午2:39 + */ +@Slf4j +@Configuration +@AllArgsConstructor +public class OauthResourceTokenConfig { + + private final ResourceServerProperties resourceServerProperties; + + /** + * 这里并不是对令牌的存储,他将访问令牌与身份验证进行转换 + * 在需要 {@link TokenStore} 的任何地方可以使用此方法 + * + * @return TokenStore + */ + @Bean + public TokenStore tokenStore() { + return new JwtTokenStore(jwtAccessTokenConverter()); + } + + /** + * jwt 令牌转换 + * + * @return jwt + */ + @Bean + public JwtAccessTokenConverter jwtAccessTokenConverter() { + JwtAccessTokenConverter converter = new JwtAccessTokenConverter(); + converter.setVerifierKey(getPubKey()); + return converter; + } + + /** + * 非对称密钥加密,获取 public key。 + * 自动选择加载方式。 + * + * @return public key + */ + private String getPubKey() { + // 如果本地没有密钥,就从授权服务器中获取 + return StringUtils.isEmpty(resourceServerProperties.getJwt().getKeyValue()) + ? getKeyFromAuthorizationServer() + : resourceServerProperties.getJwt().getKeyValue(); + } + + /** + * 本地没有公钥的时候,从服务器上获取 + * 需要进行 Basic 认证 + * + * @return public key + */ + private String getKeyFromAuthorizationServer() { + ObjectMapper objectMapper = new ObjectMapper(); + HttpHeaders httpHeaders = new HttpHeaders(); + httpHeaders.add(HttpHeaders.AUTHORIZATION, encodeClient()); + HttpEntity requestEntity = new HttpEntity<>(null, httpHeaders); + String pubKey = new RestTemplate() + .getForObject(resourceServerProperties.getJwt().getKeyUri(), String.class, requestEntity); + try { + JSONObject body = objectMapper.readValue(pubKey, JSONObject.class); + log.info("Get Key From Authorization Server."); + return body.getStr("value"); + } catch (IOException e) { + log.error("Get public key error: {}", e.getMessage()); + } + return null; + } + + /** + * 客户端信息 + * + * @return basic + */ + private String encodeClient() { + return "Basic " + Base64.getEncoder().encodeToString((resourceServerProperties.getClientId() + + ":" + resourceServerProperties.getClientSecret()).getBytes()); + } +} diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java new file mode 100644 index 0000000..9c6ed62 --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/java/com/xkcoding/oauth/controller/TestController.java @@ -0,0 +1,60 @@ +package com.xkcoding.oauth.controller; + +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +/** + * 测试接口. + * + * @author EchoCow + * @date 2020/1/9 下午2:37 + */ +@RestController +public class TestController { + + /** + * 拥有 ROLE_ADMIN 的用户才能访问的资源 + * + * @return ADMIN + */ + @PreAuthorize("hasRole('ADMIN')") + @GetMapping("/admin") + public String admin() { + return "ADMIN"; + } + + /** + * 拥有 ROLE_TEST 的用户才能访问的资源 + * + * @return TEST + */ + @PreAuthorize("hasRole('TEST')") + @GetMapping("/test") + public String test() { + return "TEST"; + } + + /** + * scope 有 READ 的用户资源才能访问 + * + * @return READ + */ + @PreAuthorize("#oauth2.hasScope('READ')") + @GetMapping("/read") + public String read() { + return "READ"; + } + + /** + * scope 有 WRITE 的用户资源才能访问 + * + * @return WRITE + */ + @PreAuthorize("#oauth2.hasScope('WRITE')") + @GetMapping("/write") + public String write() { + return "WRITE"; + } + +} diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml new file mode 100644 index 0000000..9d6558a --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/main/resources/application.yml @@ -0,0 +1,30 @@ +server: + port: 8081 +security: + oauth2: + resource: + token-info-uri: http://localhost:8080/oauth/check_token + jwt: + key-alias: oauth2 + # 如果没有此项会去请求授权服务器获取 + key-value: | + -----BEGIN PUBLIC KEY----- + MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAkF9SyMHeGAsLMwbPsKj/ + xpEtS0iCe8vTSBnIGBDZKmB3ma20Ry0Uzn3m+f40RwCXlxnUcvTw7ipoz0tMQERQ + b3X4DkYCJXPK6pAD+R9/J5odEwrO2eysByWfcbMjsZw2u5pH5hleMS0YqkrGQOxJ + pzlEcKxMePU5KYTbKUJkhOYPY+gQr61g6lF97WggSPtuQn1srT+Ptvfw6yRC4bdI + 0zV5emfXjmoLUwaQTRoGYhOFrm97vpoKiltSNIDFW01J1Lr+l77ddDFC6cdiAC0H + 5/eENWBBBTFWya8RlBTzHuikfFS1gP49PZ6MYJIVRs8p9YnnKTy7TVcGKY3XZMCA + mwIDAQAB + -----END PUBLIC KEY----- + key-uri: http://localhost:8080/oauth/token_key + id: oauth2 + client: + client-id: oauth2 + client-secret: oauth2 + access-token-uri: http://localhost:8080/oauth/token + scope: READ + +logging: + level: + org.springframework.security: debug diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java new file mode 100644 index 0000000..774a4ec --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/AuthorizationTest.java @@ -0,0 +1,38 @@ +package com.xkcoding.oauth; + +import org.junit.jupiter.api.Test; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordResourceDetails; + +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * . + * + * @author EchoCow + * @date 2020/1/9 下午3:44 + */ +public class AuthorizationTest { + public static final String AUTHORIZATION_SERVER = "http://127.0.0.1:8080"; + + protected OAuth2RestTemplate oauth2RestTemplate(String username, String password, List scope) { + ResourceOwnerPasswordResourceDetails resource = new ResourceOwnerPasswordResourceDetails(); + resource.setAccessTokenUri(AUTHORIZATION_SERVER + "/oauth/token"); + resource.setClientId("oauth2"); + resource.setClientSecret("oauth2"); + resource.setId("oauth2"); + resource.setScope(scope); + resource.setUsername(username); + resource.setPassword(password); + return new OAuth2RestTemplate(resource); + } + + @Test + void testAccessTokenWhenPassed() { + assertNotNull(oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")) + .getAccessToken()); + } +} diff --git a/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java new file mode 100644 index 0000000..ea0a432 --- /dev/null +++ b/spring-boot-demo-oauth/spring-boot-demo-oauth-resource-server/src/test/java/com/xkcoding/oauth/controller/TestControllerTest.java @@ -0,0 +1,83 @@ +package com.xkcoding.oauth.controller; + +import com.xkcoding.oauth.AuthorizationTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.oauth2.client.OAuth2RestTemplate; +import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException; + +import java.util.Arrays; +import java.util.Collections; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.springframework.http.HttpMethod.GET; + +/** + * . + * + * @author EchoCow + * @date 2020/1/9 下午3:46 + */ +public class TestControllerTest extends AuthorizationTest { + + private static final String URL = "http://127.0.0.1:8081"; + + @Test + @DisplayName("ROLE_ADMIN 角色测试") + void testAdminRoleSucceedAndTestRoleFailedWhenPassed() { + OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")); + ResponseEntity response = template.exchange(URL + "/admin", GET, null, String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("ADMIN", response.getBody()); + assertThrows(OAuth2AccessDeniedException.class, + () -> template.exchange(URL + "/test", GET, null, String.class)); + } + + @Test + @DisplayName("ROLE_Test 角色测试") + void testTestRoleSucceedWhenPassed() { + OAuth2RestTemplate template = oauth2RestTemplate("test", "123456", Collections.singletonList("READ")); + ResponseEntity response = template.exchange(URL + "/test", GET, null, String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("TEST", response.getBody()); + assertThrows(OAuth2AccessDeniedException.class, + () -> template.exchange(URL + "/admin", GET, null, String.class)); + } + + @Test + @DisplayName("SCOPE_READ 授权域测试") + void testScopeReadWhenPassed() { + OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("READ")); + ResponseEntity response = template.exchange(URL + "/read", GET, null, String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("READ", response.getBody()); + assertThrows(OAuth2AccessDeniedException.class, + () -> template.exchange(URL + "/write", GET, null, String.class)); + } + + @Test + @DisplayName("SCOPE_WRITE 授权域测试") + void testScopeWriteWhenPassed() { + OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Collections.singletonList("WRITE")); + ResponseEntity response = template.exchange(URL + "/write", GET, null, String.class); + assertEquals(HttpStatus.OK, response.getStatusCode()); + assertEquals("WRITE", response.getBody()); + assertThrows(OAuth2AccessDeniedException.class, + () -> template.exchange(URL + "/read", GET, null, String.class)); + } + + @Test + @DisplayName("SCOPE 测试") + void testScopeWhenPassed() { + OAuth2RestTemplate template = oauth2RestTemplate("admin", "123456", Arrays.asList("READ", "WRITE")); + ResponseEntity writeResponse = template.exchange(URL + "/write", GET, null, String.class); + assertEquals(HttpStatus.OK, writeResponse.getStatusCode()); + assertEquals("WRITE", writeResponse.getBody()); + ResponseEntity readResponse = template.exchange(URL + "/read", GET, null, String.class); + assertEquals(HttpStatus.OK, readResponse.getStatusCode()); + assertEquals("READ", readResponse.getBody()); + } +}