├── others ├── restful_oauth_token_demo.jpg ├── 升级spring-boot的变化.txt ├── how_to_use.txt ├── database │ ├── initial_db.ddl │ ├── initial_data.ddl │ └── oauth.ddl └── oauth_test.txt ├── src ├── main │ ├── resources │ │ ├── static │ │ │ └── favicon.ico │ │ ├── banner.txt │ │ ├── templates │ │ │ ├── fragments │ │ │ │ └── main.html │ │ │ ├── consent_error.html │ │ │ ├── error │ │ │ │ └── 403.html │ │ │ ├── mobile │ │ │ │ └── dashboard.html │ │ │ ├── unity │ │ │ │ └── dashboard.html │ │ │ ├── device_verification.html │ │ │ ├── user_overview.html │ │ │ ├── clientdetails │ │ │ │ └── client_details.html │ │ │ ├── index.html │ │ │ ├── consent.html │ │ │ ├── login.html │ │ │ └── user_form.html │ │ ├── application.properties │ │ ├── logback.xml │ │ └── jwks.json │ └── java │ │ └── com │ │ └── monkeyk │ │ └── sos │ │ ├── domain │ │ ├── shared │ │ │ ├── Repository.java │ │ │ ├── SOSConstants.java │ │ │ ├── GuidGenerator.java │ │ │ └── security │ │ │ │ └── SOSUserDetails.java │ │ ├── user │ │ │ ├── Privilege.java │ │ │ └── UserRepository.java │ │ ├── oauth │ │ │ ├── OauthRepository.java │ │ │ └── ClaimsOAuth2TokenCustomizer.java │ │ └── AbstractDomain.java │ │ ├── SpringOauthServerApplication.java │ │ ├── infrastructure │ │ ├── PasswordHandler.java │ │ ├── DateUtils.java │ │ ├── jdbc │ │ │ ├── UserProfileRowMapper.java │ │ │ ├── UserRowMapper.java │ │ │ ├── OauthClientDetailsRowMapper.java │ │ │ └── OauthRepositoryJdbc.java │ │ ├── PKCEUtils.java │ │ └── SettingsUtils.java │ │ ├── service │ │ ├── UserService.java │ │ ├── OauthService.java │ │ ├── business │ │ │ ├── RefreshTokenInlineAccessTokenInvoker.java │ │ │ ├── ClientCredentialsInlineAccessTokenInvoker.java │ │ │ ├── PasswordInlineAccessTokenInvoker.java │ │ │ └── InlineAccessTokenInvoker.java │ │ ├── dto │ │ │ ├── UserOverviewDto.java │ │ │ ├── UserFormDto.java │ │ │ ├── UserJsonDto.java │ │ │ ├── AccessTokenDto.java │ │ │ ├── UserDto.java │ │ │ └── ClientSettingsDto.java │ │ └── impl │ │ │ ├── OauthServiceImpl.java │ │ │ └── UserServiceImpl.java │ │ ├── web │ │ ├── authentication │ │ │ ├── AuthenticationRestConverter.java │ │ │ ├── AbstractAuthenticationRestConverter.java │ │ │ ├── DelegatingAuthenticationRestConverter.java │ │ │ ├── OAuth2DeviceCodeAuthenticationRestConverter.java │ │ │ ├── OAuth2ClientCredentialsAuthenticationRestConverter.java │ │ │ ├── OAuth2RefreshTokenAuthenticationRestConverter.java │ │ │ └── OAuth2AuthorizationCodeAuthenticationRestConverter.java │ │ ├── controller │ │ │ ├── resource │ │ │ │ ├── MobileController.java │ │ │ │ └── UnityController.java │ │ │ ├── OAuth2DeviceVerificationController.java │ │ │ ├── SOSController.java │ │ │ ├── UserController.java │ │ │ ├── UserFormDtoValidator.java │ │ │ ├── JwtBearerJwksController.java │ │ │ ├── ClientDetailsController.java │ │ │ └── OauthClientDetailsDtoValidator.java │ │ ├── filter │ │ │ └── CharacterEncodingIPFilter.java │ │ ├── WebUtils.java │ │ └── context │ │ │ └── SOSContextHolder.java │ │ └── config │ │ ├── MVCConfiguration.java │ │ └── WebSecurityConfigurer.java ├── test │ ├── java │ │ └── com │ │ │ └── monkeyk │ │ │ └── sos │ │ │ ├── domain │ │ │ └── shared │ │ │ │ └── GuidGeneratorTest.java │ │ │ ├── SpringOauthServerApplicationTests.java │ │ │ ├── infrastructure │ │ │ ├── AbstractRepositoryTest.java │ │ │ ├── PasswordHandlerTest.java │ │ │ ├── DateUtilsTest.java │ │ │ ├── SettingsUtilsTest.java │ │ │ ├── PKCEUtilsTest.java │ │ │ └── jdbc │ │ │ │ ├── OauthRepositoryJdbcTest.java │ │ │ │ └── UserRepositoryJdbcTest.java │ │ │ ├── ContextTest.java │ │ │ ├── service │ │ │ ├── dto │ │ │ │ └── TokenSettingsDtoTest.java │ │ │ ├── business │ │ │ │ ├── AbstractInlineAccessTokenInvokerTest.java │ │ │ │ ├── ClientCredentialsInlineAccessTokenInvokerTest.java │ │ │ │ ├── PasswordInlineAccessTokenInvokerTest.java │ │ │ │ └── RefreshTokenInlineAccessTokenInvokerTest.java │ │ │ ├── JwksTest.java │ │ │ └── JwtBearerFlowTest.java │ │ │ ├── config │ │ │ └── OAuth2ServerConfigurationTest.java │ │ │ └── web │ │ │ └── controller │ │ │ ├── resource │ │ │ └── UnityControllerTest.java │ │ │ └── OAuthRestControllerTest.java │ └── resources │ │ ├── application-test.properties │ │ └── logback.xml └── docs │ └── asciidoc │ └── index.adoc └── .gitignore /others/restful_oauth_token_demo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeyk/spring-oauth-server/HEAD/others/restful_oauth_token_demo.jpg -------------------------------------------------------------------------------- /src/main/resources/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/monkeyk/spring-oauth-server/HEAD/src/main/resources/static/favicon.ico -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/shared/Repository.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.shared; 2 | 3 | /** 4 | * @author Shengzhao Li 5 | */ 6 | 7 | public interface Repository { 8 | } -------------------------------------------------------------------------------- /others/升级spring-boot的变化.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 记录 spring-oauth-server 升级到 spring-boot 后的变化 4 | 5 | 1. client_secret 加密保存 6 | 2. 密码加密方式由 MD5 变成 BCrypt 7 | 3. resourceId 可为可选 8 | 4.增加CSRF支持 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | !.mvn/wrapper/maven-wrapper.jar 3 | 4 | ### STS ### 5 | .apt_generated 6 | .classpath 7 | .factorypath 8 | .project 9 | .settings 10 | .springBeans 11 | 12 | ### IntelliJ IDEA ### 13 | .idea 14 | *.iws 15 | *.iml 16 | *.ipr 17 | 18 | ### NetBeans ### 19 | nbproject/private/ 20 | build/ 21 | nbbuild/ 22 | dist/ 23 | nbdist/ 24 | .nb-gradle/ -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/user/Privilege.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.user; 2 | 3 | /** 4 | * @author Shengzhao Li 5 | */ 6 | public enum Privilege { 7 | /** 8 | * Default privilege 9 | */ 10 | USER, 11 | 12 | /** 13 | * //admin 14 | */ 15 | ADMIN, 16 | /** 17 | * //资源权限:UNITY 18 | */ 19 | UNITY, 20 | /** 21 | * //资源权限:MOBILE 22 | */ 23 | MOBILE 24 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/domain/shared/GuidGeneratorTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.shared; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.*; 6 | 7 | /** 8 | * 2023/10/13 10:28 9 | * 10 | * @author Shengzhao Li 11 | * @since 3.0.0 12 | */ 13 | class GuidGeneratorTest { 14 | 15 | @Test 16 | void generate() { 17 | 18 | String generate = GuidGenerator.generate(); 19 | assertNotNull(generate); 20 | // System.out.println(generate); 21 | } 22 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/SpringOauthServerApplicationTests.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos; 2 | 3 | 4 | import org.junit.jupiter.api.Test; 5 | import org.springframework.boot.test.context.SpringBootTest; 6 | import org.springframework.test.context.TestPropertySource; 7 | 8 | 9 | /** 10 | * @since 2.0.0 11 | */ 12 | @SpringBootTest 13 | @TestPropertySource(locations = "classpath:application-test.properties") 14 | public class SpringOauthServerApplicationTests { 15 | 16 | @Test 17 | public void contextLoads() { 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/AbstractRepositoryTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import com.monkeyk.sos.ContextTest; 4 | import org.springframework.beans.factory.annotation.Autowired; 5 | import org.springframework.jdbc.core.JdbcTemplate; 6 | 7 | /** 8 | * @author Shengzhao Li 9 | */ 10 | public abstract class AbstractRepositoryTest extends ContextTest { 11 | 12 | 13 | @Autowired 14 | private JdbcTemplate jdbcTemplate; 15 | 16 | 17 | public JdbcTemplate jdbcTemplate() { 18 | return jdbcTemplate; 19 | } 20 | 21 | 22 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/oauth/OauthRepository.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.oauth; 2 | 3 | import com.monkeyk.sos.domain.shared.Repository; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author Shengzhao Li 9 | * @since 1.0.0 10 | */ 11 | public interface OauthRepository extends Repository { 12 | 13 | OauthClientDetails findOauthClientDetails(String clientId); 14 | 15 | List findAllOauthClientDetails(); 16 | 17 | void updateOauthClientDetailsArchive(String clientId, boolean archive); 18 | 19 | void saveOauthClientDetails(OauthClientDetails clientDetails); 20 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/SpringOauthServerApplication.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos; 2 | 3 | import org.springframework.boot.SpringApplication; 4 | import org.springframework.boot.autoconfigure.SpringBootApplication; 5 | 6 | 7 | /** 8 | * 2017-12-05 9 | * 10 | * @author Shengzhao Li 11 | * @since 1.0.0 12 | */ 13 | @SpringBootApplication 14 | public class SpringOauthServerApplication { 15 | 16 | /** 17 | * 详细 请参考 others/how_to_use.txt 文件 18 | * 19 | * @param args args 20 | */ 21 | public static void main(String[] args) { 22 | SpringApplication.run(SpringOauthServerApplication.class, args); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/PasswordHandler.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import com.monkeyk.sos.web.context.SOSContextHolder; 4 | import org.springframework.security.crypto.password.PasswordEncoder; 5 | 6 | /** 7 | * 2016/3/25 8 | * 9 | * @author Shengzhao Li 10 | */ 11 | public abstract class PasswordHandler { 12 | 13 | 14 | 15 | private PasswordHandler() { 16 | } 17 | 18 | 19 | public static String encode(String password) { 20 | PasswordEncoder passwordEncoder = SOSContextHolder.getBean(PasswordEncoder.class); 21 | return passwordEncoder.encode(password); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/UserService.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service; 2 | 3 | import com.monkeyk.sos.service.dto.UserFormDto; 4 | import com.monkeyk.sos.service.dto.UserJsonDto; 5 | import com.monkeyk.sos.service.dto.UserOverviewDto; 6 | import org.springframework.security.core.userdetails.UserDetailsService; 7 | 8 | /** 9 | * @author Shengzhao Li 10 | */ 11 | public interface UserService extends UserDetailsService { 12 | 13 | UserJsonDto loadCurrentUserJsonDto(); 14 | 15 | UserOverviewDto loadUserOverviewDto(UserOverviewDto overviewDto); 16 | 17 | boolean isExistedUsername(String username); 18 | 19 | String saveUser(UserFormDto formDto); 20 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/AuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | 5 | import java.util.Map; 6 | 7 | /** 8 | * 2023/10/31 10:27 9 | * 10 | * @author Shengzhao Li 11 | * @see org.springframework.security.web.authentication.AuthenticationConverter 12 | * @since 3.0.0 13 | */ 14 | public interface AuthenticationRestConverter { 15 | 16 | /** 17 | * 从请求参数中转化到 Authentication 18 | * 19 | * @param parameters 请求参数 20 | * @return Authentication or null 21 | */ 22 | Authentication convert(Map parameters); 23 | 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/OauthService.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service; 2 | 3 | import com.monkeyk.sos.service.dto.OauthClientDetailsDto; 4 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 5 | 6 | import java.util.List; 7 | 8 | /** 9 | * @author Shengzhao Li 10 | */ 11 | 12 | public interface OauthService { 13 | 14 | OauthClientDetails loadOauthClientDetails(String clientId); 15 | 16 | List loadAllOauthClientDetailsDtos(); 17 | 18 | void archiveOauthClientDetails(String clientId); 19 | 20 | OauthClientDetailsDto loadOauthClientDetailsDto(String clientId); 21 | 22 | void registerClientDetails(OauthClientDetailsDto formDto); 23 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvoker.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | 4 | 5 | /** 6 | * 2019/7/5 7 | *

8 | *

9 | * grant_type = refresh_token 10 | * 11 | * @author Shengzhao Li 12 | * @since 2.0.1 13 | */ 14 | public class RefreshTokenInlineAccessTokenInvoker extends InlineAccessTokenInvoker { 15 | 16 | 17 | public RefreshTokenInlineAccessTokenInvoker() { 18 | } 19 | 20 | // @Override 21 | // protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) { 22 | // return new RefreshTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory); 23 | // } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/docs/asciidoc/index.adoc: -------------------------------------------------------------------------------- 1 | = {project-id} API Docs 2 | :toc: left 3 | :toc-title: Contents 4 | :revnumber: {project-version} 5 | 6 | 7 | // == 应用版本API 8 | // .http-request 9 | // include::{snippets}/UnityControllerTest/version/http-request.adoc[] 10 | // .curl-request 11 | // include::{snippets}/UnityControllerTest/version/curl-request.adoc[] 12 | // .request-body 13 | // include::{snippets}/UnityControllerTest/version/request-body.adoc[] 14 | // .http-response 15 | // include::{snippets}/UnityControllerTest/version/http-response.adoc[] 16 | // .response-body 17 | // include::{snippets}/UnityControllerTest/version/response-body.adoc[] 18 | 19 | == Unity resource API 20 | 21 | operation::UnityControllerTest/userInfo[] 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/ContextTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos; 2 | 3 | import org.springframework.boot.test.context.SpringBootTest; 4 | import org.springframework.test.context.TestPropertySource; 5 | import org.springframework.test.context.junit4.AbstractTransactionalJUnit4SpringContextTests; 6 | import org.springframework.test.context.transaction.BeforeTransaction; 7 | 8 | /** 9 | * @author Shengzhao Li 10 | */ 11 | 12 | @SpringBootTest 13 | @TestPropertySource(locations = "classpath:application-test.properties") 14 | public abstract class ContextTest extends AbstractTransactionalJUnit4SpringContextTests { 15 | 16 | 17 | @BeforeTransaction 18 | public void before() throws Exception { 19 | 20 | } 21 | 22 | 23 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/dto/TokenSettingsDtoTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; 5 | 6 | import static org.junit.jupiter.api.Assertions.*; 7 | 8 | /** 9 | * 2023/10/13 14:24 10 | * 11 | * @author Shengzhao Li 12 | */ 13 | class TokenSettingsDtoTest { 14 | 15 | 16 | @Test 17 | void toSettings() { 18 | 19 | 20 | TokenSettingsDto settingsDto = new TokenSettingsDto(); 21 | TokenSettings tokenSettings = settingsDto.toSettings(); 22 | assertNotNull(tokenSettings); 23 | // System.out.println(tokenSettings); 24 | 25 | 26 | } 27 | 28 | 29 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvoker.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | 4 | 5 | /** 6 | * 2019/7/5 7 | *

8 | *

9 | * grant_type = client_credentials 10 | * 11 | * @author Shengzhao Li 12 | * @since 2.0.1 13 | */ 14 | public class ClientCredentialsInlineAccessTokenInvoker extends InlineAccessTokenInvoker { 15 | 16 | 17 | public ClientCredentialsInlineAccessTokenInvoker() { 18 | } 19 | 20 | // @Override 21 | // protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) { 22 | // return new ClientCredentialsTokenGranter(this.tokenServices, this.clientDetailsService, oAuth2RequestFactory); 23 | // } 24 | 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvoker.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | 4 | 5 | /** 6 | * 2019/7/5 7 | *

8 | *

9 | * grant_type = password 10 | * 11 | * @author Shengzhao Li 12 | * @since 2.0.1 13 | */ 14 | public class PasswordInlineAccessTokenInvoker extends InlineAccessTokenInvoker { 15 | 16 | 17 | public PasswordInlineAccessTokenInvoker() { 18 | } 19 | 20 | // @Override 21 | // protected TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory) { 22 | // return new ResourceOwnerPasswordTokenGranter(this.authenticationManager, this.tokenServices, this.clientDetailsService, oAuth2RequestFactory); 23 | // } 24 | 25 | 26 | 27 | } 28 | -------------------------------------------------------------------------------- /src/main/resources/banner.txt: -------------------------------------------------------------------------------- 1 | ${AnsiColor.BRIGHT_BLACK} 2 | _ _ _ 3 | (_) | | | | 4 | ___ _ __ _ __ _ _ __ __ _ ______ ___ __ _ _ _| |_| |__ ______ ___ ___ _ ____ _____ _ __ 5 | / __| '_ \| '__| | '_ \ / _` |______/ _ \ / _` | | | | __| '_ \______/ __|/ _ \ '__\ \ / / _ \ '__| 6 | \__ \ |_) | | | | | | | (_| | | (_) | (_| | |_| | |_| | | | \__ \ __/ | \ V / __/ | 7 | |___/ .__/|_| |_|_| |_|\__, | \___/ \__,_|\__,_|\__|_| |_| |___/\___|_| \_/ \___|_| 8 | | | __/ | 9 | |_| |___/ 10 | spring-oauth-server: ${application.formatted-version} 11 | spring -boot: ${spring-boot.formatted-version} 12 | -------------------------------------------------------------------------------- /others/how_to_use.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 使用的主要技术与版本号 4 | *Java (openjdk 17) 5 | *Spring-Boot (3.1.2) 6 | *spring-security-oauth2-authorization-server (1.1.1) 7 | 8 | 9 | 如何使用? 10 | 1.项目是Maven管理的, 需要本地安装maven(开发用的maven版本号为3.6.0), 还有MySql(开发用的mysql版本号为5.7.22) 11 | 12 | 2.下载(或clone)项目到本地 13 | 14 | 3.创建MySQL数据库(数据库名oauth2_boot), 并运行相应的SQL脚本(脚本文件位于others/database目录), 15 | 运行脚本的顺序: initial_db.ddl -> oauth.ddl -> initial_data.ddl 16 | 17 | 4.修改application.properties(位于src/resources目录)中的数据库连接信息(包括username, password等) 18 | 19 | 5.将本地项目导入到IDE(如Intellij IDEA)中,直接运行 SpringOauthServerApplication.java (默认端口为8080) 20 | 21 | 6.参考oauth2.1-flow.md(位于others目录)的内容并测试之(也可在浏览器中访问相应的地址,如: http://localhost:8080). 22 | 23 | 7. 运行单元测试时请先创建数据库 oauth2_boot_test, 并依次运行SQL脚本. 24 | 运行脚本的顺序: initial_db.ddl -> oauth.ddl 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/user/UserRepository.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.user; 2 | 3 | import com.monkeyk.sos.domain.shared.Repository; 4 | 5 | import java.util.List; 6 | 7 | /** 8 | * @author Shengzhao Li 9 | */ 10 | 11 | public interface UserRepository extends Repository { 12 | 13 | User findByGuid(String guid); 14 | 15 | void saveUser(User user); 16 | 17 | void updateUser(User user); 18 | 19 | User findByUsername(String username); 20 | 21 | /** 22 | * 查询 User 的 各类 profile 基础数据 23 | * 包括 phone, email, address, nickname, updated_at 24 | * 25 | * @param username username 26 | * @return User only have profile fields 27 | * @since 3.0.0 28 | */ 29 | User findProfileByUsername(String username); 30 | 31 | /** 32 | * 注意:产品化的设计此处应该有分页会更好 33 | */ 34 | List findUsersByUsername(String username); 35 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/AbstractAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.oauth2.core.OAuth2AuthenticationException; 4 | import org.springframework.security.oauth2.core.OAuth2Error; 5 | 6 | /** 7 | * 2023/10/31 10:35 8 | * 9 | * @author Shengzhao Li 10 | * @since 3.0.0 11 | */ 12 | public abstract class AbstractAuthenticationRestConverter implements AuthenticationRestConverter { 13 | 14 | static final String ACCESS_TOKEN_REQUEST_ERROR_URI = "https://datatracker.ietf.org/doc/html/rfc6749#section-5.2"; 15 | 16 | 17 | protected void throwError(String errorCode, String parameterName, String errorUri) { 18 | OAuth2Error error = new OAuth2Error(errorCode, "OAuth 2.0 Parameter: " + parameterName, errorUri); 19 | throw new OAuth2AuthenticationException(error); 20 | } 21 | 22 | } 23 | -------------------------------------------------------------------------------- /src/main/resources/templates/fragments/main.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Fragments 7 | 8 |

9 | 10 | 15 |
16 | 17 | 18 | 19 | 20 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/resource/MobileController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller.resource; 2 | 3 | import com.monkeyk.sos.service.dto.UserJsonDto; 4 | import com.monkeyk.sos.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.ResponseBody; 9 | 10 | /** 11 | * @author Shengzhao Li 12 | */ 13 | @Controller 14 | @RequestMapping("/m/") 15 | public class MobileController { 16 | 17 | @Autowired 18 | private UserService userService; 19 | 20 | 21 | @RequestMapping("dashboard") 22 | public String dashboard() { 23 | return "mobile/dashboard"; 24 | } 25 | 26 | @RequestMapping("user_info") 27 | @ResponseBody 28 | public UserJsonDto userInfo() { 29 | return userService.loadCurrentUserJsonDto(); 30 | } 31 | 32 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/resource/UnityController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller.resource; 2 | 3 | import com.monkeyk.sos.service.dto.UserJsonDto; 4 | import com.monkeyk.sos.service.UserService; 5 | import org.springframework.beans.factory.annotation.Autowired; 6 | import org.springframework.stereotype.Controller; 7 | import org.springframework.web.bind.annotation.RequestMapping; 8 | import org.springframework.web.bind.annotation.ResponseBody; 9 | 10 | /** 11 | * @author Shengzhao Li 12 | */ 13 | @Controller 14 | @RequestMapping("/unity/") 15 | public class UnityController { 16 | 17 | 18 | @Autowired 19 | private UserService userService; 20 | 21 | 22 | @RequestMapping("dashboard") 23 | public String dashboard() { 24 | return "unity/dashboard"; 25 | } 26 | 27 | @RequestMapping("user_info") 28 | @ResponseBody 29 | public UserJsonDto userInfo() { 30 | return userService.loadCurrentUserJsonDto(); 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/config/OAuth2ServerConfigurationTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.config; 2 | 3 | import com.nimbusds.jose.jwk.source.JWKSource; 4 | import com.nimbusds.jose.jwk.source.JWKSourceBuilder; 5 | import com.nimbusds.jose.proc.SecurityContext; 6 | import org.junit.jupiter.api.Test; 7 | import org.springframework.core.io.ClassPathResource; 8 | import org.springframework.core.io.Resource; 9 | 10 | import static com.monkeyk.sos.config.OAuth2ServerConfiguration.KEYSTORE_NAME; 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | /** 14 | * 2023/10/12 17:58 15 | * 16 | * @author Shengzhao Li 17 | * @since 3.0.0 18 | */ 19 | class OAuth2ServerConfigurationTest { 20 | 21 | 22 | @Test 23 | void jwkSource() throws Exception { 24 | 25 | Resource resource = new ClassPathResource(KEYSTORE_NAME); 26 | JWKSource jwkSource = JWKSourceBuilder.create(resource.getURL()).build(); 27 | assertNotNull(jwkSource); 28 | 29 | } 30 | 31 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/shared/SOSConstants.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.shared; 2 | 3 | /** 4 | * 2023/9/23 18:54 5 | * 6 | * @author Shengzhao Li 7 | * @since 3.0.0 8 | */ 9 | public interface SOSConstants { 10 | 11 | /** 12 | * device verification URI 13 | * 14 | * @see org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter 15 | */ 16 | String DEVICE_VERIFICATION_ENDPOINT_URI = "/oauth2/device_verification"; 17 | 18 | 19 | /** 20 | * oauth2 consent page uri 21 | */ 22 | String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; 23 | 24 | /** 25 | * oauth2 authorize uri 26 | * 27 | * @see org.springframework.security.oauth2.server.authorization.web.OAuth2AuthorizationEndpointFilter 28 | */ 29 | String AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize"; 30 | 31 | /** 32 | * 对称算法名称前缀,如HS256 33 | * 详见 MacAlgorithm.java 34 | */ 35 | String HS = "HS"; 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/shared/GuidGenerator.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.shared; 2 | 3 | 4 | import org.apache.commons.lang3.RandomStringUtils; 5 | 6 | import java.util.UUID; 7 | 8 | /** 9 | * @author Shengzhao Li 10 | */ 11 | public abstract class GuidGenerator { 12 | 13 | 14 | // private static RandomValueStringGenerator defaultClientSecretGenerator = new RandomValueStringGenerator(32); 15 | 16 | 17 | /** 18 | * private constructor 19 | */ 20 | private GuidGenerator() { 21 | } 22 | 23 | /** 24 | * generate random number, length 32 25 | * 26 | * @return number 27 | * @since 3.0.0 28 | */ 29 | public static String generateNumber() { 30 | return RandomStringUtils.random(32, false, true); 31 | } 32 | 33 | 34 | public static String generate() { 35 | return UUID.randomUUID().toString().replaceAll("-", ""); 36 | } 37 | 38 | public static String generateClientSecret() { 39 | return RandomStringUtils.random(32, true, true); 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /src/main/resources/templates/consent_error.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Consent Error - Spring Security&OAuth2.1 10 | 11 | 12 | 25 | 26 | 27 |
28 |

Consent Error

29 |

Message:

30 |
31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/templates/error/403.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 403 - Spring Security&OAuth2.1 10 | 11 | 12 | 25 | 26 | 27 |
28 |

403 - Access Denied

29 |

Sorry, you do not have permission to access this resource.

30 |
31 | 32 | -------------------------------------------------------------------------------- /src/main/resources/templates/mobile/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Mobile 资源 - Spring Security&OAuth2.1 10 | 11 | 12 | 13 | 14 | Home 15 | 16 |

Hi Mobile 17 | 你已成功访问 [mobile] 资源 18 |

19 | 20 | 用户信息: 21 |
22 | 23 |
24 |
25 |

26 | 访问API 27 |

28 | 用户信息(JSON) 29 | 30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /src/main/resources/templates/unity/dashboard.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Unity 资源 - Spring Security&OAuth2.1 10 | 11 | 12 | 13 | 14 | Home 15 | 16 |

Hi Unity 17 | 你已成功访问 [unity] 资源 18 |

19 | 20 | 用户信息: 21 |
22 | 23 |
24 |
25 |

26 | 访问API 27 |

28 | 用户信息(JSON) 29 | 30 | 31 |
32 | 33 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/UserOverviewDto.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import java.io.Serial; 4 | import java.io.Serializable; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | 8 | /** 9 | * 2016/3/12 10 | * 11 | * @author Shengzhao Li 12 | */ 13 | public class UserOverviewDto implements Serializable { 14 | @Serial 15 | private static final long serialVersionUID = 2023379587030489248L; 16 | 17 | 18 | private String username; 19 | 20 | 21 | private List userDtos = new ArrayList<>(); 22 | 23 | 24 | public UserOverviewDto() { 25 | } 26 | 27 | public int getSize() { 28 | return userDtos.size(); 29 | } 30 | 31 | public String getUsername() { 32 | return username; 33 | } 34 | 35 | public void setUsername(String username) { 36 | this.username = username; 37 | } 38 | 39 | public List getUserDtos() { 40 | return userDtos; 41 | } 42 | 43 | public void setUserDtos(List userDtos) { 44 | this.userDtos = userDtos; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/OAuth2DeviceVerificationController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import org.springframework.stereotype.Controller; 4 | import org.springframework.web.bind.annotation.GetMapping; 5 | import org.springframework.web.bind.annotation.RequestMapping; 6 | import org.springframework.web.bind.annotation.RequestMethod; 7 | 8 | import static com.monkeyk.sos.domain.shared.SOSConstants.DEVICE_VERIFICATION_ENDPOINT_URI; 9 | 10 | 11 | /** 12 | * 2023/10/17 18:49 13 | *

14 | * Device code flow use 15 | * 16 | * @author Shengzhao Li 17 | * @see org.springframework.security.oauth2.server.authorization.web.OAuth2DeviceVerificationEndpointFilter 18 | * @since 3.0.0 19 | */ 20 | @Controller 21 | public class OAuth2DeviceVerificationController { 22 | 23 | 24 | /** 25 | * Device verification page 26 | * 27 | * @return view 28 | */ 29 | @RequestMapping(value = DEVICE_VERIFICATION_ENDPOINT_URI, method = {RequestMethod.GET, RequestMethod.POST}) 30 | public String deviceVerification() { 31 | return "device_verification"; 32 | } 33 | 34 | 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/DateUtils.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import java.time.LocalDate; 4 | import java.time.LocalDateTime; 5 | import java.time.format.DateTimeFormatter; 6 | import java.util.Locale; 7 | 8 | /** 9 | * @author Shengzhao Li 10 | */ 11 | public abstract class DateUtils { 12 | 13 | public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss"; 14 | 15 | 16 | /** 17 | * Private constructor 18 | */ 19 | private DateUtils() { 20 | } 21 | 22 | public static LocalDateTime now() { 23 | return LocalDateTime.now(); 24 | } 25 | 26 | 27 | public static String toDateTime(LocalDateTime date) { 28 | return toDateTime(date, DEFAULT_DATE_TIME_FORMAT); 29 | } 30 | 31 | public static String toDateTime(LocalDateTime dateTime, String pattern) { 32 | return dateTime.format(DateTimeFormatter.ofPattern(pattern, Locale.SIMPLIFIED_CHINESE)); 33 | } 34 | 35 | 36 | 37 | public static String toDateText(LocalDate date, String pattern) { 38 | if (date == null || pattern == null) { 39 | return null; 40 | } 41 | return date.format(DateTimeFormatter.ofPattern(pattern, Locale.SIMPLIFIED_CHINESE)); 42 | } 43 | 44 | 45 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserProfileRowMapper.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure.jdbc; 2 | 3 | import com.monkeyk.sos.domain.user.User; 4 | import org.springframework.jdbc.core.RowMapper; 5 | 6 | import java.sql.ResultSet; 7 | import java.sql.SQLException; 8 | 9 | /** 10 | * table: user_ 11 | * 2023/10/17 12 | * 13 | * @author Shengzhao Li 14 | * @since 3.0.0 15 | */ 16 | public class UserProfileRowMapper implements RowMapper { 17 | 18 | 19 | public UserProfileRowMapper() { 20 | } 21 | 22 | @Override 23 | public User mapRow(ResultSet rs, int i) throws SQLException { 24 | User user = new User(); 25 | 26 | user.id(rs.getInt("id")); 27 | user.guid(rs.getString("guid")); 28 | 29 | user.archived(rs.getBoolean("archived")); 30 | user.createTime(rs.getTimestamp("create_time").toLocalDateTime()); 31 | 32 | user.email(rs.getString("email")); 33 | user.phone(rs.getString("phone")); 34 | user.username(rs.getString("username")); 35 | 36 | user.address(rs.getString("address")); 37 | user.nickname(rs.getString("nickname")); 38 | user.enabled(rs.getBoolean("enabled")); 39 | user.updatedAt(rs.getLong("updated_at")); 40 | 41 | return user; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/SOSController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | 4 | import com.monkeyk.sos.web.WebUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.RequestMapping; 11 | 12 | /** 13 | * 2018/4/19 14 | *

15 | * starup 16 | * 17 | * @author Shengzhao Li 18 | */ 19 | @Controller 20 | public class SOSController { 21 | 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(SOSController.class); 24 | 25 | 26 | /** 27 | * 首页 28 | */ 29 | @RequestMapping(value = "/") 30 | public String index(Model model) { 31 | return "index"; 32 | } 33 | 34 | 35 | //Go login 36 | @GetMapping(value = {"/login"}) 37 | public String login(Model model) { 38 | LOG.info("Go to login, IP: {}", WebUtils.getIp()); 39 | return "login"; 40 | } 41 | 42 | 43 | // /** 44 | // * 403 无权限访问 45 | // * 46 | // * @return view 47 | // * @since 3.0.0 48 | // */ 49 | // @GetMapping("/access_denied") 50 | // public String accessDenied() { 51 | // return "access_denied"; 52 | // } 53 | 54 | 55 | } 56 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/shared/security/SOSUserDetails.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.shared.security; 2 | 3 | import com.fasterxml.jackson.annotation.JsonTypeInfo; 4 | import com.monkeyk.sos.domain.user.Privilege; 5 | import com.monkeyk.sos.domain.user.User; 6 | import org.springframework.security.core.GrantedAuthority; 7 | import org.springframework.security.core.authority.SimpleGrantedAuthority; 8 | 9 | import java.io.Serial; 10 | 11 | /** 12 | * @author Shengzhao Li 13 | */ 14 | @JsonTypeInfo(use = JsonTypeInfo.Id.CLASS, property = "@class") 15 | public class SOSUserDetails extends org.springframework.security.core.userdetails.User { 16 | 17 | @Serial 18 | private static final long serialVersionUID = 3957586021470480642L; 19 | 20 | public static final String ROLE_PREFIX = "ROLE_"; 21 | 22 | public static final GrantedAuthority DEFAULT_USER_ROLE = new SimpleGrantedAuthority(ROLE_PREFIX + Privilege.USER.name()); 23 | 24 | /** 25 | * @since 3.0.0 26 | */ 27 | protected String userGuid; 28 | 29 | 30 | public SOSUserDetails(User user) { 31 | super(user.username(), user.password(), user.enabled(), 32 | true, true, true, user.generateAuthorities()); 33 | this.userGuid = user.guid(); 34 | } 35 | 36 | public String getUserGuid() { 37 | return userGuid; 38 | } 39 | 40 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/PasswordHandlerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | 4 | import com.monkeyk.sos.ContextTest; 5 | import org.junit.jupiter.api.Test; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.crypto.password.PasswordEncoder; 8 | 9 | import static org.junit.jupiter.api.Assertions.assertNotNull; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | 12 | /* 13 | * @author Shengzhao Li 14 | */ 15 | public class PasswordHandlerTest extends ContextTest { 16 | 17 | 18 | @Autowired 19 | private PasswordEncoder passwordEncoder; 20 | 21 | 22 | // @Test 23 | // public void testMd5() throws Exception { 24 | // 25 | // final String md5 = PasswordHandler.encode("123456"); 26 | // assertNotNull(md5); 27 | //// System.out.println(md5); 28 | // } 29 | 30 | @Test 31 | void encode() throws Exception { 32 | 33 | String pwd = "Admin@2013"; 34 | String encode = PasswordHandler.encode(pwd); 35 | assertNotNull(encode); 36 | // System.out.println(encode); 37 | 38 | } 39 | 40 | @Test 41 | void matches() { 42 | String pwd = "Admin@2013"; 43 | boolean matches = passwordEncoder.matches(pwd, "$2a$10$bIIt6KqIMweTZZC.IIHBLuN3dEIJL0LQFRPrtWTujn9O3Sl5Us5vW"); 44 | assertTrue(matches); 45 | } 46 | 47 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/filter/CharacterEncodingIPFilter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.filter; 2 | 3 | import com.monkeyk.sos.web.WebUtils; 4 | import jakarta.servlet.FilterChain; 5 | import jakarta.servlet.ServletException; 6 | import jakarta.servlet.http.HttpServletRequest; 7 | import jakarta.servlet.http.HttpServletResponse; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.web.filter.CharacterEncodingFilter; 11 | 12 | 13 | import java.io.IOException; 14 | 15 | /** 16 | * 2016/1/30 17 | * 18 | * @author Shengzhao Li 19 | * @since 1.0.0 20 | */ 21 | public class CharacterEncodingIPFilter extends CharacterEncodingFilter { 22 | 23 | private static final Logger LOG = LoggerFactory.getLogger(CharacterEncodingIPFilter.class); 24 | 25 | 26 | @Override 27 | protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { 28 | recordIP(request); 29 | super.doFilterInternal(request, response, filterChain); 30 | } 31 | 32 | private void recordIP(HttpServletRequest request) { 33 | final String ip = WebUtils.retrieveClientIp(request); 34 | WebUtils.setIp(ip); 35 | if (LOG.isDebugEnabled()) { 36 | LOG.debug("Send request uri: {}, from IP: {}", request.getRequestURI(), ip); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/UserFormDto.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import com.monkeyk.sos.domain.user.Privilege; 4 | import com.monkeyk.sos.domain.user.User; 5 | import com.monkeyk.sos.infrastructure.PasswordHandler; 6 | 7 | import java.io.Serial; 8 | 9 | /** 10 | * 2016/3/25 11 | * 12 | * @author Shengzhao Li 13 | */ 14 | public class UserFormDto extends UserDto { 15 | @Serial 16 | private static final long serialVersionUID = 7959857016962260738L; 17 | 18 | 19 | private String password; 20 | 21 | public UserFormDto() { 22 | } 23 | 24 | 25 | public Privilege[] getAllPrivileges() { 26 | return new Privilege[]{Privilege.MOBILE, Privilege.UNITY}; 27 | } 28 | 29 | public String getPassword() { 30 | return password; 31 | } 32 | 33 | public void setPassword(String password) { 34 | this.password = password; 35 | } 36 | 37 | public User newUser() { 38 | final User user = new User() 39 | .username(getUsername()) 40 | .phone(getPhone()) 41 | .email(getEmail()) 42 | .password(PasswordHandler.encode(getPassword())); 43 | user.privileges().addAll(getPrivileges()); 44 | //v3.0.0 added 45 | user.address(getAddress()) 46 | .nickname(getNickname()) 47 | .enabled(isEnabled()); 48 | return user; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | spring.application.name=spring-oauth-server 4 | # 5 | # MySQL 6 | ##################### 7 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 8 | spring.datasource.url=jdbc:mysql://localhost:3306/oauth2_boot?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai 9 | spring.datasource.username=andaily 10 | spring.datasource.password=andaily 11 | #Datasource properties 12 | spring.datasource.type=com.zaxxer.hikari.HikariDataSource 13 | spring.datasource.hikari.maximum-pool-size=20 14 | #spring.datasource.hikari.minimum-idle=2 15 | # 16 | # MVC 17 | spring.thymeleaf.encoding=UTF-8 18 | spring.thymeleaf.cache=false 19 | # 20 | server.port=8080 21 | # 22 | # oauth2 custom issuer, since v3.0.0 23 | spring.security.oauth2.authorizationserver.issuer=http://127.0.0.1:${server.port} 24 | # 25 | # Redis 26 | # 27 | #spring.redis.host=localhost 28 | #spring.redis.port=6379 29 | #spring.redis.database=0 30 | #spring.redis.password= 31 | #spring.redis.timeout=2000 32 | #spring.redis.ssl=false 33 | # 34 | # Condition Config 35 | # @since 2.1.0 36 | # Available TokenStore value: jdbc, jwt 37 | #sos.token.store=jwt 38 | # jwt key (length >= 16), optional 39 | # @since 2.1.0 40 | #sos.token.store.jwt.key=IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa 41 | # reuse refreshToken, default true, optional 42 | # @since 2.1.0 43 | #sos.reuse.refresh-token=true 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/resources/application-test.properties: -------------------------------------------------------------------------------- 1 | # 2 | # 3 | spring.application.name=spring-oauth-server 4 | # 5 | # MySQL 6 | ##################### 7 | spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver 8 | spring.datasource.url=jdbc:mysql://localhost:3306/oauth2_boot_test?autoReconnect=true&autoReconnectForPools=true&useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai 9 | spring.datasource.username=andaily 10 | spring.datasource.password=andaily 11 | #Datasource properties 12 | spring.datasource.type=com.zaxxer.hikari.HikariDataSource 13 | spring.datasource.hikari.maximum-pool-size=20 14 | #spring.datasource.hikari.minimum-idle=2 15 | # 16 | # MVC 17 | spring.thymeleaf.encoding=UTF-8 18 | spring.thymeleaf.cache=false 19 | # 20 | server.port=8080 21 | # 22 | # oauth2 custom issuer, since v3.0.0 23 | spring.security.oauth2.authorizationserver.issuer=http://127.0.0.1:${server.port} 24 | # 25 | # Redis 26 | # 27 | #spring.redis.host=localhost 28 | #spring.redis.port=6379 29 | #spring.redis.database=0 30 | #spring.redis.password= 31 | #spring.redis.timeout=2000 32 | #spring.redis.ssl=false 33 | # 34 | # Condition Config 35 | # @since 2.1.0 36 | # Available TokenStore value: jdbc, jwt 37 | #sos.token.store=jwt 38 | # jwt key (length >= 16), optional 39 | # @since 2.1.0 40 | #sos.token.store.jwt.key=IH6S2dhCEMwGr7uE4fBakSuDh9SoIrRa 41 | # reuse refreshToken, default true, optional 42 | # @since 2.1.0 43 | #sos.reuse.refresh-token=true 44 | 45 | 46 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/DateUtilsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure; 13 | 14 | 15 | 16 | import org.junit.jupiter.api.Test; 17 | 18 | import java.sql.Timestamp; 19 | import java.time.LocalDate; 20 | import java.time.LocalDateTime; 21 | 22 | import static org.junit.jupiter.api.Assertions.*; 23 | 24 | 25 | /* 26 | * @author Shengzhao Li 27 | */ 28 | public class DateUtilsTest { 29 | 30 | 31 | @Test 32 | public void convert() { 33 | 34 | LocalDateTime localDateTime = LocalDateTime.of(2015, 4, 3, 12, 30, 22); 35 | 36 | final LocalDate localDate = localDateTime.toLocalDate(); 37 | System.out.println(localDate); 38 | 39 | final Timestamp timestamp = Timestamp.valueOf(localDateTime); 40 | assertNotNull(timestamp); 41 | System.out.println(timestamp); 42 | 43 | 44 | final String text = DateUtils.toDateTime(localDateTime); 45 | assertEquals(text,"2015-04-03 12:30:22"); 46 | 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/SettingsUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; 5 | import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; 6 | 7 | import static org.junit.jupiter.api.Assertions.*; 8 | 9 | /** 10 | * 2023/10/13 14:59 11 | * 12 | * @author Shengzhao Li 13 | * @since 3.0.0 14 | */ 15 | class SettingsUtilsTest { 16 | 17 | 18 | @Test 19 | void textTokenSettings() { 20 | 21 | TokenSettings settings = TokenSettings.builder() 22 | .reuseRefreshTokens(false) 23 | .build(); 24 | String s = SettingsUtils.textTokenSettings(settings); 25 | assertNotNull(s); 26 | // System.out.println(s); 27 | 28 | TokenSettings tokenSettings = SettingsUtils.buildTokenSettings(s); 29 | assertNotNull(tokenSettings); 30 | 31 | } 32 | 33 | @Test 34 | void textClientSettings() { 35 | 36 | ClientSettings settings = ClientSettings.builder() 37 | .requireProofKey(true) 38 | .build(); 39 | String s = SettingsUtils.textClientSettings(settings); 40 | assertNotNull(s); 41 | 42 | // System.out.println(s); 43 | 44 | ClientSettings clientSettings = SettingsUtils.buildClientSettings(s); 45 | assertNotNull(clientSettings); 46 | 47 | } 48 | 49 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/WebUtils.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web; 2 | 3 | 4 | import jakarta.servlet.http.HttpServletRequest; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | /** 8 | * @author Shengzhao Li 9 | */ 10 | public abstract class WebUtils { 11 | 12 | 13 | public static final String UTF_8 = "UTF-8"; 14 | 15 | 16 | /** 17 | * Sync by pom.xml 18 | */ 19 | public static final String VERSION = "3.0.0"; 20 | 21 | 22 | private static ThreadLocal ipThreadLocal = new ThreadLocal<>(); 23 | 24 | 25 | public static void setIp(String ip) { 26 | ipThreadLocal.set(ip); 27 | } 28 | 29 | public static String getIp() { 30 | return ipThreadLocal.get(); 31 | } 32 | 33 | //private 34 | private WebUtils() { 35 | } 36 | 37 | 38 | /** 39 | * Retrieve client ip address 40 | * 41 | * @param request HttpServletRequest 42 | * @return IP 43 | */ 44 | public static String retrieveClientIp(HttpServletRequest request) { 45 | String ip = request.getHeader("x-forwarded-for"); 46 | if (isUnAvailableIp(ip)) { 47 | ip = request.getHeader("Proxy-Client-IP"); 48 | } 49 | if (isUnAvailableIp(ip)) { 50 | ip = request.getHeader("WL-Proxy-Client-IP"); 51 | } 52 | if (isUnAvailableIp(ip)) { 53 | ip = request.getRemoteAddr(); 54 | } 55 | return ip; 56 | } 57 | 58 | private static boolean isUnAvailableIp(String ip) { 59 | return StringUtils.isEmpty(ip) || "unknown".equalsIgnoreCase(ip); 60 | } 61 | 62 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/PKCEUtilsTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import org.junit.jupiter.api.Test; 4 | 5 | import static org.junit.jupiter.api.Assertions.assertNotNull; 6 | import static org.junit.jupiter.api.Assertions.assertTrue; 7 | 8 | /** 9 | * 2023/10/16 22:53 10 | * 11 | * @author Shengzhao Li 12 | * @since 3.0.0 13 | */ 14 | class PKCEUtilsTest { 15 | 16 | 17 | @Test 18 | void generateCodeVerifier() { 19 | 20 | String verifier = PKCEUtils.generateCodeVerifier(); 21 | assertNotNull(verifier); 22 | assertTrue(verifier.length() >= 32); 23 | } 24 | 25 | 26 | @Test 27 | void generateCodeChallenge() { 28 | 29 | String verifier = PKCEUtils.generateCodeVerifier(); 30 | assertNotNull(verifier); 31 | 32 | String challenge = PKCEUtils.generateCodeChallenge(verifier); 33 | assertNotNull(challenge); 34 | 35 | } 36 | 37 | 38 | /** 39 | * PKCE 需要的参数生成测试 40 | * code_challenge_method : S256 (alg: SHA-256) 固定值 41 | * code_verifier : 随机生成且base64 encode的值 (推荐随机值至少32位) 42 | * code_challenge : 对 code_verifier 使用指定算法进行计算(digest)并base encode的值 43 | * 44 | */ 45 | @Test 46 | void pkceFlow() { 47 | 48 | // 1. 随机生成code_verifier 49 | String codeVerifier = PKCEUtils.generateCodeVerifier(); 50 | // System.out.println("code_verifier -> " + codeVerifier); 51 | 52 | //2. 按指定算法计算 挑战码 code_challenge 53 | String codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier); 54 | 55 | assertNotNull(codeChallenge); 56 | // System.out.println("code_challenge -> " + codeChallenge); 57 | } 58 | 59 | 60 | } -------------------------------------------------------------------------------- /src/test/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${spring.application.name} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/PKCEUtils.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import org.apache.commons.lang3.RandomStringUtils; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.Base64; 9 | 10 | /** 11 | * 2023/10/16 22:45 12 | *

13 | * PKCE tool: 14 | * 15 | * @author Shengzhao Li 16 | * @since 3.0.0 17 | */ 18 | public abstract class PKCEUtils { 19 | 20 | private static final String ALG = "SHA-256"; 21 | 22 | 23 | private PKCEUtils() { 24 | } 25 | 26 | /** 27 | * 随机生成32的 code_verifier 28 | * 29 | * @return code_verifier 30 | */ 31 | public static String generateCodeVerifier() { 32 | // 1. 随机生成code_verifier 33 | String codeVerifierVal = RandomStringUtils.random(32, true, true); 34 | //2. 对 code_verifier 进行base64 encode 35 | return Base64.getEncoder().encodeToString(codeVerifierVal.getBytes(StandardCharsets.UTF_8)); 36 | } 37 | 38 | /** 39 | * 根据指定的 code_verifier 计算 code_challenge 40 | * 41 | * @param codeVerifier code_verifier 42 | * @return code_challenge 43 | */ 44 | public static String generateCodeChallenge(String codeVerifier) { 45 | MessageDigest md; 46 | try { 47 | md = MessageDigest.getInstance(ALG); 48 | } catch (NoSuchAlgorithmException e) { 49 | throw new IllegalStateException("JDK not found alg: '" + ALG + "' ??", e); 50 | } 51 | byte[] digest = md.digest(codeVerifier.getBytes(StandardCharsets.US_ASCII)); 52 | return Base64.getUrlEncoder().withoutPadding().encodeToString(digest); 53 | } 54 | 55 | 56 | } 57 | -------------------------------------------------------------------------------- /others/database/initial_db.ddl: -------------------------------------------------------------------------------- 1 | -- ############### 2 | -- create MySQL database , if need create, cancel the comment 3 | -- ############### 4 | -- create database if not exists oauth2_boot default character set utf8; 5 | -- use oauth2_boot set default character = utf8; 6 | 7 | -- ############### 8 | -- grant privileges to oauth2/oauth2 9 | -- ############### 10 | -- GRANT ALL PRIVILEGES ON oauth2.* TO oauth2@localhost IDENTIFIED BY "oauth2"; 11 | 12 | -- ############### 13 | -- Domain: User 14 | -- ############### 15 | Drop table if exists user_; 16 | CREATE TABLE user_ 17 | ( 18 | id int(11) NOT NULL auto_increment, 19 | guid varchar(255) not null unique, 20 | create_time datetime, 21 | archived tinyint(1) default '0', 22 | updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 23 | username varchar(255) not null unique, 24 | password varchar(255) not null, 25 | enabled tinyint(1) default '1', 26 | phone varchar(255), 27 | email varchar(255), 28 | address varchar(255), 29 | nickname varchar(255), 30 | updated_at int(15) default 0, 31 | default_user tinyint(1) default '0', 32 | last_login_time datetime, 33 | PRIMARY KEY (id), 34 | index idx_username (username) 35 | ) ENGINE = InnoDB 36 | AUTO_INCREMENT = 20 37 | DEFAULT CHARSET = utf8; 38 | 39 | 40 | -- ############### 41 | -- Domain: Privilege 42 | -- ############### 43 | Drop table if exists user_privilege; 44 | CREATE TABLE user_privilege 45 | ( 46 | user_id int(11), 47 | privilege varchar(255), 48 | KEY user_id_index (user_id) 49 | ) ENGINE = InnoDB 50 | DEFAULT CHARSET = utf8; 51 | 52 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/DelegatingAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.web.authentication.AuthenticationConverter; 5 | import org.springframework.util.Assert; 6 | 7 | import java.util.Collections; 8 | import java.util.LinkedList; 9 | import java.util.List; 10 | import java.util.Map; 11 | 12 | /** 13 | * 2023/10/31 10:30 14 | * 15 | * @author Shengzhao Li 16 | * @see org.springframework.security.oauth2.server.authorization.web.authentication.DelegatingAuthenticationConverter 17 | * @since 3.0.0 18 | */ 19 | public final class DelegatingAuthenticationRestConverter implements AuthenticationRestConverter { 20 | 21 | private final List converters; 22 | 23 | /** 24 | * Constructs a {@code DelegatingAuthenticationConverter} using the provided parameters. 25 | * 26 | * @param converters a {@code List} of {@link AuthenticationConverter}(s) 27 | */ 28 | public DelegatingAuthenticationRestConverter(List converters) { 29 | Assert.notEmpty(converters, "converters cannot be empty"); 30 | this.converters = Collections.unmodifiableList(new LinkedList<>(converters)); 31 | } 32 | 33 | 34 | @Override 35 | public Authentication convert(Map parameters) { 36 | Assert.notNull(parameters, "parameters cannot be null"); 37 | for (AuthenticationRestConverter converter : this.converters) { 38 | Authentication authentication = converter.convert(parameters); 39 | if (authentication != null) { 40 | return authentication; 41 | } 42 | } 43 | return null; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/resources/templates/device_verification.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Device Login - Spring Security&OAuth2.1 10 | 11 | 12 | 13 |

14 |
15 |

Device Login

16 |
17 |
18 |
19 |
20 |
21 | 22 | 24 |

Please type device user code

25 |
26 | 27 |
28 | 29 |
30 |
31 | Cancel 32 |
33 |
34 |
35 |
36 |
37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/jdbc/UserRowMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure.jdbc; 13 | 14 | import com.monkeyk.sos.domain.user.User; 15 | import org.springframework.jdbc.core.RowMapper; 16 | 17 | import java.sql.ResultSet; 18 | import java.sql.SQLException; 19 | 20 | /** 21 | * table: user_ 22 | * 2015/11/16 23 | * 24 | * @author Shengzhao Li 25 | */ 26 | public class UserRowMapper implements RowMapper { 27 | 28 | 29 | public UserRowMapper() { 30 | } 31 | 32 | @Override 33 | public User mapRow(ResultSet rs, int i) throws SQLException { 34 | User user = new User(); 35 | 36 | user.id(rs.getInt("id")); 37 | user.guid(rs.getString("guid")); 38 | 39 | user.archived(rs.getBoolean("archived")); 40 | user.createTime(rs.getTimestamp("create_time").toLocalDateTime()); 41 | 42 | user.email(rs.getString("email")); 43 | user.phone(rs.getString("phone")); 44 | 45 | user.password(rs.getString("password")); 46 | user.username(rs.getString("username")); 47 | 48 | user.lastLoginTime(rs.getTimestamp("last_login_time")); 49 | //v3.0.0 added 50 | user.address(rs.getString("address")); 51 | user.nickname(rs.getString("nickname")); 52 | user.enabled(rs.getBoolean("enabled")); 53 | user.updatedAt(rs.getLong("updated_at")); 54 | 55 | return user; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | ${spring.application.name} 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n 12 | 13 | 14 | 15 | 16 | 17 | true 18 | 19 | 20 | logs/%d{yyyy-MM-dd}/sos-%i.log 21 | 10MB 22 | 15 23 | 25 | 26 | 27 | 28 | %d{yyyy-MM-dd HH:mm:ss} [%-5level] [%.80c{10}][%L] -%m%n 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/UserController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.monkeyk.sos.service.dto.UserFormDto; 4 | import com.monkeyk.sos.service.dto.UserOverviewDto; 5 | import com.monkeyk.sos.service.UserService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.validation.BindingResult; 10 | import org.springframework.web.bind.annotation.ModelAttribute; 11 | import org.springframework.web.bind.annotation.RequestMapping; 12 | import org.springframework.web.bind.annotation.RequestMethod; 13 | 14 | /** 15 | * @author Shengzhao Li 16 | */ 17 | @Controller 18 | @RequestMapping("/user/") 19 | public class UserController { 20 | 21 | 22 | @Autowired 23 | private UserService userService; 24 | 25 | @Autowired 26 | private UserFormDtoValidator validator; 27 | 28 | /** 29 | * @return View page 30 | */ 31 | @RequestMapping("overview") 32 | public String overview(UserOverviewDto overviewDto, Model model) { 33 | overviewDto = userService.loadUserOverviewDto(overviewDto); 34 | model.addAttribute("overviewDto", overviewDto); 35 | return "user_overview"; 36 | } 37 | 38 | 39 | @RequestMapping(value = "form/plus", method = RequestMethod.GET) 40 | public String showForm(Model model) { 41 | model.addAttribute("formDto", new UserFormDto()); 42 | return "user_form"; 43 | } 44 | 45 | 46 | @RequestMapping(value = "form/plus", method = RequestMethod.POST) 47 | public String submitRegisterClient(@ModelAttribute("formDto") UserFormDto formDto, BindingResult result) { 48 | validator.validate(formDto, result); 49 | if (result.hasErrors()) { 50 | return "user_form"; 51 | } 52 | userService.saveUser(formDto); 53 | return "redirect:../overview"; 54 | } 55 | 56 | 57 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/config/MVCConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.config; 2 | 3 | 4 | import com.monkeyk.sos.web.filter.CharacterEncodingIPFilter; 5 | import jakarta.servlet.Filter; 6 | import org.springframework.boot.web.servlet.FilterRegistrationBean; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.http.converter.HttpMessageConverter; 10 | import org.springframework.http.converter.StringHttpMessageConverter; 11 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 12 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; 13 | 14 | import java.nio.charset.StandardCharsets; 15 | import java.util.List; 16 | 17 | /** 18 | * 2018/1/30 19 | *

20 | * Spring MVC 扩展配置 21 | *

22 | * 23 | * @author Shengzhao Li 24 | * @since 2.0.0 25 | */ 26 | @Configuration 27 | public class MVCConfiguration implements WebMvcConfigurer { 28 | 29 | 30 | /** 31 | * 扩展拦截器 32 | */ 33 | @Override 34 | public void addInterceptors(InterceptorRegistry registry) { 35 | 36 | WebMvcConfigurer.super.addInterceptors(registry); 37 | } 38 | 39 | 40 | /** 41 | * 解决乱码问题 42 | * For UTF-8 43 | */ 44 | @Override 45 | public void configureMessageConverters(List> converters) { 46 | WebMvcConfigurer.super.configureMessageConverters(converters); 47 | converters.add(new StringHttpMessageConverter(StandardCharsets.UTF_8)); 48 | } 49 | 50 | 51 | /** 52 | * 字符编码配置 UTF-8 53 | */ 54 | @Bean 55 | public FilterRegistrationBean encodingFilter() { 56 | FilterRegistrationBean registrationBean = new FilterRegistrationBean<>(); 57 | registrationBean.setFilter(new CharacterEncodingIPFilter()); 58 | registrationBean.addUrlPatterns("/*"); 59 | //值越小越靠前 60 | registrationBean.setOrder(1); 61 | return registrationBean; 62 | } 63 | 64 | 65 | } 66 | -------------------------------------------------------------------------------- /src/main/resources/templates/user_overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | User Overview - Spring Security&OAuth2.1 10 | 11 | 12 | 13 | 14 | Home 15 | 16 |

User Overview

17 | 18 |
19 | Add User 20 |
21 |
22 |
23 | 25 |
26 | 27 |  Total: [[${overviewDto.size}]] 28 |
29 |
30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 |
UsernamePrivilegeEnabledPhoneEmailNicknameAddressCreateTime
56 | 57 |
58 | 59 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/UserJsonDto.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import com.monkeyk.sos.domain.user.Privilege; 4 | import com.monkeyk.sos.domain.user.User; 5 | 6 | import java.io.Serial; 7 | import java.io.Serializable; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | 11 | /** 12 | * @author Shengzhao Li 13 | */ 14 | public class UserJsonDto implements Serializable { 15 | 16 | @Serial 17 | private static final long serialVersionUID = -704681024783524371L; 18 | 19 | private String guid; 20 | private boolean archived; 21 | 22 | private String username; 23 | private String phone; 24 | private String email; 25 | 26 | private List privileges = new ArrayList<>(); 27 | 28 | public UserJsonDto() { 29 | } 30 | 31 | public UserJsonDto(User user) { 32 | this.guid = user.guid(); 33 | this.archived = user.archived(); 34 | this.username = user.username(); 35 | 36 | this.phone = user.phone(); 37 | this.email = user.email(); 38 | 39 | final List privilegeList = user.privileges(); 40 | for (Privilege privilege : privilegeList) { 41 | this.privileges.add(privilege.name()); 42 | } 43 | } 44 | 45 | public boolean isArchived() { 46 | return archived; 47 | } 48 | 49 | public void setArchived(boolean archived) { 50 | this.archived = archived; 51 | } 52 | 53 | public String getGuid() { 54 | return guid; 55 | } 56 | 57 | public void setGuid(String guid) { 58 | this.guid = guid; 59 | } 60 | 61 | public String getUsername() { 62 | return username; 63 | } 64 | 65 | public void setUsername(String username) { 66 | this.username = username; 67 | } 68 | 69 | public String getPhone() { 70 | return phone; 71 | } 72 | 73 | public void setPhone(String phone) { 74 | this.phone = phone; 75 | } 76 | 77 | public String getEmail() { 78 | return email; 79 | } 80 | 81 | public void setEmail(String email) { 82 | this.email = email; 83 | } 84 | 85 | public List getPrivileges() { 86 | return privileges; 87 | } 88 | 89 | public void setPrivileges(List privileges) { 90 | this.privileges = privileges; 91 | } 92 | } -------------------------------------------------------------------------------- /src/main/resources/jwks.json: -------------------------------------------------------------------------------- 1 | { 2 | "keys": [ 3 | { 4 | "kty": "EC", 5 | "d": "X_gLHsJlSyK4gT_qeinb2gV7enJ1_2wq_Kxk-h3f-Mo", 6 | "crv": "P-256", 7 | "kid": "sos-ecc-kid1", 8 | "key_ops": [ 9 | "sign", 10 | "deriveKey", 11 | "decrypt", 12 | "encrypt", 13 | "verify" 14 | ], 15 | "x": "UyCuPXhC0_KLRqfWPNDU4ZljSx7lQ_vP7VbYDiOZmsk", 16 | "y": "2HuQhn3bfkmYiB6BLQKlN8tkI8awkeOiKaNk1cu06ow", 17 | "alg": "ES256" 18 | }, 19 | { 20 | "p": "1IKQCCAPhMgxUbgGa9Yjsowt3Q7rUjF68GBW0BF3QaY6zdrt1tGRLd_wVGq4uLBlb0jUUV591YOdYQHYpqgjozMfmpSG6UxikUGzzNihB0-9pczWxGe03hbLr5M3ueDIEBh81_aigSwnUGTTYCZhUPRewlJSkPg2SlXWfrB8tYU", 21 | "kty": "RSA", 22 | "q": "13hSjzOO8BjVbcjfa2QsyDMVLcclagFLeaTejBZG_ZDRpvvq6zL9MyghGc5q-qlMxZCZwci8WOCyPwKfvB7Ca_3fdKVL0U7VSyTuXTRX1OCpxoOj6IbxzuzWeFEAwEkL6PeRPYFz-bgWd955NdCCS5rL11SBQneIIavtYTKiKkk", 23 | "d": "F6t-8VhYR5Sy_7rNo5S75wxLgxlKc_WMqGsd39xcebdCY7MQnFxHq0_GUOq-RQKmhqydJXKdC3rElopxeojUmbX1mlnznjlv8Yu5JTVq5kMELuzl0-MyqeyHCM027p_-gjShNSLhhR3I8_GUZGvt-6q6H4yaGGGx9t1bbAjnLYQK-4zzl2VcNqHETIDYwhi626FGy1uZCHIDsojeVgW7HQAx26HAGBIkPMbiFCINLQRf-cOsEX4ksKfrgbH5QOG16yZObYHy1Ulx0HKgP_GaaqliZ6C-6-w05Umv6V_KY9qQiehFAFVRJ82lZtQ3HV1Ivoxi4U-ptYSaMGkDOqij2Q", 24 | "e": "AQAB", 25 | "kid": "sos-rsa-kid2", 26 | "key_ops": [ 27 | "deriveKey", 28 | "verify", 29 | "encrypt", 30 | "decrypt", 31 | "sign" 32 | ], 33 | "qi": "jetZOG6EMEDAoeAy8RiJxHFnFJMOqGULd5wkPwAi6LV9wt8dgdxj_rocK0a4ksSfEu5EFeuJ8lPVpBwMJhZh5j2YJvmVzC7FxhH2sQ3FD-tu6hwU9IhnRLm2CeEaSG92upWUoZCRnLwVpKamOVJjJAk19TmL7FUGt93a3Gemb88", 34 | "dp": "ry5mH1yWjmYdSflCydiAGuq10BpBYMNLTiaMyf7r6WFn7lTAZariXAfT7TMAzbcUFzXZWK5lWwKhVNuZxmCq6Bj3v40a3e1K_-VCm-YkcIuKkcgXb1byYXY3OKhKct9a7PHS0JEPCx7j1cEYApYA-SRJjTUhvUHwNz0lkdBZLaU", 35 | "alg": "RS256", 36 | "dq": "Wa4lxp5x9rKPWnNJsjvue6DvRq9lfhpt3IJncizvfSgianrdiukdA4bHSCNm2U9Pucb2h_ZRljhnV9xyuWygBSyULcuCo-pI0k7buwVHLT4Yy5wMw4Iu8K4Ykdk9E8sTXvJzjALuT1h0WY3KK0DOikMyZjww1IZFraYOVe8qGak", 37 | "n": "st2IswiZyQXHy86KBYQdEYv3sAfWpyx-e4o0Dcqvpck0E1FpZfVcFzbLy9B7YHvXv1SseVcg93iiNYgGlPDeZxPllz4-oIisDvSmEJdAidhqQxxpMeSjeQzvVu4CKjGFG9jA68pTm-KDia3Y516b4tPyKhHGIUZq2yJrNIs2QjTikYbn5AxAQ244cDPTsuEV5yqdOdyWvdlrn4WSFLiPt31MboT6et7Hmm90fwbMDSaWWb2XNo2gOnzWFwlNO2s8zK_Z1IWhmreb_XH5mW9xirrT03nbnLTLcmLtZYHFKjP55zRFDgKsXeo9BQNG3dkCsWz0N8pURaN6cuXYoYGU7Q" 38 | } 39 | ] 40 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/AbstractDomain.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain; 2 | 3 | import com.monkeyk.sos.domain.shared.GuidGenerator; 4 | import com.monkeyk.sos.infrastructure.DateUtils; 5 | 6 | import java.io.Serial; 7 | import java.io.Serializable; 8 | import java.time.LocalDateTime; 9 | 10 | /** 11 | * @author Shengzhao Li 12 | */ 13 | public abstract class AbstractDomain implements Serializable { 14 | 15 | @Serial 16 | private static final long serialVersionUID = 6569365774429340632L; 17 | /** 18 | * Database id 19 | */ 20 | protected long id; 21 | 22 | protected boolean archived; 23 | /** 24 | * Domain business guid. 25 | */ 26 | protected String guid = GuidGenerator.generate(); 27 | 28 | /** 29 | * The domain create time. 30 | */ 31 | protected LocalDateTime createTime = DateUtils.now(); 32 | 33 | public AbstractDomain() { 34 | } 35 | 36 | public long id() { 37 | return id; 38 | } 39 | 40 | public void id(long id) { 41 | this.id = id; 42 | } 43 | 44 | public boolean archived() { 45 | return archived; 46 | } 47 | 48 | public AbstractDomain archived(boolean archived) { 49 | this.archived = archived; 50 | return this; 51 | } 52 | 53 | public String guid() { 54 | return guid; 55 | } 56 | 57 | public void guid(String guid) { 58 | this.guid = guid; 59 | } 60 | 61 | public LocalDateTime createTime() { 62 | return createTime; 63 | } 64 | 65 | 66 | @Override 67 | public boolean equals(Object o) { 68 | if (this == o) { 69 | return true; 70 | } 71 | if (!(o instanceof AbstractDomain)) { 72 | return false; 73 | } 74 | AbstractDomain that = (AbstractDomain) o; 75 | return guid.equals(that.guid); 76 | } 77 | 78 | @Override 79 | public int hashCode() { 80 | return guid.hashCode(); 81 | } 82 | 83 | //For subclass override it 84 | public void saveOrUpdate() { 85 | } 86 | 87 | @Override 88 | public String toString() { 89 | final StringBuilder sb = new StringBuilder(); 90 | sb.append("{id=").append(id); 91 | sb.append(", archived=").append(archived); 92 | sb.append(", guid='").append(guid).append('\''); 93 | sb.append(", createTime=").append(createTime); 94 | sb.append('}'); 95 | return sb.toString(); 96 | } 97 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/UserFormDtoValidator.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.monkeyk.sos.service.dto.UserFormDto; 4 | import com.monkeyk.sos.domain.user.Privilege; 5 | import com.monkeyk.sos.service.UserService; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | import org.springframework.validation.Errors; 10 | import org.springframework.validation.Validator; 11 | 12 | import java.util.List; 13 | 14 | /** 15 | * 2016/3/25 16 | * 17 | * @author Shengzhao Li 18 | */ 19 | @Component 20 | public class UserFormDtoValidator implements Validator { 21 | 22 | 23 | @Autowired 24 | private UserService userService; 25 | 26 | @Override 27 | public boolean supports(Class clazz) { 28 | return UserFormDto.class.equals(clazz); 29 | } 30 | 31 | @Override 32 | public void validate(Object target, Errors errors) { 33 | UserFormDto formDto = (UserFormDto) target; 34 | 35 | validateUsername(errors, formDto); 36 | validatePassword(errors, formDto); 37 | validatePrivileges(errors, formDto); 38 | } 39 | 40 | private void validatePrivileges(Errors errors, UserFormDto formDto) { 41 | final List privileges = formDto.getPrivileges(); 42 | if (privileges == null || privileges.isEmpty()) { 43 | errors.rejectValue("privileges", null, "Privileges is required"); 44 | } 45 | } 46 | 47 | private void validatePassword(Errors errors, UserFormDto formDto) { 48 | final String password = formDto.getPassword(); 49 | if (StringUtils.isEmpty(password)) { 50 | errors.rejectValue("password", null, "Password is required"); 51 | } else if (password.length() < 10) { 52 | errors.rejectValue("password", null, "Password length must be >= 10"); 53 | } 54 | } 55 | 56 | private void validateUsername(Errors errors, UserFormDto formDto) { 57 | final String username = formDto.getUsername(); 58 | if (StringUtils.isEmpty(username)) { 59 | errors.rejectValue("username", null, "Username is required"); 60 | return; 61 | } 62 | 63 | boolean existed = userService.isExistedUsername(username); 64 | if (existed) { 65 | errors.rejectValue("username", null, "Username already existed"); 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/OAuth2DeviceCodeAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 6 | import org.springframework.security.oauth2.core.OAuth2ErrorCodes; 7 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 8 | import org.springframework.security.oauth2.server.authorization.authentication.OAuth2DeviceCodeAuthenticationToken; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * 2023/10/31 10:33 16 | * 17 | * @author Shengzhao Li 18 | * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2DeviceCodeAuthenticationConverter 19 | * @since 3.0.0 20 | */ 21 | public final class OAuth2DeviceCodeAuthenticationRestConverter extends AbstractAuthenticationRestConverter { 22 | 23 | 24 | @Override 25 | public Authentication convert(Map parameters) { 26 | // grant_type (REQUIRED) 27 | String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); 28 | if (!AuthorizationGrantType.DEVICE_CODE.getValue().equals(grantType)) { 29 | return null; 30 | } 31 | 32 | Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); 33 | 34 | // MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); 35 | 36 | // device_code (REQUIRED) 37 | String deviceCode = parameters.get(OAuth2ParameterNames.DEVICE_CODE); 38 | if (!StringUtils.hasText(deviceCode)) { 39 | throwError( 40 | OAuth2ErrorCodes.INVALID_REQUEST, 41 | OAuth2ParameterNames.DEVICE_CODE, 42 | ACCESS_TOKEN_REQUEST_ERROR_URI); 43 | } 44 | 45 | Map additionalParameters = new HashMap<>(); 46 | parameters.forEach((key, value) -> { 47 | if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && 48 | !key.equals(OAuth2ParameterNames.CLIENT_ID) && 49 | !key.equals(OAuth2ParameterNames.DEVICE_CODE)) { 50 | additionalParameters.put(key, value); 51 | } 52 | }); 53 | 54 | return new OAuth2DeviceCodeAuthenticationToken(deviceCode, clientPrincipal, additionalParameters); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/impl/OauthServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.impl; 2 | 3 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 4 | import com.monkeyk.sos.domain.oauth.OauthRepository; 5 | import com.monkeyk.sos.service.OauthService; 6 | import com.monkeyk.sos.service.dto.OauthClientDetailsDto; 7 | import com.monkeyk.sos.web.WebUtils; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.List; 14 | 15 | /** 16 | * @author Shengzhao Li 17 | */ 18 | @Service("oauthService") 19 | public class OauthServiceImpl implements OauthService { 20 | 21 | 22 | private static final Logger LOG = LoggerFactory.getLogger(OauthServiceImpl.class); 23 | 24 | @Autowired 25 | private OauthRepository oauthRepository; 26 | 27 | @Override 28 | // @Transactional(readOnly = true) 29 | public OauthClientDetails loadOauthClientDetails(String clientId) { 30 | return oauthRepository.findOauthClientDetails(clientId); 31 | } 32 | 33 | @Override 34 | // @Transactional(readOnly = true) 35 | public List loadAllOauthClientDetailsDtos() { 36 | List clientDetailses = oauthRepository.findAllOauthClientDetails(); 37 | return OauthClientDetailsDto.toDtos(clientDetailses); 38 | } 39 | 40 | @Override 41 | // @Transactional(propagation = Propagation.REQUIRED) 42 | public void archiveOauthClientDetails(String clientId) { 43 | oauthRepository.updateOauthClientDetailsArchive(clientId, true); 44 | if (LOG.isDebugEnabled()) { 45 | LOG.debug("{}|Update OauthClientDetails(clientId: {}) archive = true", WebUtils.getIp(), clientId); 46 | } 47 | } 48 | 49 | @Override 50 | // @Transactional(readOnly = true) 51 | public OauthClientDetailsDto loadOauthClientDetailsDto(String clientId) { 52 | final OauthClientDetails oauthClientDetails = oauthRepository.findOauthClientDetails(clientId); 53 | return oauthClientDetails != null ? new OauthClientDetailsDto(oauthClientDetails) : null; 54 | } 55 | 56 | @Override 57 | // @Transactional(propagation = Propagation.REQUIRED) 58 | public void registerClientDetails(OauthClientDetailsDto formDto) { 59 | OauthClientDetails clientDetails = formDto.createDomain(); 60 | oauthRepository.saveOauthClientDetails(clientDetails); 61 | if (LOG.isDebugEnabled()) { 62 | LOG.debug("{}|Save OauthClientDetails: {}", WebUtils.getIp(), clientDetails); 63 | } 64 | } 65 | 66 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/business/AbstractInlineAccessTokenInvokerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 4 | import com.monkeyk.sos.domain.oauth.OauthRepository; 5 | import com.monkeyk.sos.domain.shared.GuidGenerator; 6 | import com.monkeyk.sos.domain.user.Privilege; 7 | import com.monkeyk.sos.domain.user.User; 8 | import com.monkeyk.sos.domain.user.UserRepository; 9 | import com.monkeyk.sos.infrastructure.AbstractRepositoryTest; 10 | import com.monkeyk.sos.infrastructure.PasswordHandler; 11 | import org.apache.commons.lang3.RandomStringUtils; 12 | import org.springframework.beans.factory.annotation.Autowired; 13 | 14 | /** 15 | * 2019/7/6 16 | * 17 | * @author Shengzhao Li 18 | */ 19 | public abstract class AbstractInlineAccessTokenInvokerTest extends AbstractRepositoryTest { 20 | 21 | 22 | @Autowired 23 | OauthRepository oauthRepository; 24 | 25 | @Autowired 26 | UserRepository userRepository; 27 | 28 | 29 | String clientId = "client_id_" + RandomStringUtils.random(6, true, true); 30 | String clientSecret = "client_secret_" + RandomStringUtils.random(6, true, true); 31 | 32 | 33 | String username = "user_" + RandomStringUtils.random(6, true, true); 34 | String password = "password_" + RandomStringUtils.random(6, true, true); 35 | 36 | 37 | User createUser() { 38 | 39 | 40 | User user = new User(username, PasswordHandler.encode(password), "13300001111", "test@ssss.com"); 41 | user.privileges().add(Privilege.UNITY); 42 | user.privileges().add(Privilege.USER); 43 | user.privileges().add(Privilege.MOBILE); 44 | 45 | userRepository.saveUser(user); 46 | 47 | return user; 48 | } 49 | 50 | 51 | OauthClientDetails createClientDetails() { 52 | OauthClientDetails clientDetails = new OauthClientDetails(); 53 | clientDetails.clientId(clientId) 54 | .clientName("TestClient") 55 | .id(GuidGenerator.generateNumber()) 56 | .clientSecret(PasswordHandler.encode(clientSecret)) 57 | .authorizationGrantTypes(grantTypes()) 58 | .clientAuthenticationMethods("client_secret_post") 59 | .clientSettings("") 60 | .tokenSettings("") 61 | .scopes("openid"); 62 | // .accessTokenValidity(200) 63 | // .resourceIds(RESOURCE_ID); 64 | 65 | 66 | oauthRepository.saveOauthClientDetails(clientDetails); 67 | return clientDetails; 68 | } 69 | 70 | String grantTypes() { 71 | return "authorization_code,password,client_credentials,refresh_token"; 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/OAuth2ClientCredentialsAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 6 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 7 | import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientCredentialsAuthenticationToken; 8 | import org.springframework.util.StringUtils; 9 | 10 | import java.util.*; 11 | 12 | /** 13 | * 2023/10/31 10:33 14 | * 15 | * @author Shengzhao Li 16 | * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter 17 | * @since 3.0.0 18 | */ 19 | public final class OAuth2ClientCredentialsAuthenticationRestConverter extends AbstractAuthenticationRestConverter { 20 | 21 | 22 | @Override 23 | public Authentication convert(Map parameters) { 24 | // grant_type (REQUIRED) 25 | String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); 26 | if (!AuthorizationGrantType.CLIENT_CREDENTIALS.getValue().equals(grantType)) { 27 | return null; 28 | } 29 | 30 | Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); 31 | 32 | // MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); 33 | 34 | // scope (OPTIONAL) 35 | String scope = parameters.get(OAuth2ParameterNames.SCOPE); 36 | // if (StringUtils.hasText(scope) && 37 | // parameters.get(OAuth2ParameterNames.SCOPE).size() != 1) { 38 | // throwError( 39 | // OAuth2ErrorCodes.INVALID_REQUEST, 40 | // OAuth2ParameterNames.SCOPE, 41 | // ACCESS_TOKEN_REQUEST_ERROR_URI); 42 | // } 43 | Set requestedScopes = null; 44 | if (StringUtils.hasText(scope)) { 45 | requestedScopes = new HashSet<>( 46 | Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); 47 | } 48 | 49 | Map additionalParameters = new HashMap<>(); 50 | parameters.forEach((key, value) -> { 51 | if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && 52 | !key.equals(OAuth2ParameterNames.SCOPE)) { 53 | additionalParameters.put(key, value); 54 | } 55 | }); 56 | 57 | return new OAuth2ClientCredentialsAuthenticationToken( 58 | clientPrincipal, requestedScopes, additionalParameters); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/context/SOSContextHolder.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.context; 2 | 3 | import com.monkeyk.sos.web.WebUtils; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.BeansException; 7 | import org.springframework.beans.factory.BeanFactory; 8 | import org.springframework.beans.factory.BeanFactoryAware; 9 | import org.springframework.beans.factory.InitializingBean; 10 | import org.springframework.beans.factory.annotation.Value; 11 | import org.springframework.util.Assert; 12 | 13 | /** 14 | * 2019/7/6 15 | *

16 | *

17 | * Spring ApplicationContext Holder, 18 | * get Spring bean from beanFactory 19 | * 20 | * @author Shengzhao Li 21 | * @since 2.0.1 22 | */ 23 | public class SOSContextHolder implements BeanFactoryAware, InitializingBean { 24 | 25 | private static final Logger LOG = LoggerFactory.getLogger(SOSContextHolder.class); 26 | 27 | /** 28 | * @since 2.1.0 29 | */ 30 | private static BeanFactory beanFactory; 31 | 32 | 33 | /** 34 | * @since 2.1.0 35 | */ 36 | @Value("${spring.application.name:spring-oauth-server}") 37 | private String applicationName; 38 | 39 | 40 | public SOSContextHolder() { 41 | } 42 | 43 | 44 | @Override 45 | public void setBeanFactory(BeanFactory beanFactory) throws BeansException { 46 | SOSContextHolder.beanFactory = beanFactory; 47 | } 48 | 49 | 50 | /** 51 | * Get Spring Bean by clazz. 52 | * 53 | * @param clazz Class 54 | * @param class type 55 | * @return Bean instance 56 | */ 57 | public static T getBean(Class clazz) { 58 | if (beanFactory == null) { 59 | throw new IllegalStateException("beanFactory is null"); 60 | } 61 | return beanFactory.getBean(clazz); 62 | } 63 | 64 | 65 | /** 66 | * Get Spring Bean by beanId. 67 | * 68 | * @param beanId beanId 69 | * @param class type 70 | * @return Bean instance 71 | */ 72 | @SuppressWarnings("unchecked") 73 | public static T getBean(String beanId) { 74 | if (beanFactory == null) { 75 | throw new IllegalStateException("beanFactory is null"); 76 | } 77 | return (T) beanFactory.getBean(beanId); 78 | } 79 | 80 | 81 | @Override 82 | public void afterPropertiesSet() throws Exception { 83 | Assert.notNull(beanFactory, "beanFactory is null"); 84 | 85 | // if (LOG.isDebugEnabled()) { 86 | // TokenStore tokenStore = getBean(TokenStore.class); 87 | // LOG.debug("{} use tokenStore: {}", this.applicationName, tokenStore); 88 | // } 89 | 90 | if (LOG.isInfoEnabled()) { 91 | LOG.info("{} context initialized, version: {}", this.applicationName, WebUtils.VERSION); 92 | } 93 | } 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthClientDetailsRowMapper.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure.jdbc; 13 | 14 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 15 | import org.springframework.jdbc.core.RowMapper; 16 | 17 | import java.sql.ResultSet; 18 | import java.sql.SQLException; 19 | import java.sql.Timestamp; 20 | 21 | /** 22 | * table: oauth2_registered_client 23 | * 2015/11/16 24 | * 25 | * @author Shengzhao Li 26 | */ 27 | public class OauthClientDetailsRowMapper implements RowMapper { 28 | 29 | 30 | public OauthClientDetailsRowMapper() { 31 | } 32 | 33 | @Override 34 | public OauthClientDetails mapRow(ResultSet rs, int i) throws SQLException { 35 | OauthClientDetails clientDetails = new OauthClientDetails(); 36 | 37 | clientDetails.id(rs.getString("id")); 38 | clientDetails.archived(rs.getBoolean("archived")); 39 | clientDetails.createTime(rs.getTimestamp("create_time").toLocalDateTime()); 40 | clientDetails.clientId(rs.getString("client_id")); 41 | clientDetails.clientIdIssuedAt(rs.getTimestamp("client_id_issued_at").toInstant()); 42 | clientDetails.clientName(rs.getString("client_name")); 43 | 44 | clientDetails.clientAuthenticationMethods(rs.getString("client_authentication_methods")); 45 | clientDetails.clientSecret(rs.getString("client_secret")); 46 | 47 | clientDetails.scopes(rs.getString("scopes")); 48 | clientDetails.authorizationGrantTypes(rs.getString("authorization_grant_types")); 49 | clientDetails.redirectUris(rs.getString("redirect_uris")); 50 | 51 | clientDetails.postLogoutRedirectUris(rs.getString("post_logout_redirect_uris")); 52 | clientDetails.clientSettings(rs.getString("client_settings")); 53 | clientDetails.tokenSettings(rs.getString("token_settings")); 54 | 55 | Timestamp secretExpiresAt = rs.getTimestamp("client_secret_expires_at"); 56 | if (secretExpiresAt != null) { 57 | clientDetails.clientSecretExpiresAt(secretExpiresAt.toInstant()); 58 | } 59 | 60 | return clientDetails; 61 | } 62 | 63 | 64 | private Integer getInteger(ResultSet rs, String columnName) throws SQLException { 65 | final Object object = rs.getObject(columnName); 66 | if (object != null) { 67 | return (Integer) object; 68 | } 69 | return null; 70 | } 71 | 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/OAuth2RefreshTokenAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 6 | import org.springframework.security.oauth2.core.OAuth2ErrorCodes; 7 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 8 | import org.springframework.security.oauth2.server.authorization.authentication.OAuth2RefreshTokenAuthenticationToken; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.util.*; 12 | 13 | /** 14 | * 2023/10/31 10:33 15 | * 16 | * @author Shengzhao Li 17 | * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter 18 | * @since 3.0.0 19 | */ 20 | public final class OAuth2RefreshTokenAuthenticationRestConverter extends AbstractAuthenticationRestConverter { 21 | 22 | 23 | @Override 24 | public Authentication convert(Map parameters) { 25 | // grant_type (REQUIRED) 26 | String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); 27 | if (!AuthorizationGrantType.REFRESH_TOKEN.getValue().equals(grantType)) { 28 | return null; 29 | } 30 | 31 | Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); 32 | 33 | // MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); 34 | 35 | // refresh_token (REQUIRED) 36 | String refreshToken = parameters.get(OAuth2ParameterNames.REFRESH_TOKEN); 37 | if (!StringUtils.hasText(refreshToken)) { 38 | throwError( 39 | OAuth2ErrorCodes.INVALID_REQUEST, 40 | OAuth2ParameterNames.REFRESH_TOKEN, 41 | ACCESS_TOKEN_REQUEST_ERROR_URI); 42 | } 43 | 44 | // scope (OPTIONAL) 45 | String scope = parameters.get(OAuth2ParameterNames.SCOPE); 46 | // if (!StringUtils.hasText(scope)) { 47 | // throwError( 48 | // OAuth2ErrorCodes.INVALID_REQUEST, 49 | // OAuth2ParameterNames.SCOPE, 50 | // ACCESS_TOKEN_REQUEST_ERROR_URI); 51 | // } 52 | Set requestedScopes = null; 53 | if (StringUtils.hasText(scope)) { 54 | requestedScopes = new HashSet<>( 55 | Arrays.asList(StringUtils.delimitedListToStringArray(scope, " "))); 56 | } 57 | 58 | Map additionalParameters = new HashMap<>(); 59 | parameters.forEach((key, value) -> { 60 | if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && 61 | !key.equals(OAuth2ParameterNames.REFRESH_TOKEN) && 62 | !key.equals(OAuth2ParameterNames.SCOPE)) { 63 | additionalParameters.put(key, value); 64 | } 65 | }); 66 | 67 | return new OAuth2RefreshTokenAuthenticationToken( 68 | refreshToken, clientPrincipal, requestedScopes, additionalParameters); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/business/ClientCredentialsInlineAccessTokenInvokerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | import com.monkeyk.sos.service.dto.AccessTokenDto; 4 | import org.junit.jupiter.api.Disabled; 5 | import org.junit.jupiter.api.Test; 6 | 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static org.junit.jupiter.api.Assertions.assertNotNull; 12 | import static org.junit.jupiter.api.Assertions.assertThrows; 13 | 14 | 15 | /** 16 | * 2019/7/6 17 | * 18 | * @author Shengzhao Li 19 | */ 20 | public class ClientCredentialsInlineAccessTokenInvokerTest extends AbstractInlineAccessTokenInvokerTest { 21 | 22 | 23 | @Test 24 | @Disabled 25 | public void invokeNormal() { 26 | 27 | createClientDetails(); 28 | 29 | Map params = new HashMap<>(); 30 | params.put("client_id", clientId); 31 | params.put("client_secret", clientSecret); 32 | params.put("grant_type", "client_credentials"); 33 | params.put("scope", "read"); 34 | 35 | 36 | ClientCredentialsInlineAccessTokenInvoker accessTokenInvoker = new ClientCredentialsInlineAccessTokenInvoker(); 37 | final AccessTokenDto accessTokenDto = accessTokenInvoker.invoke(params); 38 | 39 | assertNotNull(accessTokenDto); 40 | assertNotNull(accessTokenDto.getAccessToken()); 41 | 42 | // System.out.println(accessTokenDto); 43 | 44 | } 45 | 46 | // @Test(expected = NoSuchClientException.class) 47 | @Test 48 | public void invalidClientId() { 49 | 50 | createClientDetails(); 51 | 52 | Map params = new HashMap<>(); 53 | params.put("client_id", clientId + "ssoso"); 54 | params.put("client_secret", clientSecret); 55 | params.put("grant_type", "client_credentials"); 56 | params.put("scope", "read"); 57 | 58 | 59 | ClientCredentialsInlineAccessTokenInvoker accessTokenInvoker = new ClientCredentialsInlineAccessTokenInvoker(); 60 | // AccessTokenDto accessTokenDto; 61 | assertThrows(Exception.class, () -> { 62 | accessTokenInvoker.invoke(params); 63 | }); 64 | // final AccessTokenDto accessTokenDto = accessTokenInvoker.invoke(params); 65 | 66 | // assertNotNull(accessTokenDto); 67 | // assertNotNull(accessTokenDto.getAccessToken()); 68 | 69 | // System.out.println(accessTokenDto); 70 | 71 | } 72 | 73 | @Test() 74 | @Disabled 75 | public void invalidClientSecret() { 76 | 77 | createClientDetails(); 78 | 79 | Map params = new HashMap<>(); 80 | params.put("client_id", clientId); 81 | params.put("client_secret", clientSecret + "sooe"); 82 | params.put("grant_type", "client_credentials"); 83 | params.put("scope", "read"); 84 | 85 | 86 | ClientCredentialsInlineAccessTokenInvoker accessTokenInvoker = new ClientCredentialsInlineAccessTokenInvoker(); 87 | final AccessTokenDto accessTokenDto = accessTokenInvoker.invoke(params); 88 | 89 | assertNotNull(accessTokenDto); 90 | assertNotNull(accessTokenDto.getAccessToken()); 91 | 92 | // System.out.println(accessTokenDto); 93 | 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/authentication/OAuth2AuthorizationCodeAuthenticationRestConverter.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.authentication; 2 | 3 | import org.springframework.security.core.Authentication; 4 | import org.springframework.security.core.context.SecurityContextHolder; 5 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 6 | import org.springframework.security.oauth2.core.OAuth2ErrorCodes; 7 | import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames; 8 | import org.springframework.security.oauth2.server.authorization.authentication.OAuth2AuthorizationCodeAuthenticationToken; 9 | import org.springframework.util.StringUtils; 10 | 11 | import java.util.HashMap; 12 | import java.util.Map; 13 | 14 | /** 15 | * 2023/10/31 10:33 16 | * 17 | * @author Shengzhao Li 18 | * @see org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter 19 | * @since 3.0.0 20 | */ 21 | public final class OAuth2AuthorizationCodeAuthenticationRestConverter extends AbstractAuthenticationRestConverter { 22 | 23 | 24 | @Override 25 | public Authentication convert(Map parameters) { 26 | // grant_type (REQUIRED) 27 | String grantType = parameters.get(OAuth2ParameterNames.GRANT_TYPE); 28 | if (!AuthorizationGrantType.AUTHORIZATION_CODE.getValue().equals(grantType)) { 29 | return null; 30 | } 31 | 32 | Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication(); 33 | 34 | // MultiValueMap parameters = OAuth2EndpointUtils.getParameters(request); 35 | 36 | // code (REQUIRED) 37 | String code = parameters.get(OAuth2ParameterNames.CODE); 38 | if (!StringUtils.hasText(code)) { 39 | throwError( 40 | OAuth2ErrorCodes.INVALID_REQUEST, 41 | OAuth2ParameterNames.CODE, 42 | ACCESS_TOKEN_REQUEST_ERROR_URI); 43 | } 44 | 45 | // redirect_uri (REQUIRED) 46 | // Required only if the "redirect_uri" parameter was included in the authorization request 47 | String redirectUri = parameters.get(OAuth2ParameterNames.REDIRECT_URI); 48 | if (!StringUtils.hasText(redirectUri)) { 49 | throwError( 50 | OAuth2ErrorCodes.INVALID_REQUEST, 51 | OAuth2ParameterNames.REDIRECT_URI, 52 | ACCESS_TOKEN_REQUEST_ERROR_URI); 53 | } 54 | 55 | Map additionalParameters = new HashMap<>(); 56 | parameters.forEach((key, value) -> { 57 | if (!key.equals(OAuth2ParameterNames.GRANT_TYPE) && 58 | !key.equals(OAuth2ParameterNames.CLIENT_ID) && 59 | !key.equals(OAuth2ParameterNames.CODE) && 60 | !key.equals(OAuth2ParameterNames.REDIRECT_URI)) { 61 | // additionalParameters.put(key, (value.size() == 1) ? value.get(0) : value.toArray(new String[0])); 62 | additionalParameters.put(key, value); 63 | } 64 | }); 65 | 66 | return new OAuth2AuthorizationCodeAuthenticationToken( 67 | code, clientPrincipal, redirectUri, additionalParameters); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbcTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure.jdbc; 13 | 14 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 15 | import com.monkeyk.sos.domain.oauth.OauthRepository; 16 | import com.monkeyk.sos.domain.shared.GuidGenerator; 17 | import com.monkeyk.sos.infrastructure.AbstractRepositoryTest; 18 | 19 | import com.monkeyk.sos.infrastructure.SettingsUtils; 20 | import org.junit.jupiter.api.Test; 21 | import org.springframework.beans.factory.annotation.Autowired; 22 | import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; 23 | import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; 24 | 25 | 26 | import java.util.List; 27 | 28 | import static org.junit.jupiter.api.Assertions.*; 29 | 30 | 31 | /* 32 | * @author Shengzhao Li 33 | */ 34 | public class OauthRepositoryJdbcTest extends AbstractRepositoryTest { 35 | 36 | 37 | @Autowired 38 | private OauthRepository oauthRepositoryMyBatis; 39 | 40 | 41 | @Test 42 | public void findOauthClientDetails() { 43 | OauthClientDetails oauthClientDetails = oauthRepositoryMyBatis.findOauthClientDetails("unity-client"); 44 | assertNull(oauthClientDetails); 45 | 46 | } 47 | 48 | 49 | @Test 50 | public void saveOauthClientDetails() { 51 | 52 | final String clientId = GuidGenerator.generate(); 53 | 54 | OauthClientDetails clientDetails = new OauthClientDetails() 55 | .id(GuidGenerator.generate()) 56 | .clientName("Test-client") 57 | .clientAuthenticationMethods("client_secret_post") 58 | .authorizationGrantTypes("authorization_code") 59 | .scopes("openid") 60 | .clientSettings(SettingsUtils.textClientSettings(ClientSettings.builder().build())) 61 | .tokenSettings(SettingsUtils.textTokenSettings(TokenSettings.builder().build())) 62 | .clientId(clientId); 63 | oauthRepositoryMyBatis.saveOauthClientDetails(clientDetails); 64 | 65 | final OauthClientDetails oauthClientDetails = oauthRepositoryMyBatis.findOauthClientDetails(clientId); 66 | assertNotNull(oauthClientDetails); 67 | assertNotNull(oauthClientDetails.clientId()); 68 | assertNull(oauthClientDetails.clientSecret()); 69 | 70 | } 71 | 72 | @Test 73 | public void findAllOauthClientDetails() { 74 | final List allOauthClientDetails = oauthRepositoryMyBatis.findAllOauthClientDetails(); 75 | assertNotNull(allOauthClientDetails); 76 | assertTrue(allOauthClientDetails.isEmpty()); 77 | } 78 | 79 | @Test 80 | public void updateOauthClientDetailsArchive() { 81 | oauthRepositoryMyBatis.updateOauthClientDetailsArchive("ddooelddd", true); 82 | } 83 | 84 | 85 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/business/PasswordInlineAccessTokenInvokerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | import com.monkeyk.sos.service.dto.AccessTokenDto; 4 | import org.junit.jupiter.api.Disabled; 5 | import org.junit.jupiter.api.Test; 6 | 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | 14 | /** 15 | * 2019/7/6 16 | * 17 | * @author Shengzhao Li 18 | */ 19 | public class PasswordInlineAccessTokenInvokerTest extends AbstractInlineAccessTokenInvokerTest { 20 | 21 | 22 | @Test 23 | @Disabled 24 | public void invokeNormal() { 25 | 26 | createClientDetails(); 27 | 28 | createUser(); 29 | 30 | Map params = new HashMap<>(); 31 | params.put("client_id", clientId); 32 | params.put("client_secret", clientSecret); 33 | params.put("grant_type", "password"); 34 | params.put("scope", "read"); 35 | params.put("username", username); 36 | params.put("password", password); 37 | 38 | 39 | PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker(); 40 | final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params); 41 | 42 | assertNotNull(tokenDto); 43 | assertNotNull(tokenDto.getAccessToken()); 44 | assertNotNull(tokenDto.getRefreshToken()); 45 | 46 | // System.out.println(accessTokenDto); 47 | 48 | } 49 | 50 | 51 | @Test() 52 | public void invalidUsername() { 53 | 54 | createClientDetails(); 55 | 56 | Map params = new HashMap<>(); 57 | params.put("client_id", clientId); 58 | params.put("client_secret", clientSecret); 59 | params.put("grant_type", "password"); 60 | params.put("scope", "read"); 61 | 62 | params.put("username", "useraaa"); 63 | params.put("password", "password"); 64 | 65 | PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker(); 66 | assertThrows(Exception.class, () -> { 67 | accessTokenInvoker.invoke(params); 68 | }); 69 | // final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params); 70 | 71 | // assertNull(tokenDto); 72 | 73 | // System.out.println(accessTokenDto); 74 | 75 | } 76 | 77 | 78 | @Test() 79 | public void invalidScope() { 80 | 81 | createClientDetails(); 82 | createUser(); 83 | 84 | Map params = new HashMap<>(); 85 | params.put("client_id", clientId); 86 | params.put("client_secret", clientSecret); 87 | params.put("grant_type", "password"); 88 | // params.put("scope", "read"); 89 | 90 | params.put("username", username); 91 | params.put("password", password); 92 | 93 | PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker(); 94 | assertThrows(IllegalStateException.class, () -> { 95 | accessTokenInvoker.invoke(params); 96 | }); 97 | // final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params); 98 | 99 | // assertNull(tokenDto); 100 | 101 | // System.out.println(accessTokenDto); 102 | 103 | } 104 | 105 | 106 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/domain/oauth/ClaimsOAuth2TokenCustomizer.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.domain.oauth; 2 | 3 | import com.monkeyk.sos.domain.shared.GuidGenerator; 4 | import com.monkeyk.sos.domain.user.User; 5 | import com.monkeyk.sos.domain.user.UserRepository; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 10 | import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames; 11 | import org.springframework.security.oauth2.jwt.JwtClaimsSet; 12 | import org.springframework.security.oauth2.server.authorization.OAuth2Authorization; 13 | import org.springframework.security.oauth2.server.authorization.OAuth2TokenType; 14 | import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext; 15 | import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer; 16 | 17 | import java.util.Set; 18 | 19 | /** 20 | * 2023/10/17 21 | *

22 | * 扩展 jwt id_token claims 属性生成 23 | * 24 | * @author Shengzhao Li 25 | * @since 3.0.0 26 | */ 27 | public class ClaimsOAuth2TokenCustomizer implements OAuth2TokenCustomizer { 28 | 29 | private static final Logger LOG = LoggerFactory.getLogger(ClaimsOAuth2TokenCustomizer.class); 30 | 31 | @Autowired 32 | private UserRepository userRepository; 33 | 34 | public ClaimsOAuth2TokenCustomizer() { 35 | } 36 | 37 | @Override 38 | public void customize(JwtEncodingContext context) { 39 | 40 | JwtClaimsSet.Builder claims = context.getClaims(); 41 | //jti 42 | claims.id(GuidGenerator.generateNumber()); 43 | 44 | //根据不同的 scope 与 tokenType添加扩展属性 45 | OAuth2TokenType tokenType = context.getTokenType(); 46 | if (!OidcParameterNames.ID_TOKEN.equals(tokenType.getValue())) { 47 | //非 id_token 排除 48 | return; 49 | } 50 | OAuth2Authorization authorization = context.getAuthorization(); 51 | if (authorization == null) { 52 | if (LOG.isDebugEnabled()) { 53 | LOG.debug("Null OAuth2Authorization, ignore customize"); 54 | } 55 | return; 56 | } 57 | String username = authorization.getPrincipalName(); 58 | User user = userRepository.findProfileByUsername(username); 59 | boolean nullUser = (user == null); 60 | 61 | Set scopes = context.getAuthorizedScopes(); 62 | if (scopes.contains(OidcScopes.ADDRESS)) { 63 | String attrVal = nullUser ? null : user.address(); 64 | claims.claim(OidcScopes.ADDRESS, attrVal == null ? "" : attrVal); 65 | } 66 | if (scopes.contains(OidcScopes.EMAIL)) { 67 | String attrVal = nullUser ? null : user.email(); 68 | claims.claim(OidcScopes.EMAIL, attrVal == null ? "" : attrVal); 69 | } 70 | if (scopes.contains(OidcScopes.PHONE)) { 71 | String attrVal = nullUser ? null : user.phone(); 72 | claims.claim(OidcScopes.PHONE, attrVal == null ? "" : attrVal); 73 | } 74 | if (scopes.contains(OidcScopes.PROFILE)) { 75 | String attrVal = nullUser ? null : user.nickname(); 76 | claims.claim("nickname", attrVal == null ? "" : attrVal); 77 | claims.claim("updated_at", 0); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/JwksTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service; 2 | 3 | import com.nimbusds.jose.JWSAlgorithm; 4 | import com.nimbusds.jose.jwk.Curve; 5 | import com.nimbusds.jose.jwk.ECKey; 6 | import com.nimbusds.jose.jwk.RSAKey; 7 | import org.junit.jupiter.api.Test; 8 | 9 | import java.security.KeyPair; 10 | import java.security.KeyPairGenerator; 11 | import java.security.PrivateKey; 12 | import java.security.PublicKey; 13 | import java.security.interfaces.ECPublicKey; 14 | import java.security.interfaces.RSAPublicKey; 15 | import java.util.Set; 16 | 17 | import static com.nimbusds.jose.jwk.KeyOperation.*; 18 | import static org.junit.jupiter.api.Assertions.assertNotNull; 19 | 20 | /** 21 | * 2023/10/18 15:12 22 | *

23 | * JWK 24 | * generate 25 | * 26 | * @author Shengzhao Li 27 | * @since 3.0.0 28 | */ 29 | public class JwksTest { 30 | 31 | 32 | /** 33 | * ES256 jwk generate 34 | * 35 | * @throws Exception e 36 | */ 37 | @Test 38 | void jwkEC() throws Exception { 39 | 40 | Curve point = Curve.P_256; 41 | // Curve point = Curve.P_384; 42 | // Curve point = Curve.P_521; 43 | 44 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("EC"); 45 | keyPairGenerator.initialize(point.toECParameterSpec()); 46 | 47 | KeyPair keyPair = keyPairGenerator.generateKeyPair(); 48 | 49 | PublicKey aPublic = keyPair.getPublic(); 50 | PrivateKey aPrivate = keyPair.getPrivate(); 51 | 52 | 53 | ECKey key = new ECKey.Builder(point, (ECPublicKey) aPublic) 54 | .privateKey(aPrivate) 55 | .keyOperations(Set.of( 56 | SIGN, 57 | VERIFY, 58 | ENCRYPT, 59 | DECRYPT, 60 | DERIVE_KEY)) 61 | // keyId 必须唯一 62 | .keyID("sos-ecc-kid1") 63 | .algorithm(JWSAlgorithm.ES256) 64 | .build(); 65 | assertNotNull(key); 66 | 67 | String json = key.toJSONString(); 68 | assertNotNull(json); 69 | // System.out.println(json); 70 | 71 | 72 | } 73 | 74 | /** 75 | * RS256 jwk generate 76 | * 77 | * @throws Exception e 78 | */ 79 | @Test 80 | void jwkRS() throws Exception { 81 | 82 | KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); 83 | keyPairGenerator.initialize(2048); 84 | KeyPair keyPair = keyPairGenerator.generateKeyPair(); 85 | 86 | PrivateKey aPrivate = keyPair.getPrivate(); 87 | PublicKey aPublic = keyPair.getPublic(); 88 | 89 | 90 | RSAKey key = new RSAKey.Builder((RSAPublicKey) aPublic) 91 | .privateKey(aPrivate) 92 | // .keyUse(KeyUse.SIGNATURE) 93 | .keyOperations(Set.of( 94 | SIGN, 95 | VERIFY, 96 | ENCRYPT, 97 | DECRYPT, 98 | DERIVE_KEY)) 99 | .algorithm(JWSAlgorithm.RS256) 100 | .keyID("sos-rsa-kid2") 101 | .build(); 102 | 103 | assertNotNull(key); 104 | String json = key.toJSONString(); 105 | assertNotNull(json); 106 | // System.out.println(json); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/AccessTokenDto.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import com.fasterxml.jackson.annotation.JsonProperty; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.security.oauth2.core.OAuth2AccessToken; 6 | import org.springframework.security.oauth2.core.OAuth2RefreshToken; 7 | 8 | 9 | import java.io.Serial; 10 | import java.io.Serializable; 11 | import java.time.temporal.ChronoField; 12 | 13 | /** 14 | * 2019/7/5 15 | *

16 | * {"access_token":"iuy0fbfe-da2c-4840-8b66-848168ad8d00","token_type":"bearer","refresh_token":"9406e12f-d62e-42bd-ad40-0206d94ae7ds","expires_in":7199,"scope":"read"} 17 | * 18 | * @author Shengzhao Li 19 | * @since 2.0.1 20 | */ 21 | public class AccessTokenDto implements Serializable { 22 | @Serial 23 | private static final long serialVersionUID = -8894979171517528312L; 24 | 25 | 26 | @JsonProperty("access_token") 27 | private String accessToken; 28 | 29 | @JsonProperty("token_type") 30 | private String tokenType; 31 | 32 | @JsonProperty("refresh_token") 33 | private String refreshToken; 34 | 35 | @JsonProperty("scope") 36 | private String scope; 37 | 38 | @JsonProperty("expires_in") 39 | private int expiresIn; 40 | 41 | 42 | public AccessTokenDto() { 43 | } 44 | 45 | 46 | public AccessTokenDto(OAuth2AccessToken token) { 47 | this(token, null); 48 | } 49 | 50 | /** 51 | * @since 3.0.0 52 | */ 53 | public AccessTokenDto(OAuth2AccessToken token, OAuth2RefreshToken refreshToken) { 54 | this.accessToken = token.getTokenValue(); 55 | this.expiresIn = token.getExpiresAt().get(ChronoField.SECOND_OF_DAY); 56 | 57 | this.scope = StringUtils.join(token.getScopes(), ","); 58 | this.tokenType = token.getTokenType().getValue(); 59 | 60 | this.refreshToken = refreshToken != null ? refreshToken.getTokenValue() : null; 61 | // final OAuth2RefreshToken oAuth2RefreshToken = token.getRefreshToken(); 62 | // if (oAuth2RefreshToken != null) { 63 | // this.refreshToken = oAuth2RefreshToken.getValue(); 64 | // } 65 | } 66 | 67 | 68 | public String getAccessToken() { 69 | return accessToken; 70 | } 71 | 72 | public void setAccessToken(String accessToken) { 73 | this.accessToken = accessToken; 74 | } 75 | 76 | public String getTokenType() { 77 | return tokenType; 78 | } 79 | 80 | public void setTokenType(String tokenType) { 81 | this.tokenType = tokenType; 82 | } 83 | 84 | public String getRefreshToken() { 85 | return refreshToken; 86 | } 87 | 88 | public void setRefreshToken(String refreshToken) { 89 | this.refreshToken = refreshToken; 90 | } 91 | 92 | public String getScope() { 93 | return scope; 94 | } 95 | 96 | public void setScope(String scope) { 97 | this.scope = scope; 98 | } 99 | 100 | public int getExpiresIn() { 101 | return expiresIn; 102 | } 103 | 104 | public void setExpiresIn(int expiresIn) { 105 | this.expiresIn = expiresIn; 106 | } 107 | 108 | @Override 109 | public String toString() { 110 | return "{" + 111 | "accessToken='" + accessToken + '\'' + 112 | ", tokenType='" + tokenType + '\'' + 113 | ", refreshToken='" + refreshToken + '\'' + 114 | ", scope='" + scope + '\'' + 115 | ", expiresIn=" + expiresIn + 116 | '}'; 117 | } 118 | 119 | } 120 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/JwtBearerJwksController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.nimbusds.jose.jwk.JWK; 4 | import com.nimbusds.jose.jwk.JWKSet; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.web.bind.annotation.GetMapping; 8 | import org.springframework.web.bind.annotation.RestController; 9 | 10 | import java.util.Arrays; 11 | import java.util.Map; 12 | 13 | /** 14 | * 2023/10/24 16:24 15 | *

16 | * grant_type=jwt-bearer 中的 jwkSetUrl 实现参考 17 | *

18 | * todo: 此实现仅供参考;实际生产时应该由client端应用提供 19 | * 20 | * @author Shengzhao Li 21 | * @since 3.0.0 22 | */ 23 | @RestController 24 | public class JwtBearerJwksController { 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(JwtBearerJwksController.class); 27 | 28 | 29 | /** 30 | * RS256 公私钥对 31 | * 如何生成? 详见 JwksTest.java 32 | */ 33 | public static final String RS256_KEY = "{\"p\":\"-Y5ymP0tAtOmpksf6y1rT-CsGUyklercT0vY0fMbkUyZH8igxUr0ZjXVr3Yzhlh8sJ5y5-0IEpPw7L4v7_OmCC-7t_M-ntf2-36rqIrK7AMhGf4mle4pMQhBeIJN0n91wMxmNXMwto4L3MWZ8f6K1QH1cirj3_BQsA4XXEgMMKE\",\"kty\":\"RSA\",\"q\":\"_HUwOfykJSjDkisyAK3QaNDFxik3HLTr7m0kU3UNLc1KRaNTIwPYuLaskGE4Se6Idy8TLc7NuEB96VSd9LaGakrDPBwh9ZcN8uBJVA162TCA1RUJjwO4k33uxkVo8gvNQ5ooBnEdT-rMhrjZa3ko-vLR5KCQHs6Gq6SWLBalth8\",\"d\":\"D65_9R01rDFuXc6qJKlNo8-x52jBYtDJJSxFoXW3Znek3fwTX7Le10lNKHf0EEJixnmXumIivl4hFCCBvlc-KP6P_OZZmU9JzC-gezUFdOuhfouMJh6VpbO272nqIfOU8UZJEXCxMSvOqJs-grekSqWMdEZpFytlG6hxNGVEJcy619rPdKL-xUlIliK0M4BItOn24u0Awd4msHyOz9F5UamDa8dnnuRlCJSnqUxBhvMicxP-k4ZXqx_csiVJt5GSkBU2-68T4NYPsTBqUufXsPVbThcoHI6COdWv8dQ5ovNI6P02aEUYA0-QlGVC4mPCmxo81Q8ukK5UUOvjFP7cAQ\",\"e\":\"AQAB\",\"kid\":\"jwt-bearer-demo-rsa-kid1\",\"key_ops\":[\"encrypt\",\"verify\",\"deriveKey\",\"sign\",\"decrypt\"],\"qi\":\"glJKxfNKRauPqt-yQBuiF6XyfIxSSts0ZZJRyf4CAvlXmruWlZdd2IwY4V67SPBvoOHm1o32zI0clQabPt1ovHS1fMfPuy1L2ytQUL3yVSVddhkG9otadaPQW8kuc86wLdKwUjpBREQjwNeaTxkuoJVPcbXlNsayA6h17ljceBc\",\"dp\":\"lXGWcsN6Ru0UKRVn4d_rGYSDywq4rQZeNCZJi0C4S4TBVeVBUaSXQvYOJurz5AntcZ8RVI3_fZCWgE9MSbdwwApFsdy6rUjLIMQ0a9PhvQAKvJQT60kZ5cD54_60N9AYZgKBWpTGoSvjMqwqil5SKUjpARtqJtq0lxl5J8wFcME\",\"alg\":\"RS256\",\"dq\":\"CiaAEOTKiL_x1Q-ti_9xELXMLeJ8V8gicEytGDntlLjbUp91eUPvU8XsfEWcaMSRchFPeRkGhnD5XwdK7orkLqPg46rR5rjzE5_W8u0z0kWz-F1HLBvfMPbwQcKKrKiy0RQCpfeoUQ1Euen2u-58KlLXA5U9FjABlCci7pTehss\",\"n\":\"9hp17DWgdCzJBq8T0hyV5F99-7_NtJu01yL95jZ9UF7bErGdqBtfw6_X5NmI1zMwmsAiksARr5_X7Hr3Gg2EbadLPymYAoGpaIwOZV04hHr_pJmqxNOaQU89_CDz-fmOhRoizZgxKAfWGCW1VLrKMaU3h4gs-G2gT0xQPDpkuXDV7WxYViqfLPhP94Cnk-geCeJpkY9q9BFZGkqW9mYeb2Ut1owlgY-Rfz-RID5gqGjL_AS3DYvvNf9_4eI8v3ahqRKDUXccw_sntEwBs95zWbRXQXBHgIKNIKp4ITnsN7OPc66QlJSpzqSkeOx0fvnCJ5bIh4fViqqLtp0akdFZfw\"}"; 34 | 35 | 36 | /** 37 | * ES256 公私钥对 38 | */ 39 | public static final String ES256_KEY = "{\"kty\":\"EC\",\"d\":\"J6ZIiWeVp4fTXAp5W2w9nw7lACkGaAjOAuLOlrzATDo\",\"crv\":\"P-256\",\"kid\":\"jwt-bearer-demo-ecc-kid\",\"key_ops\":[\"sign\",\"verify\",\"encrypt\",\"deriveKey\",\"decrypt\"],\"x\":\"fJ4RA2IawTPMIWx7bqlYTzrjM8Gl4YQMNRaX4isqeDI\",\"y\":\"sBeszsJArg2sdc1AdrxIyDIgDIVw84KWF27FsnkQenc\",\"alg\":\"ES256\"}"; 40 | 41 | 42 | /** 43 | * client 端提供的 jwks 参考实现; 44 | * 返回 public-key 45 | * 46 | * @see org.springframework.security.oauth2.server.authorization.web.NimbusJwkSetEndpointFilter 47 | */ 48 | @GetMapping("/api/public/oauth2/jwt_bearer/demo_jwks") 49 | public Map jwks() throws Exception { 50 | 51 | JWK rsJwk = JWK.parse(RS256_KEY); 52 | JWK esJwk = JWK.parse(ES256_KEY); 53 | JWKSet jwkSet = new JWKSet(Arrays.asList(rsJwk, esJwk)); 54 | 55 | //注意:只返回 publicKey 56 | return jwkSet.toJSONObject(true); 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/ClientDetailsController.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.monkeyk.sos.infrastructure.PKCEUtils; 4 | import com.monkeyk.sos.service.dto.OauthClientDetailsDto; 5 | import com.monkeyk.sos.service.OauthService; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.security.oauth2.core.AuthorizationGrantType; 8 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.ui.Model; 11 | import org.springframework.validation.BindingResult; 12 | import org.springframework.web.bind.annotation.*; 13 | 14 | import java.util.List; 15 | 16 | /** 17 | * Handle 'client_details' management 18 | *

19 | * v3.0.0 中叫 'RegisteredClient', table: oauth2_registered_client 20 | * 21 | * @author Shengzhao Li 22 | * @see org.springframework.security.oauth2.server.authorization.client.RegisteredClient 23 | */ 24 | @Controller 25 | public class ClientDetailsController { 26 | 27 | 28 | @Autowired 29 | private OauthService oauthService; 30 | 31 | @Autowired 32 | private OauthClientDetailsDtoValidator clientDetailsDtoValidator; 33 | 34 | 35 | @RequestMapping("client_details") 36 | public String clientDetails(Model model) { 37 | List clientDetailsDtoList = oauthService.loadAllOauthClientDetailsDtos(); 38 | model.addAttribute("clientDetailsDtoList", clientDetailsDtoList); 39 | return "clientdetails/client_details"; 40 | } 41 | 42 | 43 | /** 44 | * Logic delete 45 | */ 46 | @RequestMapping("archive_client/{clientId}") 47 | public String archiveClient(@PathVariable("clientId") String clientId) { 48 | oauthService.archiveOauthClientDetails(clientId); 49 | return "redirect:../client_details"; 50 | } 51 | 52 | /** 53 | * Test client 54 | */ 55 | @GetMapping("test_client/{clientId}") 56 | public String testClient(@PathVariable("clientId") String clientId, Model model) { 57 | OauthClientDetailsDto clientDetailsDto = oauthService.loadOauthClientDetailsDto(clientId); 58 | model.addAttribute("clientDetailsDto", clientDetailsDto); 59 | //v3.0.0 added PKCE params 60 | String codeVerifier = PKCEUtils.generateCodeVerifier(); 61 | String codeChallenge = PKCEUtils.generateCodeChallenge(codeVerifier); 62 | model.addAttribute("codeVerifier", codeVerifier) 63 | .addAttribute("codeChallenge", codeChallenge); 64 | return "clientdetails/test_client"; 65 | } 66 | 67 | 68 | /** 69 | * Register client 70 | */ 71 | @RequestMapping(value = "register_client", method = RequestMethod.GET) 72 | public String registerClient(Model model) { 73 | OauthClientDetailsDto formDto = new OauthClientDetailsDto(); 74 | //初始化 v3.0.0 added 75 | formDto.setClientAuthenticationMethods("client_secret_post"); 76 | formDto.setScopes(OidcScopes.OPENID); 77 | formDto.setAuthorizationGrantTypes(AuthorizationGrantType.AUTHORIZATION_CODE.getValue()); 78 | 79 | model.addAttribute("formDto", formDto); 80 | return "clientdetails/register_client"; 81 | } 82 | 83 | 84 | /** 85 | * Submit register client 86 | */ 87 | @RequestMapping(value = "register_client", method = RequestMethod.POST) 88 | public String submitRegisterClient(@ModelAttribute("formDto") OauthClientDetailsDto formDto, BindingResult result) { 89 | clientDetailsDtoValidator.validate(formDto, result); 90 | if (result.hasErrors()) { 91 | return "clientdetails/register_client"; 92 | } 93 | oauthService.registerClientDetails(formDto); 94 | return "redirect:client_details"; 95 | } 96 | 97 | 98 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/SettingsUtils.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.infrastructure; 2 | 3 | import com.fasterxml.jackson.core.type.TypeReference; 4 | import com.fasterxml.jackson.databind.Module; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.springframework.security.jackson2.SecurityJackson2Modules; 7 | import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module; 8 | import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; 9 | import org.springframework.security.oauth2.server.authorization.settings.ConfigurationSettingNames; 10 | import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat; 11 | import org.springframework.security.oauth2.server.authorization.settings.TokenSettings; 12 | 13 | import java.util.List; 14 | import java.util.Map; 15 | 16 | /** 17 | * 2023/10/13 14:49 18 | * 19 | * @author Shengzhao Li 20 | * @since 3.0.0 21 | */ 22 | public abstract class SettingsUtils { 23 | 24 | 25 | private static ObjectMapper objectMapper = new ObjectMapper(); 26 | 27 | static { 28 | // ClassLoader classLoader = JdbcRegisteredClientRepository.class.getClassLoader(); 29 | ClassLoader classLoader = SettingsUtils.class.getClassLoader(); 30 | List securityModules = SecurityJackson2Modules.getModules(classLoader); 31 | objectMapper.registerModules(securityModules); 32 | objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module()); 33 | } 34 | 35 | 36 | private SettingsUtils() { 37 | } 38 | 39 | /** 40 | * text settings -> TokenSettings 41 | * 42 | * @param settings text 43 | * @return TokenSettings 44 | */ 45 | public static TokenSettings buildTokenSettings(String settings) { 46 | Map map = parseMap(settings); 47 | TokenSettings.Builder builder = TokenSettings.withSettings(map); 48 | if (!map.containsKey(ConfigurationSettingNames.Token.ACCESS_TOKEN_FORMAT)) { 49 | builder.accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED); 50 | } 51 | return builder.build(); 52 | } 53 | 54 | /** 55 | * TokenSettings -> text 56 | * 57 | * @param settings TokenSettings 58 | * @return text 59 | */ 60 | public static String textTokenSettings(TokenSettings settings) { 61 | Map map = settings.getSettings(); 62 | return writeMap(map); 63 | } 64 | 65 | 66 | /** 67 | * ClientSettings -> text 68 | * 69 | * @param settings ClientSettings 70 | * @return text 71 | */ 72 | public static String textClientSettings(ClientSettings settings) { 73 | Map map = settings.getSettings(); 74 | return writeMap(map); 75 | } 76 | 77 | 78 | /** 79 | * text settings -> ClientSettings 80 | * 81 | * @param settings text 82 | * @return ClientSettings 83 | */ 84 | public static ClientSettings buildClientSettings(String settings) { 85 | Map map = parseMap(settings); 86 | return ClientSettings.withSettings(map) 87 | .build(); 88 | } 89 | 90 | 91 | private static Map parseMap(String data) { 92 | try { 93 | return objectMapper.readValue(data, new TypeReference<>() { 94 | }); 95 | } catch (Exception ex) { 96 | throw new IllegalArgumentException(ex.getMessage(), ex); 97 | } 98 | } 99 | 100 | private static String writeMap(Map data) { 101 | try { 102 | return objectMapper.writeValueAsString(data); 103 | } catch (Exception ex) { 104 | throw new IllegalArgumentException(ex.getMessage(), ex); 105 | } 106 | } 107 | 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/impl/UserServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.impl; 2 | 3 | import com.monkeyk.sos.domain.shared.security.SOSUserDetails; 4 | import com.monkeyk.sos.domain.user.User; 5 | import com.monkeyk.sos.domain.user.UserRepository; 6 | import com.monkeyk.sos.service.UserService; 7 | import com.monkeyk.sos.service.dto.UserDto; 8 | import com.monkeyk.sos.service.dto.UserFormDto; 9 | import com.monkeyk.sos.service.dto.UserJsonDto; 10 | import com.monkeyk.sos.service.dto.UserOverviewDto; 11 | import com.monkeyk.sos.web.WebUtils; 12 | import org.slf4j.Logger; 13 | import org.slf4j.LoggerFactory; 14 | import org.springframework.beans.factory.annotation.Autowired; 15 | import org.springframework.security.core.Authentication; 16 | import org.springframework.security.core.context.SecurityContextHolder; 17 | import org.springframework.security.core.userdetails.UserDetails; 18 | import org.springframework.security.core.userdetails.UsernameNotFoundException; 19 | import org.springframework.stereotype.Service; 20 | 21 | import java.util.List; 22 | 23 | /** 24 | * @author Shengzhao Li 25 | */ 26 | @Service("userService") 27 | public class UserServiceImpl implements UserService { 28 | 29 | 30 | private static final Logger LOG = LoggerFactory.getLogger(UserServiceImpl.class); 31 | 32 | 33 | @Autowired 34 | private UserRepository userRepository; 35 | 36 | /** 37 | * 提示:产品化的设计此处应加上缓存提高性能 38 | */ 39 | @Override 40 | public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { 41 | User user = userRepository.findByUsername(username); 42 | if (user == null || user.archived()) { 43 | throw new UsernameNotFoundException("Not found any user for username[" + username + "]"); 44 | } 45 | 46 | return new SOSUserDetails(user); 47 | } 48 | 49 | @Override 50 | public UserJsonDto loadCurrentUserJsonDto() { 51 | final Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 52 | final Object principal = authentication.getPrincipal(); 53 | 54 | // if (authentication instanceof OAuth2Authentication && 55 | // (principal instanceof String || principal instanceof org.springframework.security.core.userdetails.User)) { 56 | // return loadOauthUserJsonDto((OAuth2Authentication) authentication); 57 | // } else { 58 | final SOSUserDetails userDetails = (SOSUserDetails) principal; 59 | return new UserJsonDto(userRepository.findByGuid(userDetails.getUserGuid())); 60 | // } 61 | } 62 | 63 | /** 64 | * 提示:产品化的设计此处应该有分页会更好 65 | */ 66 | @Override 67 | public UserOverviewDto loadUserOverviewDto(UserOverviewDto overviewDto) { 68 | List users = userRepository.findUsersByUsername(overviewDto.getUsername()); 69 | overviewDto.setUserDtos(UserDto.toDtos(users)); 70 | return overviewDto; 71 | } 72 | 73 | @Override 74 | public boolean isExistedUsername(String username) { 75 | final User user = userRepository.findByUsername(username); 76 | return user != null; 77 | } 78 | 79 | @Override 80 | public String saveUser(UserFormDto formDto) { 81 | User user = formDto.newUser(); 82 | userRepository.saveUser(user); 83 | LOG.debug("{}|Save User: {}", WebUtils.getIp(), user); 84 | return user.guid(); 85 | } 86 | 87 | 88 | // private UserJsonDto loadOauthUserJsonDto(OAuth2Authentication oAuth2Authentication) { 89 | // UserJsonDto userJsonDto = new UserJsonDto(); 90 | // userJsonDto.setUsername(oAuth2Authentication.getName()); 91 | // 92 | // final Collection authorities = oAuth2Authentication.getAuthorities(); 93 | // for (GrantedAuthority authority : authorities) { 94 | // userJsonDto.getPrivileges().add(authority.getAuthority()); 95 | // } 96 | // 97 | // return userJsonDto; 98 | // } 99 | } -------------------------------------------------------------------------------- /others/oauth_test.txt: -------------------------------------------------------------------------------- 1 | 2 | 适用范围:v3.0.0 之前的版本。 3 | 4 | 方式1:基于浏览器 (访问时后跳到登录页面,登录成功后跳转到redirect_uri指定的地址) [GET] 5 | 说明:只能使用admin或unity 账号登录才能有权限访问,若使用mobile账号登录将返回Access is denied 6 | http://localhost:8080/oauth/authorize?client_id=unity-client&redirect_uri=http%3a%2f%2flocalhost%3a8080%2fspring-oauth-server%2funity%2fdashboard&response_type=code&scope=read 7 | 8 | 说明: 由于mobile-client只支持password,refresh_token, 所以不管用哪个账号登录后都将返回 OAuth Error 9 | http://localhost:8080/oauth/authorize?client_id=mobile-client&redirect_uri=http%3a%2f%2flocalhost%3a8080%2fspring-oauth-server%2fm%2fdashboard&response_type=code&scope=read 10 | 11 | 12 | 13 | 14 | 响应的URL如: 15 | http://localhost:8080/unity/dashboard?code=hGQ8qx 16 | 17 | 通过code换取access_token [POST] (注意:这一步用httpclient在程序中调用,不要在浏览器中) 18 | http://localhost:8080/oauth/token?client_id=unity-client&client_secret=unity&grant_type=authorization_code&code=hGQ8qx&redirect_uri=http%3a%2f%2flocalhost%3a8080%2fspring-oauth-server%2funity%2fdashboard 19 | 20 | 21 | 方式2:基于客户端 (注意参数中的username,password,对应用户的账号,密码) [POST] (注意:这一步用httpclient在程序中调用,不要在浏览器中) 22 | http://localhost:8080/oauth/token?client_id=mobile-client&client_secret=mobile&grant_type=password&scope=read&username=mobile&password=mobile 23 | 24 | 说明:由于unity-client不支持password,所以若用unity-client通过password方式去授权,将返回 invalid_client 25 | http://localhost:8080/oauth/token?client_id=unity-client&client_secret=unity&grant_type=password&scope=read&username=mobile&password=mobile 26 | 27 | 28 | 29 | 获取access_token响应的数据如: 30 | {"access_token":"3420d0e0-ed77-45e1-8370-2b55af0a62e8","token_type":"bearer","refresh_token":"b36f4978-a172-4aa8-af89-60f58abe3ba1","expires_in":43199,"scope":"read write"} 31 | 32 | 33 | 获取access_token后访问资源 [GET] 34 | http://localhost:8080/unity/dashboard?access_token=89767569-5b78-4b26-ae2d-d361aa3e6bf9 35 | 36 | 37 | 38 | 刷新access_token [POST] (注意:这一步用httpclient在程序中调用,不要在浏览器中) 39 | http://localhost:8080/oauth/token?client_id=mobile-client&client_secret=mobile&grant_type=refresh_token&refresh_token=b36f4978-a172-4aa8-af89-60f58abe3ba1 40 | 41 | 42 | Restful OAuth2 Test [POST] (注意:这一步用httpclient在程序中调用,不要在浏览器中) 43 | URL: /oauth/rest_token 44 | ContentType: application/json 45 | 46 | DEMO URL: http://localhost:8080/oauth2/rest_token 47 | Request Body: 48 | {"grant_type":"client_credentials","scope":"read","client_id":"credentials","client_secret":"credentials","username":"user","password":"123"} 49 | 50 | Response Body: 51 | { 52 | "access_token": "cd165ebc-562d-45df-8488-9f1ba947553e", 53 | "token_type": "bearer", 54 | "expires_in": 43193, 55 | "scope": "read" 56 | } 57 | 58 | 59 | 60 | 61 | 更多的测试请访问 62 | https://gitee.com/mkk/spring-oauth-client 63 | 64 | 65 | ------------------------------------------------------------------------------------------------ 66 | grant_type(授权方式) 67 | 1.authorization_code 授权码模式(即先登录获取code,再获取token) 68 | 2.password 密码模式(将用户名,密码传过去,直接获取token) 69 | 3.refresh_token 刷新token 70 | 4.implicit 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash) 71 | 5.client_credentials 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向'服务端'获取资源) 72 | 73 | 74 | scope 75 | 1.read 76 | 2.write 77 | 3.trust 78 | 79 | 80 | ------------------------------------------------------------------------------------------------ 81 | 82 | Resource API 83 | Use it get resource-server resources after auth successful. will use it in project. 84 | (retrieve current logged user information) 85 | 86 | [ROLE_UNITY] 87 | http://localhost:8080/unity/user_info?access_token=b03b99a1-f128-4d6e-b9d3-38a0ebcab5ef 88 | Response JSON 89 | {"archived":false,"email":"unity@wdcy.cc","guid":"55b713df1c6f423e842ad68668523c49","phone":"","privileges":["UNITY"],"username":"unity"} 90 | 91 | [ROLE_MOBILE] 92 | http://localhost:8080/m/user_info?access_token=20837fa5-a0a1-4c76-9083-1f0e47ca0208 93 | Response JSON 94 | {"archived":false,"email":"mobile@wdcy.cc","guid":"612025cb3f964a64a48bbdf77e53c2c1","phone":"","privileges":["MOBILE"],"username":"mobile"} 95 | 96 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/infrastructure/jdbc/OauthRepositoryJdbc.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure.jdbc; 13 | 14 | import com.monkeyk.sos.domain.oauth.OauthClientDetails; 15 | import com.monkeyk.sos.domain.oauth.OauthRepository; 16 | import org.springframework.beans.factory.annotation.Autowired; 17 | import org.springframework.jdbc.core.JdbcTemplate; 18 | import org.springframework.stereotype.Repository; 19 | 20 | import java.sql.Timestamp; 21 | import java.time.Instant; 22 | import java.util.List; 23 | 24 | /** 25 | * 2015/11/16 26 | * 27 | * @author Shengzhao Li 28 | */ 29 | @Repository("oauthRepositoryJdbc") 30 | public class OauthRepositoryJdbc implements OauthRepository { 31 | 32 | 33 | private final OauthClientDetailsRowMapper oauthClientDetailsRowMapper = new OauthClientDetailsRowMapper(); 34 | 35 | 36 | @Autowired 37 | private JdbcTemplate jdbcTemplate; 38 | 39 | 40 | @Override 41 | public OauthClientDetails findOauthClientDetails(String clientId) { 42 | final String sql = " select * from oauth2_registered_client where client_id = ? "; 43 | final List list = this.jdbcTemplate.query(sql, oauthClientDetailsRowMapper, clientId); 44 | return list.isEmpty() ? null : list.get(0); 45 | } 46 | 47 | @Override 48 | public List findAllOauthClientDetails() { 49 | final String sql = " select * from oauth2_registered_client where archived = 0 order by create_time desc "; 50 | return this.jdbcTemplate.query(sql, oauthClientDetailsRowMapper); 51 | } 52 | 53 | @Override 54 | public void updateOauthClientDetailsArchive(String clientId, boolean archive) { 55 | final String sql = " update oauth2_registered_client set archived = ? where client_id = ? "; 56 | this.jdbcTemplate.update(sql, archive, clientId); 57 | } 58 | 59 | @Override 60 | public void saveOauthClientDetails(final OauthClientDetails clientDetails) { 61 | final String sql = " insert into oauth2_registered_client(id,create_time,client_id,client_id_issued_at,client_secret,client_secret_expires_at," + 62 | "client_name,client_authentication_methods,authorization_grant_types,redirect_uris," + 63 | " post_logout_redirect_uris,scopes,client_settings,token_settings) values (?,?,?,?,?,?,?,?,?,?,?,?,?,?)"; 64 | 65 | this.jdbcTemplate.update(sql, ps -> { 66 | int index = 1; 67 | ps.setString(index++, clientDetails.id()); 68 | ps.setTimestamp(index++, Timestamp.valueOf(clientDetails.createTime())); 69 | ps.setString(index++, clientDetails.clientId()); 70 | ps.setTimestamp(index++, Timestamp.from(clientDetails.clientIdIssuedAt())); 71 | 72 | ps.setString(index++, clientDetails.clientSecret()); 73 | Instant clientSecretExpiresAt = clientDetails.clientSecretExpiresAt(); 74 | ps.setTimestamp(index++, clientSecretExpiresAt != null ? Timestamp.from(clientSecretExpiresAt) : null); 75 | ps.setString(index++, clientDetails.clientName()); 76 | 77 | ps.setString(index++, clientDetails.clientAuthenticationMethods()); 78 | ps.setString(index++, clientDetails.authorizationGrantTypes()); 79 | ps.setString(index++, clientDetails.redirectUris()); 80 | 81 | ps.setString(index++, clientDetails.postLogoutRedirectUris()); 82 | ps.setString(index++, clientDetails.scopes()); 83 | ps.setString(index++, clientDetails.clientSettings()); 84 | 85 | ps.setString(index++, clientDetails.tokenSettings()); 86 | }); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/resources/templates/clientdetails/client_details.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | client_details - Spring Security&OAuth2.1 10 | 11 | 12 | 17 | 18 | 19 | Home 20 | 21 |

22 |
23 |

client_details

24 |
25 |
26 |
27 | 注册client 28 |
29 |
30 |
31 | 32 |
33 | 34 |
35 |
    36 |
  • 37 |
    38 |
    39 | test 40 | archive 42 |
    43 | Archived 44 |
    45 |

    46 | [[${cli.clientId}]] - 47 | 48 |

    49 | 50 |
    51 | client_id:   52 | client_secret: ***  53 |
    54 | grant_types:   55 | authentication_methods:   56 |
    57 | scopes:   58 | redirect_uri:   59 |
    60 | client_id_issued:   61 | client_secret_expires: 62 |
    63 | client_settings: 64 |
    65 | token_settings: 66 |
    67 | create_time:   68 | archived:   70 |
    71 |
  • 72 | 73 |
74 |
75 | 每一个item对应oauth2_registered_client表中的一条数据; 共条数据. 77 |
78 | 对数据库表的详细说明请访问 79 | https://andaily.com/spring-oauth-server/db_table_description_3.0.0.html 80 | (或访问项目others目录的db_table_description_3.0.0.html文件) 81 |
82 |
83 | 84 |
85 | 86 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/infrastructure/jdbc/UserRepositoryJdbcTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.infrastructure.jdbc; 13 | 14 | import com.monkeyk.sos.domain.user.User; 15 | import com.monkeyk.sos.domain.user.UserRepository; 16 | import com.monkeyk.sos.infrastructure.AbstractRepositoryTest; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.springframework.beans.factory.annotation.Autowired; 20 | 21 | 22 | import java.util.List; 23 | 24 | import static org.junit.jupiter.api.Assertions.*; 25 | 26 | 27 | /* 28 | * @author Shengzhao Li 29 | */ 30 | public class UserRepositoryJdbcTest extends AbstractRepositoryTest { 31 | 32 | 33 | @Autowired 34 | private UserRepository userRepository; 35 | 36 | 37 | /** 38 | * @since 3.0.0 39 | */ 40 | @Test 41 | void findProfileByUsername() { 42 | 43 | String username = "userxxxx"; 44 | User user = userRepository.findProfileByUsername(username); 45 | assertNull(user); 46 | 47 | User user2 = new User(username, "{123}", "123", "ewo@honyee.cc"); 48 | user2.address("address").nickname("nick-name"); 49 | userRepository.saveUser(user2); 50 | 51 | User user3 = userRepository.findProfileByUsername(username); 52 | assertNotNull(user3); 53 | assertNotNull(user3.phone()); 54 | assertNotNull(user3.email()); 55 | 56 | } 57 | 58 | 59 | @Test 60 | public void findByGuid() { 61 | User user = userRepository.findByGuid("oood"); 62 | assertNull(user); 63 | 64 | user = new User("user", "123", "123", "ewo@honyee.cc"); 65 | userRepository.saveUser(user); 66 | 67 | user = userRepository.findByGuid(user.guid()); 68 | assertNotNull(user); 69 | assertNotNull(user.email()); 70 | 71 | 72 | } 73 | 74 | @Test 75 | public void findUsersByUsername() { 76 | User user = userRepository.findByGuid("oood"); 77 | assertNull(user); 78 | 79 | user = new User("user", "123", "123", "ewo@honyee.cc"); 80 | userRepository.saveUser(user); 81 | 82 | final List list = userRepository.findUsersByUsername(user.username()); 83 | assertNotNull(list); 84 | 85 | assertEquals(list.size(), 1); 86 | 87 | } 88 | 89 | 90 | @Test 91 | public void updateUser() { 92 | User user = new User("user", "123", "123", "ewo@honyee.cc"); 93 | userRepository.saveUser(user); 94 | 95 | user = userRepository.findByGuid(user.guid()); 96 | assertNotNull(user); 97 | assertNotNull(user.email()); 98 | 99 | String newEmail = "test@honyee.cc"; 100 | user.email(newEmail).phone("12344444"); 101 | userRepository.updateUser(user); 102 | 103 | user = userRepository.findByGuid(user.guid()); 104 | assertNotNull(user); 105 | assertEquals(user.email(), newEmail); 106 | } 107 | 108 | 109 | @Test 110 | public void findByUsername() { 111 | String username = "user"; 112 | User user = new User(username, "123", "123", "ewo@honyee.cc"); 113 | userRepository.saveUser(user); 114 | 115 | User result = userRepository.findByUsername(username); 116 | assertNotNull(result); 117 | } 118 | 119 | 120 | /* 121 | * Run the test must initial db firstly 122 | * */ 123 | // @Test() 124 | public void testPrivilege() { 125 | 126 | String guid = "55b713df1c6f423e842ad68668523c49"; 127 | final User user = userRepository.findByGuid(guid); 128 | 129 | assertNotNull(user); 130 | assertEquals(user.privileges().size(), 1); 131 | 132 | 133 | } 134 | 135 | 136 | } -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/business/RefreshTokenInlineAccessTokenInvokerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | import com.monkeyk.sos.service.dto.AccessTokenDto; 4 | import org.junit.jupiter.api.Disabled; 5 | import org.junit.jupiter.api.Test; 6 | 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | import static org.junit.jupiter.api.Assertions.*; 12 | 13 | 14 | /** 15 | * 2019/7/6 16 | * 17 | * @author Shengzhao Li 18 | */ 19 | public class RefreshTokenInlineAccessTokenInvokerTest extends AbstractInlineAccessTokenInvokerTest { 20 | 21 | 22 | @Test 23 | @Disabled 24 | public void invokeNormal() { 25 | 26 | createClientDetails(); 27 | 28 | createUser(); 29 | 30 | Map params = new HashMap<>(); 31 | params.put("client_id", clientId); 32 | params.put("client_secret", clientSecret); 33 | params.put("grant_type", "password"); 34 | params.put("scope", "read"); 35 | params.put("username", username); 36 | params.put("password", password); 37 | 38 | 39 | PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker(); 40 | final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params); 41 | 42 | assertNotNull(tokenDto); 43 | assertNotNull(tokenDto.getRefreshToken()); 44 | assertNotNull(tokenDto.getAccessToken()); 45 | 46 | 47 | Map params2 = new HashMap<>(); 48 | params2.put("client_id", clientId); 49 | params2.put("client_secret", clientSecret); 50 | params2.put("grant_type", "refresh_token"); 51 | params2.put("scope", "read"); 52 | params2.put("refresh_token", tokenDto.getRefreshToken()); 53 | 54 | 55 | RefreshTokenInlineAccessTokenInvoker refreshTokenInlineAccessTokenInvoker = new RefreshTokenInlineAccessTokenInvoker(); 56 | final AccessTokenDto accessTokenDto = refreshTokenInlineAccessTokenInvoker.invoke(params2); 57 | 58 | 59 | assertNotNull(accessTokenDto); 60 | assertNotNull(accessTokenDto.getAccessToken()); 61 | 62 | assertNotEquals(accessTokenDto.getAccessToken(), tokenDto.getAccessToken()); 63 | assertEquals(accessTokenDto.getRefreshToken(), tokenDto.getRefreshToken()); 64 | 65 | } 66 | 67 | 68 | @Test() 69 | @Disabled 70 | public void invalidRefreshToken() { 71 | 72 | createClientDetails(); 73 | 74 | createUser(); 75 | 76 | Map params = new HashMap<>(); 77 | params.put("client_id", clientId); 78 | params.put("client_secret", clientSecret); 79 | params.put("grant_type", "password"); 80 | params.put("scope", "read"); 81 | params.put("username", username); 82 | params.put("password", password); 83 | 84 | 85 | PasswordInlineAccessTokenInvoker accessTokenInvoker = new PasswordInlineAccessTokenInvoker(); 86 | final AccessTokenDto tokenDto = accessTokenInvoker.invoke(params); 87 | 88 | assertNotNull(tokenDto); 89 | assertNotNull(tokenDto.getRefreshToken()); 90 | assertNotNull(tokenDto.getAccessToken()); 91 | 92 | 93 | Map params2 = new HashMap<>(); 94 | params2.put("client_id", clientId); 95 | params2.put("client_secret", clientSecret); 96 | params2.put("grant_type", "refresh_token"); 97 | params2.put("scope", "read"); 98 | params2.put("refresh_token", tokenDto.getRefreshToken() + "sss"); 99 | 100 | 101 | RefreshTokenInlineAccessTokenInvoker refreshTokenInlineAccessTokenInvoker = new RefreshTokenInlineAccessTokenInvoker(); 102 | assertThrows(IllegalStateException.class, () -> { 103 | refreshTokenInlineAccessTokenInvoker.invoke(params2); 104 | }); 105 | // final AccessTokenDto accessTokenDto = refreshTokenInlineAccessTokenInvoker.invoke(params2); 106 | 107 | 108 | // assertNotNull(accessTokenDto); 109 | // assertNotNull(accessTokenDto.getAccessToken()); 110 | // 111 | // assertNotEquals(accessTokenDto.getAccessToken(), tokenDto.getAccessToken()); 112 | // assertEquals(accessTokenDto.getRefreshToken(), tokenDto.getRefreshToken()); 113 | 114 | } 115 | 116 | 117 | } -------------------------------------------------------------------------------- /others/database/initial_data.ddl: -------------------------------------------------------------------------------- 1 | -- Initial database data 2 | 3 | truncate user_; 4 | truncate user_privilege; 5 | -- admin, password is Admin@2013 ( All privileges) 6 | insert into user_(id, guid, create_time, email, password, phone, username, default_user) 7 | values (21, '29f6004fb1b0466f9572b02bf2ac1be8', now(), 'admin@andaily.com', 8 | '$2a$10$bIIt6KqIMweTZZC.IIHBLuN3dEIJL0LQFRPrtWTujn9O3Sl5Us5vW', '028-1234567', 'admin', 1); 9 | 10 | insert into user_privilege(user_id, privilege) 11 | values (21, 'ADMIN'); 12 | insert into user_privilege(user_id, privilege) 13 | values (21, 'UNITY'); 14 | insert into user_privilege(user_id, privilege) 15 | values (21, 'MOBILE'); 16 | 17 | -- unity, password is Unity#2013 ( ROLE_UNITY) 18 | insert into user_(id, guid, create_time, email, password, phone, username, default_user) 19 | values (22, '55b713df1c6f423e842ad68668523c49', now(), 'unity@andaily.com', 20 | '$2a$10$M/bdEKNH12ksSmMgt0p3YeSjW4C5auAjE8by9BY6oEkHTjGKNDqTO', '', 'unity', 0); 21 | 22 | insert into user_privilege(user_id, privilege) 23 | values (22, 'UNITY'); 24 | 25 | -- mobile, password is Mobile*2013 ( ROLE_MOBILE) 26 | insert into user_(id, guid, create_time, email, password, phone, username, default_user) 27 | values (23, '612025cb3f964a64a48bbdf77e53c2c1', now(), 'mobile@andaily.com', 28 | '$2a$10$MJKW44F.e.UH.54OY36b6eCPpp8KRszL3vAgqLyL1WWnpbGp7A8zW', '', 'mobile', 0); 29 | 30 | insert into user_privilege(user_id, privilege) 31 | values (23, 'MOBILE'); 32 | 33 | 34 | -- initial oauth client details test data 35 | -- 'unity-client' support browser device visit, secret: unity 36 | -- 'mobile-client' only support mobile-device visit, secret: mobile 37 | truncate oauth2_registered_client; 38 | insert into oauth2_registered_client 39 | (id, create_time, client_id, client_secret, client_name, client_authentication_methods, 40 | authorization_grant_types, redirect_uris, post_logout_redirect_uris, scopes, client_settings, token_settings) 41 | values ('851eee5eaba94b0cacca53a3ef543423', now(), 'unity-client', 42 | '$2a$10$QQTKDdNfj9sPjak6c8oWaumvTsa10MxOBOV6BW3DvLWU6VrjDfDam', 43 | 'Unity-Client', 44 | 'client_secret_post,client_secret_jwt,client_secret_basic', 45 | 'refresh_token,urn:ietf:params:oauth:grant-type:device_code,authorization_code', 46 | 'http://localhost:8080/unity/dashboard', null, 'openid,profile,email', 47 | '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":true,"settings.client.require-authorization-consent":true}', 48 | '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","ES256"],"settings.token.access-token-time-to-live":["java.time.Duration",7200.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",172800.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",120.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}'), 49 | ('aedd67f6dae441b99e3a0fb27889ce12', now(), 'mobile-client', 50 | '$2a$10$uLvpxfvm3CuUyjIvYq7a9OUmd9b3tHFKrUaMyU/jC01thrTdkBDVm', 51 | 'Mobile-Client', 52 | 'client_secret_post,client_secret_basic', 53 | 'refresh_token,password', 54 | null, null, 'openid,profile', 55 | '{"@class":"java.util.Collections$UnmodifiableMap","settings.client.require-proof-key":true,"settings.client.require-authorization-consent":true}', 56 | '{"@class":"java.util.Collections$UnmodifiableMap","settings.token.reuse-refresh-tokens":true,"settings.token.id-token-signature-algorithm":["org.springframework.security.oauth2.jose.jws.SignatureAlgorithm","ES256"],"settings.token.access-token-time-to-live":["java.time.Duration",7200.000000000],"settings.token.access-token-format":{"@class":"org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat","value":"self-contained"},"settings.token.refresh-token-time-to-live":["java.time.Duration",172800.000000000],"settings.token.authorization-code-time-to-live":["java.time.Duration",120.000000000],"settings.token.device-code-time-to-live":["java.time.Duration",300.000000000]}'); 57 | 58 | 59 | -------------------------------------------------------------------------------- /others/database/oauth.ddl: -------------------------------------------------------------------------------- 1 | -- 2 | -- Oauth sql -- MYSQL v3.0.0 3 | -- 4 | 5 | Drop table if exists oauth2_registered_client; 6 | CREATE TABLE oauth2_registered_client 7 | ( 8 | id varchar(100) NOT NULL, 9 | archived TINYINT(1) DEFAULT '0', 10 | create_time DATETIME, 11 | updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 12 | client_id varchar(100) NOT NULL, 13 | client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, 14 | client_secret varchar(200) DEFAULT NULL, 15 | client_secret_expires_at datetime DEFAULT NULL, 16 | client_name varchar(200) NOT NULL, 17 | client_authentication_methods varchar(1000) NOT NULL, 18 | authorization_grant_types varchar(1000) NOT NULL, 19 | redirect_uris varchar(1000) DEFAULT NULL, 20 | post_logout_redirect_uris varchar(1000) DEFAULT NULL, 21 | scopes varchar(1000) NOT NULL, 22 | client_settings varchar(2000) NOT NULL, 23 | token_settings varchar(2000) NOT NULL, 24 | PRIMARY KEY (id) 25 | ) ENGINE = InnoDB 26 | DEFAULT CHARSET = utf8; 27 | 28 | -- authorization 29 | Drop table if exists oauth2_authorization; 30 | CREATE TABLE oauth2_authorization 31 | ( 32 | id varchar(100) NOT NULL, 33 | registered_client_id varchar(100) NOT NULL, 34 | principal_name varchar(200) NOT NULL, 35 | authorization_grant_type varchar(100) NOT NULL, 36 | authorized_scopes varchar(1000) DEFAULT NULL, 37 | attributes blob DEFAULT NULL, 38 | state varchar(500) DEFAULT NULL, 39 | authorization_code_value blob DEFAULT NULL, 40 | authorization_code_issued_at datetime DEFAULT NULL, 41 | authorization_code_expires_at datetime DEFAULT NULL, 42 | authorization_code_metadata blob DEFAULT NULL, 43 | access_token_value blob DEFAULT NULL, 44 | access_token_issued_at datetime DEFAULT NULL, 45 | access_token_expires_at datetime DEFAULT NULL, 46 | access_token_metadata blob DEFAULT NULL, 47 | access_token_type varchar(100) DEFAULT NULL, 48 | access_token_scopes varchar(1000) DEFAULT NULL, 49 | oidc_id_token_value blob DEFAULT NULL, 50 | oidc_id_token_issued_at datetime DEFAULT NULL, 51 | oidc_id_token_expires_at datetime DEFAULT NULL, 52 | oidc_id_token_metadata blob DEFAULT NULL, 53 | refresh_token_value blob DEFAULT NULL, 54 | refresh_token_issued_at datetime DEFAULT NULL, 55 | refresh_token_expires_at datetime DEFAULT NULL, 56 | refresh_token_metadata blob DEFAULT NULL, 57 | user_code_value blob DEFAULT NULL, 58 | user_code_issued_at datetime DEFAULT NULL, 59 | user_code_expires_at datetime DEFAULT NULL, 60 | user_code_metadata blob DEFAULT NULL, 61 | device_code_value blob DEFAULT NULL, 62 | device_code_issued_at datetime DEFAULT NULL, 63 | device_code_expires_at datetime DEFAULT NULL, 64 | device_code_metadata blob DEFAULT NULL, 65 | updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 66 | PRIMARY KEY (id) 67 | ) ENGINE = InnoDB 68 | DEFAULT CHARSET = utf8; 69 | 70 | 71 | -- authorization consent 72 | Drop table if exists oauth2_authorization_consent; 73 | CREATE TABLE oauth2_authorization_consent 74 | ( 75 | registered_client_id varchar(100) NOT NULL, 76 | principal_name varchar(200) NOT NULL, 77 | authorities varchar(1000) NOT NULL, 78 | updated_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, 79 | PRIMARY KEY (registered_client_id, principal_name) 80 | ) ENGINE = InnoDB 81 | DEFAULT CHARSET = utf8; 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/UserDto.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2015 MONKEYK Information Technology Co. Ltd 3 | * www.monkeyk.com 4 | * All rights reserved. 5 | * 6 | * This software is the confidential and proprietary information of 7 | * MONKEYK Information Technology Co. Ltd ("Confidential Information"). 8 | * You shall not disclose such Confidential Information and shall use 9 | * it only in accordance with the terms of the license agreement you 10 | * entered into with MONKEYK Information Technology Co. Ltd. 11 | */ 12 | package com.monkeyk.sos.service.dto; 13 | 14 | import com.monkeyk.sos.domain.user.Privilege; 15 | import com.monkeyk.sos.domain.user.User; 16 | 17 | import java.io.Serial; 18 | import java.io.Serializable; 19 | import java.time.format.DateTimeFormatter; 20 | import java.util.ArrayList; 21 | import java.util.List; 22 | 23 | /** 24 | * 2016/3/12 25 | * 26 | * @author Shengzhao Li 27 | */ 28 | public class UserDto implements Serializable { 29 | @Serial 30 | private static final long serialVersionUID = -2502329463915439215L; 31 | 32 | 33 | private String guid; 34 | 35 | private String username; 36 | 37 | private String phone; 38 | private String email; 39 | 40 | private String createTime; 41 | 42 | private List privileges = new ArrayList<>(); 43 | 44 | /** 45 | * true 启用 46 | * false 禁用 47 | * 48 | * @since 3.0.0 49 | */ 50 | private boolean enabled = true; 51 | 52 | /** 53 | * 别名 54 | * 55 | * @see org.springframework.security.oauth2.core.oidc.OidcScopes#PROFILE 56 | * @since 3.0.0 57 | */ 58 | private String nickname; 59 | 60 | /** 61 | * 地址 62 | * 63 | * @see org.springframework.security.oauth2.core.oidc.OidcScopes#ADDRESS 64 | * @since 3.0.0 65 | */ 66 | private String address; 67 | 68 | 69 | public UserDto() { 70 | } 71 | 72 | 73 | public UserDto(User user) { 74 | this.guid = user.guid(); 75 | this.username = user.username(); 76 | this.phone = user.phone(); 77 | this.email = user.email(); 78 | 79 | this.privileges = user.privileges(); 80 | this.createTime = user.createTime().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME); 81 | 82 | this.enabled = user.enabled(); 83 | this.address = user.address(); 84 | this.nickname = user.nickname(); 85 | } 86 | 87 | 88 | public boolean isEnabled() { 89 | return enabled; 90 | } 91 | 92 | public void setEnabled(boolean enabled) { 93 | this.enabled = enabled; 94 | } 95 | 96 | public String getNickname() { 97 | return nickname; 98 | } 99 | 100 | public void setNickname(String nickname) { 101 | this.nickname = nickname; 102 | } 103 | 104 | public String getAddress() { 105 | return address; 106 | } 107 | 108 | public void setAddress(String address) { 109 | this.address = address; 110 | } 111 | 112 | public String getCreateTime() { 113 | return createTime; 114 | } 115 | 116 | public void setCreateTime(String createTime) { 117 | this.createTime = createTime; 118 | } 119 | 120 | public String getGuid() { 121 | return guid; 122 | } 123 | 124 | public void setGuid(String guid) { 125 | this.guid = guid; 126 | } 127 | 128 | public String getUsername() { 129 | return username; 130 | } 131 | 132 | public void setUsername(String username) { 133 | this.username = username; 134 | } 135 | 136 | public String getPhone() { 137 | return phone; 138 | } 139 | 140 | public void setPhone(String phone) { 141 | this.phone = phone; 142 | } 143 | 144 | public String getEmail() { 145 | return email; 146 | } 147 | 148 | public void setEmail(String email) { 149 | this.email = email; 150 | } 151 | 152 | public List getPrivileges() { 153 | return privileges; 154 | } 155 | 156 | public void setPrivileges(List privileges) { 157 | this.privileges = privileges; 158 | } 159 | 160 | public static List toDtos(List users) { 161 | List dtos = new ArrayList<>(users.size()); 162 | for (User user : users) { 163 | dtos.add(new UserDto(user)); 164 | } 165 | return dtos; 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /src/main/resources/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Home - Spring Security&OAuth2.1 13 | 14 | 15 | 16 | 17 |

Spring Security&OAuth2.1 18 | 3.0.0 19 |

20 | 21 |
22 | Logged: 23 |
24 | Authorities: 26 | 27 |
28 | 29 |
30 |
31 |
32 |
33 | 操作说明 34 |
    35 |
  1. 36 |

    37 | 菜单 User 是不需要OAuth 验证即可访问的(即公开的resource); 用于管理用户信息(添加,删除等). 38 |

    39 |
  2. 40 |
  3. 41 |

    42 | 菜单 Unity 与 Mobile 需要登录认证后才能访问(即受保护的resource);
    43 | Unity 需要 [ROLE_UNITY] 权限, Mobile 需要 [ROLE_MOBILE] 权限. 44 |

    45 |
  4. 46 |
  5. 47 |

    48 | device_login 用于在设备认证时,输入用户码(user_code)完成授权. 49 |

    50 |
  6. 51 |
  7. 52 |

    53 | 在使用之前, 建议先了解OAuth2.1支持的grant_type, 请访问 https://andaily.com/blog/?p=103 55 |

    56 |
  8. 57 |
  9. 58 |

    59 | 在项目的 others目录里有 oauth2.1-flow.md文件, 里面有测试的URL地址(包括浏览器与客户端的),
    62 | 若想访问 Unity 与 Mobile, 则先用基于浏览器的测试URL 访问,等验证通过后即可访问(注意不同的账号对应的权限). 63 |

    64 |
  10. 65 |
  11. 66 |

    67 | 若需要自定义client_details数据并进行测试, 68 | 可进入client_details去手动添加client_details或删除已创建的client_details. 69 |

    70 |
  12. 71 |
72 |
73 |
74 | 菜单 75 |
    76 |
  • 77 |

    78 | API - 查看提供的API文档 80 |

    81 |
  • 82 |
  • 83 |

    84 | client_details - 管理ClientDetails 85 |

    86 |
  • 87 |
  • 88 |

    89 | device_login - [device_code]流程中使用 OAuth2.1新增 91 |

    92 |
  • 93 |
  • 94 |

    95 | User - 管理User 96 |

    97 |
  • 98 |
  • 99 |

    100 | Unity - Unity 资源(resource), 需要具有 [ROLE_UNITY] 权限才能访问 101 |

    102 |
  • 103 |
  • 104 |

    105 | Mobile - Mobile资源(resource), 需要具有 [ROLE_MOBILE] 权限才能访问 106 |

    107 |
  • 108 |
109 |
110 | 111 | 112 |
113 | 114 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/web/controller/OauthClientDetailsDtoValidator.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.monkeyk.sos.service.dto.OauthClientDetailsDto; 4 | import com.monkeyk.sos.service.OauthService; 5 | 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.security.oauth2.core.oidc.OidcScopes; 9 | import org.springframework.stereotype.Component; 10 | import org.springframework.validation.Errors; 11 | import org.springframework.validation.Validator; 12 | 13 | /** 14 | * @author Shengzhao Li 15 | */ 16 | @Component 17 | public class OauthClientDetailsDtoValidator implements Validator { 18 | 19 | 20 | @Autowired 21 | private OauthService oauthService; 22 | 23 | @Override 24 | public boolean supports(Class clazz) { 25 | return OauthClientDetailsDto.class.equals(clazz); 26 | } 27 | 28 | @Override 29 | public void validate(Object target, Errors errors) { 30 | OauthClientDetailsDto clientDetailsDto = (OauthClientDetailsDto) target; 31 | 32 | validateClientId(clientDetailsDto, errors); 33 | validateClientSecret(clientDetailsDto, errors); 34 | 35 | validateGrantTypes(clientDetailsDto, errors); 36 | //v3.0.0 added 37 | validateClientName(clientDetailsDto, errors); 38 | validateScopes(clientDetailsDto, errors); 39 | validateMethods(clientDetailsDto, errors); 40 | } 41 | 42 | 43 | /** 44 | * @since 3.0.0 45 | */ 46 | private void validateMethods(OauthClientDetailsDto clientDetailsDto, Errors errors) { 47 | String methods = clientDetailsDto.getClientAuthenticationMethods(); 48 | if (StringUtils.isBlank(methods)) { 49 | errors.reject(null, "authentication_methods is required"); 50 | } 51 | } 52 | 53 | 54 | /** 55 | * @since 3.0.0 56 | */ 57 | private void validateScopes(OauthClientDetailsDto clientDetailsDto, Errors errors) { 58 | String scopes = clientDetailsDto.getScopes(); 59 | if (StringUtils.isBlank(scopes)) { 60 | errors.reject(null, "scopes is required"); 61 | } else if (!scopes.contains(OidcScopes.OPENID)) { 62 | errors.reject(null, "scopes [openid] must be selected"); 63 | } 64 | } 65 | 66 | /** 67 | * @since 3.0.0 68 | */ 69 | private void validateClientName(OauthClientDetailsDto clientDetailsDto, Errors errors) { 70 | String clientName = clientDetailsDto.getClientName(); 71 | if (StringUtils.isBlank(clientName)) { 72 | errors.reject(null, "client_name is required"); 73 | } 74 | } 75 | 76 | private void validateGrantTypes(OauthClientDetailsDto clientDetailsDto, Errors errors) { 77 | final String grantTypes = clientDetailsDto.getAuthorizationGrantTypes(); 78 | if (StringUtils.isEmpty(grantTypes)) { 79 | errors.rejectValue("authorizationGrantTypes", null, "grant_type(s) is required"); 80 | return; 81 | } 82 | 83 | if ("refresh_token".equalsIgnoreCase(grantTypes)) { 84 | errors.rejectValue("authorizationGrantTypes", null, "grant_type(s) 不能只是[refresh_token]"); 85 | } 86 | } 87 | 88 | private void validateClientSecret(OauthClientDetailsDto clientDetailsDto, Errors errors) { 89 | final String clientSecret = clientDetailsDto.getClientSecret(); 90 | if (StringUtils.isEmpty(clientSecret)) { 91 | errors.rejectValue("clientSecret", null, "client_secret is required"); 92 | return; 93 | } 94 | 95 | if (clientSecret.length() < 10) { 96 | errors.rejectValue("clientSecret", null, "client_secret 长度至少10位"); 97 | } 98 | } 99 | 100 | private void validateClientId(OauthClientDetailsDto clientDetailsDto, Errors errors) { 101 | final String clientId = clientDetailsDto.getClientId(); 102 | if (StringUtils.isEmpty(clientId)) { 103 | errors.rejectValue("clientId", null, "client_id is required"); 104 | return; 105 | } 106 | 107 | if (clientId.length() < 10) { 108 | errors.rejectValue("clientId", null, "client_id 长度至少10位"); 109 | return; 110 | } 111 | 112 | OauthClientDetailsDto clientDetailsDto1 = oauthService.loadOauthClientDetailsDto(clientId); 113 | if (clientDetailsDto1 != null) { 114 | errors.rejectValue("clientId", null, "client_id [" + clientId + "] 已存在"); 115 | } 116 | 117 | } 118 | } -------------------------------------------------------------------------------- /src/main/resources/templates/consent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 授权确认 - Spring Security&OAuth2.1 7 | 8 | 9 | 10 |
11 |
12 |

授权确认

13 |
14 |
15 |
16 |

17 | The application 18 | 19 | wants to access your account 20 | 21 |

22 |
23 |
24 |
25 |
26 |

27 | You have provided the code 28 | . 29 | Verify that this code matches what is shown on your device. 30 |

31 |
32 |
33 |
34 |
35 |

36 | The following permissions are requested by the above app.
37 | Please review these and consent if you approve. 38 |

39 |
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 |
49 | 55 | 57 |

58 |
59 | 60 |

61 | You have already granted the following permissions to the above app: 62 |

63 |
64 | 70 | 72 |

73 |
74 | 75 |
76 | 79 |
80 | 85 |
86 |
87 |
88 |
89 |
90 |

91 | 92 | Your consent to provide access is required.
93 | If you do not approve, click Cancel, in which case no information will be shared with the app. 94 |
95 |

96 |
97 |
98 |
99 |
100 | 101 | 102 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/service/JwtBearerFlowTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service; 2 | 3 | import com.nimbusds.jose.*; 4 | import com.nimbusds.jose.crypto.ECDSASigner; 5 | import com.nimbusds.jose.crypto.MACSigner; 6 | import com.nimbusds.jose.crypto.RSASSASigner; 7 | import com.nimbusds.jose.jwk.JWK; 8 | import com.nimbusds.jwt.JWTClaimsSet; 9 | import org.junit.jupiter.api.Test; 10 | 11 | import java.time.Instant; 12 | import java.util.Date; 13 | 14 | import static com.monkeyk.sos.web.controller.JwtBearerJwksController.ES256_KEY; 15 | import static com.monkeyk.sos.web.controller.JwtBearerJwksController.RS256_KEY; 16 | import static org.junit.jupiter.api.Assertions.assertNotNull; 17 | 18 | /** 19 | * 2023/10/24 10:25 20 | * 21 | * @author Shengzhao Li 22 | * @since 3.0.0 23 | */ 24 | public class JwtBearerFlowTest { 25 | 26 | 27 | /** 28 | * MAC 生成 assertion 29 | * HS256 30 | * method: CLIENT_SECRET_JWT 31 | * 32 | * @throws Exception e 33 | */ 34 | @Test 35 | void macAssertion() throws Exception { 36 | 37 | String clientId = "vLIXDF9GXg6Psfh1uzwVFUj0fucX2Zn9"; 38 | // client_secret 加密后的值 39 | String macSecret = "$2a$10$kjjdfA8SIuhlVx0q4B1GYeU..9TNU9.Aj6Vdc2v/iQTJhhmT/0xCi"; 40 | 41 | JWSSigner jwsSigner = new MACSigner(macSecret); 42 | 43 | JWSHeader header = new JWSHeader(JWSAlgorithm.HS256); 44 | 45 | 46 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 47 | .subject(clientId) 48 | .issuer(clientId) 49 | .audience("http://127.0.0.1:8080") 50 | .expirationTime(Date.from(Instant.now().plusSeconds(300L))) 51 | .build(); 52 | 53 | Payload payload = new Payload(claimsSet.toJSONObject()); 54 | 55 | JWSObject jwsObject = new JWSObject(header, payload); 56 | //签名 57 | jwsObject.sign(jwsSigner); 58 | 59 | // 将 assertion 复制放到请求参数 client_assertion 的值 60 | String assertion = jwsObject.serialize(); 61 | assertNotNull(assertion); 62 | // System.out.println(assertion); 63 | 64 | } 65 | 66 | 67 | /** 68 | * RSA 生成 assertion 69 | * SignatureAlgorithm: RS256 70 | * method: PRIVATE_KEY_JWT 71 | * 72 | * @throws Exception e 73 | */ 74 | @Test 75 | void rs256Assertion() throws Exception { 76 | 77 | JWK rsJwk = JWK.parse(RS256_KEY); 78 | 79 | JWSSigner jwsSigner = new RSASSASigner(rsJwk.toRSAKey()); 80 | JWSHeader header = new JWSHeader(JWSAlgorithm.RS256); 81 | 82 | String clientId = "dofOx6hjxlWw9qe2bnFvqbiPhuWwGWdn"; 83 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 84 | .subject(clientId) 85 | .issuer(clientId) 86 | .audience("http://127.0.0.1:8080") 87 | .expirationTime(Date.from(Instant.now().plusSeconds(300L))) 88 | .build(); 89 | 90 | Payload payload = new Payload(claimsSet.toJSONObject()); 91 | 92 | JWSObject jwsObject = new JWSObject(header, payload); 93 | //签名 94 | jwsObject.sign(jwsSigner); 95 | 96 | // 将 assertion 复制放到请求参数 client_assertion 的值 97 | String assertion = jwsObject.serialize(); 98 | assertNotNull(assertion); 99 | // System.out.println(assertion); 100 | 101 | } 102 | 103 | /** 104 | * ES 生成 assertion 105 | * SignatureAlgorithm: ES256 106 | * method: PRIVATE_KEY_JWT 107 | * 108 | * @throws Exception e 109 | */ 110 | @Test 111 | void es256Assertion() throws Exception { 112 | 113 | JWK rsJwk = JWK.parse(ES256_KEY); 114 | 115 | JWSSigner jwsSigner = new ECDSASigner(rsJwk.toECKey()); 116 | JWSHeader header = new JWSHeader(JWSAlgorithm.ES256); 117 | 118 | String clientId = "pRC9j1mwGNMuchoI8nwJ6blr1lmPBLha"; 119 | JWTClaimsSet claimsSet = new JWTClaimsSet.Builder() 120 | .subject(clientId) 121 | .issuer(clientId) 122 | .audience("http://127.0.0.1:8080") 123 | .expirationTime(Date.from(Instant.now().plusSeconds(300L))) 124 | .build(); 125 | 126 | Payload payload = new Payload(claimsSet.toJSONObject()); 127 | 128 | JWSObject jwsObject = new JWSObject(header, payload); 129 | //签名 130 | jwsObject.sign(jwsSigner); 131 | 132 | // 将 assertion 复制放到请求参数 client_assertion 的值 133 | String assertion = jwsObject.serialize(); 134 | assertNotNull(assertion); 135 | // System.out.println(assertion); 136 | 137 | } 138 | 139 | } 140 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/config/WebSecurityConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.config; 2 | 3 | import com.monkeyk.sos.web.context.SOSContextHolder; 4 | import org.springframework.beans.factory.annotation.Value; 5 | import org.springframework.context.annotation.Bean; 6 | import org.springframework.context.annotation.Configuration; 7 | import org.springframework.core.annotation.Order; 8 | import org.springframework.http.HttpMethod; 9 | import org.springframework.security.config.annotation.web.builders.HttpSecurity; 10 | import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; 11 | import org.springframework.security.config.annotation.web.configuration.WebSecurityCustomizer; 12 | import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; 13 | import org.springframework.security.crypto.password.PasswordEncoder; 14 | import org.springframework.security.web.SecurityFilterChain; 15 | 16 | /** 17 | * 2016/4/3 18 | *

19 | * Replace security.xml 20 | * 21 | * @author Shengzhao Li 22 | */ 23 | @EnableWebSecurity 24 | @Configuration(proxyBeanMethods = false) 25 | public class WebSecurityConfigurer { 26 | 27 | 28 | /** 29 | * 需要调试时 可把此配置参数换为 true 30 | * 31 | * @since 3.0.0 32 | */ 33 | @Value("${sos.spring.web.security.debug:false}") 34 | private boolean springWebSecurityDebug; 35 | 36 | 37 | /** 38 | * 扩展默认的 Web安全配置项 39 | *

40 | * defaultSecurityFilterChain 41 | * 42 | * @throws Exception e 43 | * @since 3.0.0 44 | */ 45 | @Bean 46 | @Order(2) 47 | public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { 48 | 49 | http.csrf(csrfConfigurer -> { 50 | csrfConfigurer.ignoringRequestMatchers("/oauth2/rest_token"); 51 | }); 52 | 53 | http.authorizeHttpRequests(matcherRegistry -> { 54 | // permitAll() 的URL路径属于公开访问,不需要权限 55 | matcherRegistry 56 | .requestMatchers("/favicon.ico*", "/oauth2/rest_token*", "*.js", "*.css").permitAll() 57 | .requestMatchers("/api/public/**").permitAll() 58 | .requestMatchers(HttpMethod.GET, "/login*").anonymous() 59 | 60 | // /user/ 开头的URL需要 ADMIN 权限 61 | .requestMatchers("/user/**").hasAnyRole("ADMIN") 62 | // 所有以 /unity/ 开头的 URL属于 UNITY 权限 63 | .requestMatchers("/unity/**").hasAnyRole("UNITY") 64 | // 所有以 /m/ 开头的 URL属于 MOBILE 权限 65 | .requestMatchers("/m/**").hasAnyRole("MOBILE") 66 | // anyRequest() 放最后 67 | .anyRequest().authenticated(); 68 | }); 69 | 70 | http.formLogin(formLoginConfigurer -> { 71 | formLoginConfigurer 72 | .loginPage("/login") 73 | .loginProcessingUrl("/signin") 74 | .failureUrl("/login?error_failed=true") 75 | // .defaultSuccessUrl("/") 76 | .usernameParameter("oidc_user") 77 | .passwordParameter("oidcPwd"); 78 | 79 | }); 80 | 81 | http.logout(logoutConfigurer -> { 82 | logoutConfigurer.logoutUrl("/signout") 83 | .deleteCookies("JSESSIONID") 84 | .logoutSuccessUrl("/"); 85 | }); 86 | 87 | // http.sessionManagement(configurer -> { 88 | // configurer.maximumSessions(1).maxSessionsPreventsLogin(true); 89 | // }); 90 | 91 | // http.exceptionHandling(configurer -> { 92 | // configurer.accessDeniedHandler((request, response, accessDeniedException) -> { 93 | // response.sendRedirect("/access_denied"); 94 | // }); 95 | // }); 96 | 97 | return http.build(); 98 | } 99 | 100 | 101 | /** 102 | * 安全配置自定义扩展 103 | * 104 | * @return WebSecurityCustomizer 105 | * @since 3.0.0 106 | */ 107 | @Bean 108 | public WebSecurityCustomizer webSecurityCustomizer() { 109 | return web -> web.debug(this.springWebSecurityDebug); 110 | } 111 | 112 | 113 | /** 114 | * BCrypt 加密 115 | * 116 | * @return PasswordEncoder 117 | */ 118 | @Bean 119 | public PasswordEncoder passwordEncoder() { 120 | return new BCryptPasswordEncoder(); 121 | } 122 | 123 | 124 | /** 125 | * SOSContextHolder bean 126 | * 127 | * @return SOSContextHolder bean 128 | * @since 2.0.1 129 | */ 130 | @Bean 131 | public SOSContextHolder sosContextHolder() { 132 | return new SOSContextHolder(); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/web/controller/resource/UnityControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller.resource; 2 | 3 | import com.monkeyk.sos.service.OauthService; 4 | import com.monkeyk.sos.service.UserService; 5 | import com.monkeyk.sos.service.dto.UserJsonDto; 6 | import com.monkeyk.sos.web.controller.OauthClientDetailsDtoValidator; 7 | import com.monkeyk.sos.web.controller.UserFormDtoValidator; 8 | import org.junit.jupiter.api.BeforeEach; 9 | import org.junit.jupiter.api.Test; 10 | import org.junit.jupiter.api.extension.ExtendWith; 11 | import org.mockito.Mockito; 12 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 13 | import org.springframework.boot.test.mock.mockito.MockBean; 14 | import org.springframework.http.MediaType; 15 | import org.springframework.restdocs.RestDocumentationContextProvider; 16 | import org.springframework.restdocs.RestDocumentationExtension; 17 | import org.springframework.security.crypto.password.PasswordEncoder; 18 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; 19 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 20 | import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; 21 | import org.springframework.test.context.junit.jupiter.SpringExtension; 22 | import org.springframework.test.web.servlet.MockMvc; 23 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 24 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 25 | import org.springframework.web.context.WebApplicationContext; 26 | 27 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; 28 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; 29 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; 30 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 31 | 32 | /** 33 | * 2023/10/19 17:31 34 | * 35 | * @author Shengzhao Li 36 | * @since 3.0.0 37 | */ 38 | @WebMvcTest 39 | @ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) 40 | class UnityControllerTest { 41 | 42 | 43 | private MockMvc mockMvc; 44 | 45 | 46 | @MockBean 47 | private UserService userService; 48 | 49 | @MockBean 50 | private RegisteredClientRepository registeredClientRepository; 51 | 52 | @MockBean 53 | private OAuth2AuthorizationConsentService consentService; 54 | 55 | @MockBean 56 | private OauthService oauthService; 57 | 58 | @MockBean 59 | private OauthClientDetailsDtoValidator oauthClientDetailsDtoValidator; 60 | 61 | @MockBean 62 | private UserFormDtoValidator userFormDtoValidator; 63 | 64 | 65 | @MockBean 66 | private PasswordEncoder passwordEncoder; 67 | 68 | 69 | @MockBean 70 | private AuthorizationServerSettings authorizationServerSettings; 71 | 72 | 73 | @BeforeEach 74 | public void setup(WebApplicationContext applicationContext, RestDocumentationContextProvider contextProvider) { 75 | this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext) 76 | .apply(documentationConfiguration(contextProvider)) 77 | .alwaysDo(result -> { 78 | result.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); 79 | }) 80 | .build(); 81 | } 82 | 83 | 84 | @Test 85 | void userInfo() throws Exception { 86 | 87 | 88 | UserJsonDto jsonDto = new UserJsonDto(); 89 | String username = "user111"; 90 | jsonDto.setUsername(username); 91 | jsonDto.setGuid("owwiwi0a0assdfsfs11"); 92 | jsonDto.setEmail("user111@cloudjac.com"); 93 | jsonDto.setPhone("13300002222"); 94 | jsonDto.getPrivileges().add("ROLE_USER"); 95 | 96 | Mockito.when(userService.loadCurrentUserJsonDto()).thenReturn(jsonDto); 97 | 98 | 99 | MockHttpServletRequestBuilder requestBuilder = get("/unity/user_info") 100 | .contentType(MediaType.APPLICATION_JSON); 101 | 102 | mockMvc.perform(requestBuilder) 103 | .andExpect(status().isOk()) 104 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 105 | // .andDo(print()) 106 | .andExpect(jsonPath("guid").exists()) 107 | .andExpect(jsonPath("username").value(username)) 108 | .andExpect(jsonPath("email").exists()) 109 | .andExpect(jsonPath("phone").exists()) 110 | //生成文档需要加上这句 111 | .andDo(document("{ClassName}/{methodName}")); 112 | 113 | 114 | } 115 | 116 | 117 | } -------------------------------------------------------------------------------- /src/main/resources/templates/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Authenticate - Spring Security&OAuth2.1 13 | 14 | 15 | 16 | 17 |

18 | 19 |
20 |
21 |
22 |
[U+P] Login
23 |
24 | 25 |
26 |
27 | 28 | 29 |
30 | 32 |
33 |
34 | 35 |
36 | 37 | 38 |
39 | 41 |
42 |
43 | 44 |
45 | 46 | 47 |
48 | 49 | 50 | Access denied !!! 52 | Authentication Failure 54 |
55 |
56 | 57 |
58 |
59 |
60 | 61 |
62 |
.well-known endpoint
63 |
64 |
65 |
OIDC 1.0
66 |
/.well-known/openid-configuration 67 |
68 |
OAuth 2.1
69 |
/.well-known/oauth-authorization-server 70 |
71 |
72 |
73 |
74 |
75 |
76 |

可以使用以下几个初始的账号进行登录:

77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 |
UsernamePasswordRemark
adminAdmin@2013All privileges, allow visit [Mobile] and [Unity] resources, manage user
unityUnity#2013Only allow visit [Unity] resource, support grant_type: 95 | authorization_code,refresh_token,device_code
mobileMobile*2013Only allow visit [Mobile] resource, support grant_type: password,refresh_token
104 |
105 |
106 | 107 |
108 | 109 | -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/dto/ClientSettingsDto.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.dto; 2 | 3 | import com.monkeyk.sos.infrastructure.SettingsUtils; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.springframework.security.oauth2.jose.jws.JwsAlgorithm; 6 | import org.springframework.security.oauth2.jose.jws.MacAlgorithm; 7 | import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm; 8 | import org.springframework.security.oauth2.server.authorization.settings.ClientSettings; 9 | 10 | import java.io.Serial; 11 | import java.io.Serializable; 12 | 13 | import static com.monkeyk.sos.domain.shared.SOSConstants.HS; 14 | import static org.springframework.security.oauth2.jose.jws.JwsAlgorithms.RS256; 15 | 16 | /** 17 | * 2023/10/13 11:52 18 | *

19 | * .requireProofKey(false) 20 | * .requireAuthorizationConsent(false); 21 | * 22 | * @author Shengzhao Li 23 | * @see org.springframework.security.oauth2.server.authorization.settings.ClientSettings 24 | * @since 3.0.0 25 | */ 26 | public class ClientSettingsDto implements Serializable { 27 | @Serial 28 | private static final long serialVersionUID = -7335241589844569340L; 29 | 30 | /** 31 | * 支持PKCE为true 32 | * 默认false 33 | */ 34 | private boolean requireProofKey; 35 | 36 | /** 37 | * 授权需要用户进行确认为true 38 | * 默认false 39 | */ 40 | private boolean requireAuthorizationConsent; 41 | 42 | /** 43 | * 若client有自定义的 jwk URL, 44 | * 则填写, jwt-bearer流程中会使用到(OAuth2.1新增) 45 | * 46 | * @since 3.0.0 47 | */ 48 | private String jwkSetUrl; 49 | 50 | /** 51 | * 设置生成 jwt token的算法, 52 | * 可选值来自 JwsAlgorithm 53 | * 54 | * @see JwsAlgorithm 55 | */ 56 | private String tokenEndpointAuthenticationSigningAlgorithm = RS256; 57 | 58 | 59 | public ClientSettingsDto() { 60 | } 61 | 62 | public ClientSettingsDto(String clientSettings) { 63 | ClientSettings settings = SettingsUtils.buildClientSettings(clientSettings); 64 | this.requireAuthorizationConsent = settings.isRequireAuthorizationConsent(); 65 | this.requireProofKey = settings.isRequireProofKey(); 66 | 67 | JwsAlgorithm jAlg = settings.getTokenEndpointAuthenticationSigningAlgorithm(); 68 | if (jAlg != null) { 69 | this.tokenEndpointAuthenticationSigningAlgorithm = jAlg.getName(); 70 | } 71 | this.jwkSetUrl = settings.getJwkSetUrl(); 72 | } 73 | 74 | public ClientSettings toSettings() { 75 | ClientSettings.Builder builder = ClientSettings.builder() 76 | .requireProofKey(requireProofKey) 77 | .requireAuthorizationConsent(requireAuthorizationConsent); 78 | //区分不同算法:对称/非对称 79 | if (tokenEndpointAuthenticationSigningAlgorithm.startsWith(HS)) { 80 | builder.tokenEndpointAuthenticationSigningAlgorithm(MacAlgorithm.valueOf(tokenEndpointAuthenticationSigningAlgorithm)); 81 | } else { 82 | builder.tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.valueOf(tokenEndpointAuthenticationSigningAlgorithm)); 83 | } 84 | if (StringUtils.isNotBlank(jwkSetUrl)) { 85 | builder.jwkSetUrl(jwkSetUrl); 86 | } 87 | return builder.build(); 88 | } 89 | 90 | 91 | public boolean isRequireProofKey() { 92 | return requireProofKey; 93 | } 94 | 95 | public void setRequireProofKey(boolean requireProofKey) { 96 | this.requireProofKey = requireProofKey; 97 | } 98 | 99 | public boolean isRequireAuthorizationConsent() { 100 | return requireAuthorizationConsent; 101 | } 102 | 103 | public void setRequireAuthorizationConsent(boolean requireAuthorizationConsent) { 104 | this.requireAuthorizationConsent = requireAuthorizationConsent; 105 | } 106 | 107 | public String getJwkSetUrl() { 108 | return jwkSetUrl; 109 | } 110 | 111 | public void setJwkSetUrl(String jwkSetUrl) { 112 | this.jwkSetUrl = jwkSetUrl; 113 | } 114 | 115 | public String getTokenEndpointAuthenticationSigningAlgorithm() { 116 | return tokenEndpointAuthenticationSigningAlgorithm; 117 | } 118 | 119 | public void setTokenEndpointAuthenticationSigningAlgorithm(String tokenEndpointAuthenticationSigningAlgorithm) { 120 | this.tokenEndpointAuthenticationSigningAlgorithm = tokenEndpointAuthenticationSigningAlgorithm; 121 | } 122 | 123 | @Override 124 | public String toString() { 125 | return "{" + 126 | "requireProofKey=" + requireProofKey + 127 | ", requireAuthorizationConsent=" + requireAuthorizationConsent + 128 | // ", jwkSetUrl='" + jwkSetUrl + '\'' + 129 | ", tokenEndpointAuthenticationSigningAlgorithm='" + tokenEndpointAuthenticationSigningAlgorithm + '\'' + 130 | '}'; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/resources/templates/user_form.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | Add User - Spring Security&OAuth2.1 10 | 11 | 12 | 13 | 14 | Home 15 | 16 |

Add User

17 | 18 |
19 |
20 | 21 | 22 |
23 | 25 | 26 |

Username, unique.

27 |
28 |
29 |
30 | 31 | 32 |
33 | 35 | 36 |

Password, length >= 10 .

37 |
38 |
39 |
40 | 41 | 42 |
43 | 46 | 49 | 50 |

Select Privilege(s).

51 |
52 |
53 |
54 | 55 | 56 |
57 | 60 | 63 | 64 |

Enable/Disable the user

65 |
66 |
67 | 68 | 69 |
70 | 71 | 72 |
73 | 74 | 75 |

User phone, optional.

76 |
77 |
78 |
79 | 80 | 81 |
82 | 83 | 84 |

User email, optional.

85 |
86 |
87 |
88 | 89 | 90 |
91 | 92 | 93 |

User nickname, optional.

94 |
95 |
96 |
97 | 98 | 99 |
100 | 101 | 102 |

User address, optional.

103 |
104 |
105 | 106 |
107 |
108 |
109 | 110 |
111 |
112 | 113 | 114 |
115 |
116 |
117 | 118 | Cancel 119 |
120 |
121 |
122 | 123 | 124 |
125 | 126 | -------------------------------------------------------------------------------- /src/test/java/com/monkeyk/sos/web/controller/OAuthRestControllerTest.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.web.controller; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | import com.monkeyk.sos.service.OauthService; 5 | import com.monkeyk.sos.service.UserService; 6 | import org.junit.jupiter.api.BeforeEach; 7 | import org.junit.jupiter.api.Disabled; 8 | import org.junit.jupiter.api.Test; 9 | import org.junit.jupiter.api.extension.ExtendWith; 10 | import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; 11 | import org.springframework.boot.test.mock.mockito.MockBean; 12 | import org.springframework.http.MediaType; 13 | import org.springframework.restdocs.RestDocumentationContextProvider; 14 | import org.springframework.restdocs.RestDocumentationExtension; 15 | import org.springframework.security.crypto.password.PasswordEncoder; 16 | import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService; 17 | import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository; 18 | import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings; 19 | import org.springframework.test.context.junit.jupiter.SpringExtension; 20 | import org.springframework.test.web.servlet.MockMvc; 21 | import org.springframework.test.web.servlet.request.MockHttpServletRequestBuilder; 22 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 23 | import org.springframework.web.context.WebApplicationContext; 24 | 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | import static org.junit.jupiter.api.Assertions.*; 29 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.document; 30 | import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; 31 | import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; 32 | import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print; 33 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; 34 | import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath; 35 | 36 | /** 37 | * 2023/10/19 18:11 38 | * 39 | * @author Shengzhao Li 40 | * @since 3.0.0 41 | */ 42 | @WebMvcTest 43 | @ExtendWith({RestDocumentationExtension.class, SpringExtension.class}) 44 | class OAuthRestControllerTest { 45 | 46 | 47 | private MockMvc mockMvc; 48 | 49 | 50 | @MockBean 51 | private UserService userService; 52 | 53 | @MockBean 54 | private RegisteredClientRepository registeredClientRepository; 55 | 56 | @MockBean 57 | private OAuth2AuthorizationConsentService consentService; 58 | 59 | @MockBean 60 | private OauthService oauthService; 61 | 62 | @MockBean 63 | private OauthClientDetailsDtoValidator oauthClientDetailsDtoValidator; 64 | 65 | @MockBean 66 | private UserFormDtoValidator userFormDtoValidator; 67 | 68 | 69 | @MockBean 70 | private PasswordEncoder passwordEncoder; 71 | 72 | @MockBean 73 | private AuthorizationServerSettings authorizationServerSettings; 74 | 75 | 76 | @BeforeEach 77 | public void setup(WebApplicationContext applicationContext, RestDocumentationContextProvider contextProvider) { 78 | this.mockMvc = MockMvcBuilders.webAppContextSetup(applicationContext) 79 | .apply(documentationConfiguration(contextProvider)) 80 | .alwaysDo(result -> { 81 | result.getResponse().setContentType(MediaType.APPLICATION_JSON_VALUE); 82 | }) 83 | .build(); 84 | } 85 | 86 | 87 | @Test 88 | @Disabled 89 | void postAccessToken() throws Exception { 90 | 91 | 92 | Map parameters = new HashMap<>(); 93 | parameters.put("client_id", "clientxxxx"); 94 | 95 | ObjectMapper objectMapper = new ObjectMapper(); 96 | String content = objectMapper.writeValueAsString(parameters); 97 | assertNotNull(content); 98 | 99 | MockHttpServletRequestBuilder requestBuilder = post("/oauth2/rest_token") 100 | .contentType(MediaType.APPLICATION_JSON) 101 | .content(content); 102 | 103 | mockMvc.perform(requestBuilder) 104 | //.andDo(print()) 105 | .andExpect(status().isOk()) 106 | .andExpect(content().contentType(MediaType.APPLICATION_JSON)) 107 | .andExpect(jsonPath("access_token").exists()) 108 | // .andExpect(jsonPath("username").value(username)) 109 | .andExpect(jsonPath("refresh_token").exists()) 110 | .andExpect(jsonPath("scope").exists()) 111 | .andExpect(jsonPath("token_type").exists()) 112 | .andExpect(jsonPath("expires_in").exists()) 113 | //生成文档需要加上这句 114 | .andDo(document("{ClassName}/{methodName}")); 115 | 116 | 117 | } 118 | 119 | } -------------------------------------------------------------------------------- /src/main/java/com/monkeyk/sos/service/business/InlineAccessTokenInvoker.java: -------------------------------------------------------------------------------- 1 | package com.monkeyk.sos.service.business; 2 | 3 | import com.monkeyk.sos.service.dto.AccessTokenDto; 4 | import org.apache.commons.lang3.StringUtils; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | import org.springframework.beans.factory.InitializingBean; 8 | import org.springframework.security.authentication.AuthenticationManager; 9 | import org.springframework.util.Assert; 10 | 11 | import java.util.Map; 12 | 13 | import static org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames.*; 14 | 15 | 16 | /** 17 | * 2019/7/5 18 | * 19 | * @author Shengzhao Li 20 | * @see org.springframework.security.oauth2.server.authorization.web.OAuth2TokenEndpointFilter 21 | * @since 2.0.1 22 | */ 23 | public abstract class InlineAccessTokenInvoker implements InitializingBean { 24 | 25 | 26 | private static final Logger LOG = LoggerFactory.getLogger(InlineAccessTokenInvoker.class); 27 | 28 | 29 | // protected transient AuthenticationManager authenticationManager = SOSContextHolder.getBean(AuthenticationManager.class); 30 | 31 | // protected transient AuthorizationServerTokenServices tokenServices = SOSContextHolder.getBean(AuthorizationServerTokenServices.class); 32 | // 33 | // protected transient ClientDetailsService clientDetailsService = SOSContextHolder.getBean(ClientDetailsService.class); 34 | 35 | 36 | public InlineAccessTokenInvoker() { 37 | } 38 | 39 | 40 | /** 41 | * 根据不同的 params 生成 AccessTokenDto 42 | *

43 | * 适合以下几类 grant_type: 44 | * password 45 | * refresh_token 46 | * client_credentials 47 | * 48 | * @param params Params Map 49 | * @return AccessTokenDto instance 50 | */ 51 | public AccessTokenDto invoke(Map params) { 52 | 53 | if (params == null || params.isEmpty()) { 54 | throw new IllegalStateException("Null or empty params"); 55 | } 56 | 57 | String clientId = validateParams(params); 58 | 59 | // final ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId); 60 | // if (clientDetails == null) { 61 | // LOG.warn("Not found ClientDetails by clientId: {}", clientId); 62 | // return null; 63 | // } 64 | // 65 | // OAuth2RequestFactory oAuth2RequestFactory = createOAuth2RequestFactory(); 66 | // TokenGranter tokenGranter = getTokenGranter(oAuth2RequestFactory); 67 | // LOG.debug("Use TokenGranter: {}", tokenGranter); 68 | 69 | // TokenRequest tokenRequest = oAuth2RequestFactory.createTokenRequest(params, clientDetails); 70 | // final OAuth2AccessToken oAuth2AccessToken = tokenGranter.grant(getGrantType(params), tokenRequest); 71 | // 72 | // if (oAuth2AccessToken == null) { 73 | // LOG.warn("TokenGranter: {} grant OAuth2AccessToken null", tokenGranter); 74 | // return null; 75 | // } 76 | // AccessTokenDto accessTokenDto = new AccessTokenDto(oAuth2AccessToken); 77 | // LOG.debug("Invoked accessTokenDto: {}", accessTokenDto); 78 | // return accessTokenDto; 79 | throw new UnsupportedOperationException("Not yet implements"); 80 | } 81 | 82 | 83 | /** 84 | * 校验各类 grant_type时 需要的参数 85 | * 86 | * @param params params 87 | * @return clientId 88 | */ 89 | protected String validateParams(Map params) { 90 | //validate client_id 91 | String clientId = params.get(CLIENT_ID); 92 | if (StringUtils.isBlank(clientId)) { 93 | throw new IllegalStateException("Null or empty '" + CLIENT_ID + "' from params"); 94 | } 95 | 96 | //validate grant_type 97 | final String grantType = params.get(GRANT_TYPE); 98 | if (StringUtils.isBlank(grantType)) { 99 | throw new IllegalStateException("Null or empty '" + GRANT_TYPE + "' from params"); 100 | } 101 | 102 | //validate scope 103 | final String scope = params.get(SCOPE); 104 | if (StringUtils.isBlank(scope)) { 105 | throw new IllegalStateException("Null or empty '" + SCOPE + "' from params"); 106 | } 107 | 108 | return clientId; 109 | } 110 | 111 | 112 | /** 113 | * Get grant_type from params 114 | * 115 | * @param params Map 116 | * @return Grant Type 117 | */ 118 | protected String getGrantType(Map params) { 119 | return params.get(GRANT_TYPE); 120 | } 121 | 122 | 123 | // /** 124 | // * Get TokenGranter implement 125 | // * 126 | // * @return TokenGranter 127 | // */ 128 | // protected abstract TokenGranter getTokenGranter(OAuth2RequestFactory oAuth2RequestFactory); 129 | // 130 | // /** 131 | // * Create OAuth2RequestFactory 132 | // * 133 | // * @return OAuth2RequestFactory instance 134 | // */ 135 | // protected OAuth2RequestFactory createOAuth2RequestFactory() { 136 | // return new DefaultOAuth2RequestFactory(this.clientDetailsService); 137 | // } 138 | 139 | 140 | // public void setAuthenticationManager(AuthenticationManager authenticationManager) { 141 | // this.authenticationManager = authenticationManager; 142 | // } 143 | 144 | // public void setTokenServices(AuthorizationServerTokenServices tokenServices) { 145 | // this.tokenServices = tokenServices; 146 | // } 147 | // 148 | // public void setClientDetailsService(ClientDetailsService clientDetailsService) { 149 | // this.clientDetailsService = clientDetailsService; 150 | // } 151 | 152 | @Override 153 | public void afterPropertiesSet() throws Exception { 154 | // Assert.notNull(this.authenticationManager, "authenticationManager is null"); 155 | // Assert.notNull(this.tokenServices, "tokenServices is null"); 156 | 157 | // Assert.notNull(this.clientDetailsService, "clientDetailsService is null"); 158 | } 159 | } 160 | --------------------------------------------------------------------------------