diff --git a/README.md b/README.md index 03e5c45e1139aa32ea50ce745806a2bfaabd5af3..559f0bc99220ee9231636312ea5e17d855db6ffd 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@ ## oauth2-shiro -整合Apache Oltu 与 Shiro. 提供一个轻量的OAUTH2应用框架. +--- +整合Apache Oltu 与 Shiro. 提供一个轻量的OAUTH2应用框架实现. 并根据不同的应用场景提供不同的实现(如web场景,移动设备). -该项目与spring-oauth-server实现相同的需求与场合. -只是在实现上使用的技术不同(spring-oauth-server使用Spring Security + spring-security-oauth2实现; oauth2-oltu实现); +该项目与spring-oauth-server实现相同的需求与场合. +只是在实现上使用的技术不同(spring-oauth-server使用Spring Security + spring-security-oauth2实现); 相比spring-oauth-server, oauth2-oltu具有如下特点:
JDK -- 1.7.0_40
Maven -- 3.1.0
MySQL -- 5.6.23-log
JDK -- 1.8.0_40
Maven -- 3.6.0
MySQL -- 5.7.22
authz 实现使用各类grant_type去获取token业务逻辑----获取access_token
core 将公共部分提取到该模块中, 减少重复代码, 保证一致性, 如定义ClientDetails, AccessToken; authz, resources 模块都依赖于该模块
resources 资源管理模块,将受OAUTH保护的资源(URI)放在这里----使用access_token
resources 资源管理模块,将受OAuth2保护的资源(URI)放在这里----使用access_token
resources http://andaily.com/rs/
authz https://andaily.com/authz/ v0.2
resources https://andaily.com/rs/ v0.2
Redis + MySQL https://github.com/monkeyk/oauth2-shiro-redis
Redis https://github.com/monkeyk/oauth2-shiro-redis/tree/redis
Redis + MySQL https://gitee.com/mkk/oauth2-shiro-redis
application-test.properties,位于 src/test/resources 目录中)。
AuthzApplication.java; resources默认端口为8083,启动类 ResourcesApplication.java )。
grant_type (授权方式)与功能
authorization_code -- 授权码模式(即先登录获取code,再获取token)password -- 密码模式(将用户名,密码传过去,直接获取token)password -- 密码模式(将用户名,密码传过去,直接获取token) [不推荐]refresh_token -- 刷新access_tokenimplicit(token) -- 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash)implicit(token) -- 简化模式(在redirect_uri 的Hash传递token; Auth客户端运行在浏览器中,如JS,Flash) [不推荐]client_credentials -- 客户端模式(无用户,用户向客户端注册,然后客户端以自己的名义向'服务端'获取资源)grant_type的使用进行了安全升级(如不推荐使用 password 方式),
+详细变化请查看 OAuth2.1的状态与主要特征。
从 0.2版本开始将项目的所有计划的开发内容列出来, 方便大家跟进, 也欢迎你加入.
-项目的开发管理使用开源项目 andaily-developer.
+项目的开发管理使用开源项目 andaily-developer.
计划加入Spring-Boot的实现 @@ -133,13 +141,40 @@
- Version: 0.3 [pending]
+ Version: 2.0.1 [plan]
+
+ Date: --- / ---
+
---
+ Version: 2.0.0 [finished]
+
+ Date: 2020-07-05 / 2023-09-28
+
升级使用Spring Boot,调整工程结构,使用 thymeleaf替换 servlet/jsp,打包由war换成jar
升级使用JDK 1.8, 日志框架使用logback替换log4j(处理log4j的安全漏洞)
token支持使用JWT格式,通过配置参数authz.token.generator.type来控制与向下兼容
升级shiro到v1.11.0
密码存储算法由MD5换成SHA-256,并支持盐(salt),使密码存储更安全可靠;通过配置参数authz.store.credentials.alg来控制与向下兼容
对初始的账户密码使用更安全的密码策略:包括大小写字母,数字与特殊符号,长度至少10位
+ Version: 0.3 [finished]
- Date: 2016-07-16 / ------
+ Date: 2016-07-16 / 2018-10-17
(152) - oltu版本升级到1.0.2 并完成测试.
(153) - 尝试添加并实现OIDC在 oauth2-shiro中
(153) - 尝试添加并实现OIDC在 oauth2-shiro中[canceled]
(161) - 增加必要的代码注释与配置注释, 更易理解
implicit模式不需要带上client_secret
+ +
2015-05-17 Initial project, start push code (private)
2015-07-16 oauth2-shiro项目开发状态(7月)
2015-09-06 oauth2-shiro项目开发状态(8月)
2015-09-06 项目由 私有 变为 开源, 开发 resource 模块
2015-09-26 版本0.1 开发完毕, 发布 0.1-beta 版本
2015-10-07 重构项目结构, 发布 0.1-rc 版本
2015-09-26 版本0.1 开发完毕, 发布 0.1-beta 版本
2015-10-07 重构项目结构, 发布 0.1-rc 版本
2016-05-26 开始开发 0.2 版本
2016-07-02 添加在线测试环境
2016-08-17 发布 0.2 版本
2017-01-21 加入到GitHub中(https://github.com/monkeyk/oauth2-shiro.git)
2020-04-26 推荐七个非常实用的OAuth开源项目
2016-08-17 发布 0.2 版本
2017-01-21 加入到GitHub中, Git@OSC地址: http://git.oschina.net/mkk/oauth2-shiro
2020-07-05 开始2.0.0版本开发
2023-09-28 发布 2.0.0 版本,大升级
oauth2-shiro 0.1-rc 发布 2015-10-07
oauth2-shiro 0.1-beta 发布 2015-09-26
oauth2-shiro项目开发状态(8月) 2015-09-06
oauth2-shiro项目开发状态(7月) 2015-07-16
spring-oauth-server Spring Security与OAUTH2的完整整合项目
spring-oauth-client OAuth 客户端(client)测试项目
spring-oauth-server Spring Security与OAuth2的完整整合项目
spring-oauth-client OAuth 客户端(client)测试项目
- 问答与讨论
-
+
+
与项目相关的,与OAuth相关的问题与回答,以及各类讨论请访问
https://andaily.com/blog/?dwqa-question_category=oauth
-
- 捐助
-
- 支付宝: monkeyking1987@126.com (**钊)
-
- 明瑞 -- 5元
-
- Triton -- 8.8元
-
- 半个鼠标 -- 10元
-
- 张宏俊 -- 20元 (2018-03-28)
-
关注更多我的开源项目请访问 https://andaily.com/my_projects.html
-- 若需更多的支持请联系 sz@monkeyk.com -
-
-
-
+ * Replace authz-context.xml + * + * @author Shengzhao Li + * @since 2.0.0 + */ +@Configuration +public class AuthzContextConfig { + + + /** + * 事务配置 + */ + @Bean + public TransactionManager transactionManager(DataSource dataSource) { + DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); + transactionManager.setDataSource(dataSource); + return transactionManager; + } + + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + JdbcTemplate jdbcTemplate = new JdbcTemplate(); + jdbcTemplate.setDataSource(dataSource); + return jdbcTemplate; + } + + + /** + * AuthenticationId 的生成器 + */ + @Bean + public AuthenticationIdGenerator authenticationIdGenerator() { + return new DefaultAuthenticationIdGenerator(); + } + + /** + * 默认使用MD5 OAuthIssuer, 生成随机值,如 access_token, refresh_token + * 可根据需要扩展使用其他的实现 + */ + @Bean + public OAuthIssuer oAuthIssuer() { + MD5Generator md5Generator = new MD5Generator(); + return new OAuthIssuerImpl(md5Generator); + } + + +} diff --git a/authz/src/main/java/com/monkeyk/os/config/AuthzSecurityConfig.java b/authz/src/main/java/com/monkeyk/os/config/AuthzSecurityConfig.java new file mode 100644 index 0000000000000000000000000000000000000000..50007935c8178384af6fc764c8912b1076e56cc6 --- /dev/null +++ b/authz/src/main/java/com/monkeyk/os/config/AuthzSecurityConfig.java @@ -0,0 +1,152 @@ +package com.monkeyk.os.config; + +import com.monkeyk.os.infrastructure.shiro.MkkJdbcRealm; +import org.apache.shiro.authc.credential.CredentialsMatcher; +import org.apache.shiro.authc.credential.HashedCredentialsMatcher; +import org.apache.shiro.cache.AbstractCacheManager; +import org.apache.shiro.cache.MemoryConstrainedCacheManager; +import org.apache.shiro.crypto.hash.Sha256Hash; +import org.apache.shiro.mgt.SecurityManager; +import org.apache.shiro.realm.AuthorizingRealm; +import org.apache.shiro.realm.Realm; +import org.apache.shiro.spring.web.ShiroFilterFactoryBean; +import org.apache.shiro.web.mgt.DefaultWebSecurityManager; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import javax.sql.DataSource; +import java.util.LinkedHashMap; +import java.util.Map; + +/** + * 2020/7/14 + *
+ * Replace authz-security.xml + * + * @author Shengzhao Li + * @since 2.0.0 + */ +@Configuration +public class AuthzSecurityConfig { + + /* + * 这是一个标准的 SHIRO 安全配置 + 注意OAuth的URL配置: /oauth/** = anon + * + * */ + + /** + * 存储凭证(如密码)的加密算法(如 MD5,SHA-256,SHA-384,SHA-512) + * 默认 SHA-256 + *
+ * 若是旧版本升级后继续使用MD5,请将配置值更新为MD5
+ *
+ * Replace web.xml
+ * mkk-servlet.xml
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+@Configuration
+public class AuthzWebConfig {
+
+
+ /**
+ * 字符编码配置 UTF-8
+ */
+ @Bean
+ public FilterRegistrationBean encodingFilter() {
+ FilterRegistrationBean
+ * 对各类密码的 加密,校验封装
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+@Component
+public class AuthzPasswordEncoder implements InitializingBean {
+
+ private static final Logger LOG = LoggerFactory.getLogger(AuthzPasswordEncoder.class);
+
+ /**
+ * 存储凭证(如密码)的加密算法(如 MD5,SHA-256,SHA-384,SHA-512)
+ * 默认 SHA-256
+ *
+ * 若是旧版本升级后继续使用MD5,请将配置值更新为MD5
+ *
+ * 密码加密 与 验证
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+public interface PasswordEncoder {
+
+
+ /**
+ * 加密密码
+ *
+ * @param rawPassword 原始密码
+ * @param salt 盐 or null
+ * @return 加密后的密码
+ */
+ String encode(String rawPassword, ByteSource salt);
+
+
+ /**
+ * 校验加密的密码是否正确
+ *
+ * @param rawPassword 原始密码
+ * @param encodedPassword 加密的密码
+ * @param salt 盐 or null
+ * @return true 校验正确,其他 false
+ */
+ boolean matches(String rawPassword, String encodedPassword, ByteSource salt);
+
+}
diff --git a/authz/src/main/java/com/monkeyk/os/domain/users/password/ShaPasswordEncoder.java b/authz/src/main/java/com/monkeyk/os/domain/users/password/ShaPasswordEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..3c8c84c422e26486b096b746998d2201d1ac1f27
--- /dev/null
+++ b/authz/src/main/java/com/monkeyk/os/domain/users/password/ShaPasswordEncoder.java
@@ -0,0 +1,66 @@
+package com.monkeyk.os.domain.users.password;
+
+import org.apache.shiro.crypto.hash.SimpleHash;
+import org.apache.shiro.util.ByteSource;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * 2023/9/25 16:47
+ *
+ * SHA-xx 算法实现抽象,
+ * 如SHA-256
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+public class ShaPasswordEncoder implements PasswordEncoder {
+
+ private static final String SHA_PREFIX = "SHA-";
+
+ /**
+ * 如 256, 384, 512
+ */
+ private int length;
+
+ /**
+ * Alg
+ */
+ private final String alg;
+
+ /**
+ * SHA算法
+ *
+ * @param length 如 256, 384, 512
+ */
+ public ShaPasswordEncoder(int length) {
+ this.alg = SHA_PREFIX + length;
+
+ }
+
+
+ /**
+ * SHA算法
+ *
+ * @param alg 如 SHA-256, SHA-384, SHA-512
+ */
+ public ShaPasswordEncoder(String alg) {
+ this.alg = alg;
+ }
+
+ @Override
+ public String encode(String rawPassword, ByteSource salt) {
+ char[] pwdChar = rawPassword.toCharArray();
+ SimpleHash simpleHash = new SimpleHash(this.alg, pwdChar, salt);
+ return simpleHash.toHex();
+ }
+
+ @Override
+ public boolean matches(String rawPassword, String encodedPassword, ByteSource salt) {
+ String encode = this.encode(rawPassword, salt);
+ return MessageDigest.isEqual(
+ encode.getBytes(StandardCharsets.UTF_8),
+ encodedPassword.getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/OauthJdbcRepository.java b/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/OauthJdbcRepository.java
index 72cb939cc801531211cf2eee416a7bb2d7374d0a..f4a1a07aea7fd044671199c8f75c3ebf1c2094ab 100644
--- a/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/OauthJdbcRepository.java
+++ b/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/OauthJdbcRepository.java
@@ -23,10 +23,10 @@ import java.util.List;
public class OauthJdbcRepository extends AbstractJdbcRepository implements OauthRepository {
- private static ClientDetailsRowMapper clientDetailsRowMapper = new ClientDetailsRowMapper();
- private static OauthCodeRowMapper oauthCodeRowMapper = new OauthCodeRowMapper();
+ private final ClientDetailsRowMapper clientDetailsRowMapper = new ClientDetailsRowMapper();
+ private final OauthCodeRowMapper oauthCodeRowMapper = new OauthCodeRowMapper();
- private static AccessTokenRowMapper accessTokenRowMapper = new AccessTokenRowMapper();
+ private final AccessTokenRowMapper accessTokenRowMapper = new AccessTokenRowMapper();
@Override
@@ -41,42 +41,36 @@ public class OauthJdbcRepository extends AbstractJdbcRepository implements Oauth
final String sql = " insert into oauth_client_details(client_id,client_secret,client_name, client_uri,client_icon_uri,resource_ids, scope,grant_types, " +
"redirect_uri,roles,access_token_validity,refresh_token_validity,description,archived,trusted) values (?,?,?, ?,?,?,?,?, ?,?, ?,? ,?,?,?)";
- return jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, clientDetails.getClientId());
- ps.setString(2, clientDetails.getClientSecret());
- ps.setString(3, clientDetails.getName());
-
- ps.setString(4, clientDetails.getClientUri());
- ps.setString(5, clientDetails.getIconUri());
- ps.setString(6, clientDetails.resourceIds());
-
- ps.setString(7, clientDetails.scope());
- ps.setString(8, clientDetails.grantTypes());
- ps.setString(9, clientDetails.getRedirectUri());
-
- ps.setString(10, clientDetails.roles());
- ps.setInt(11, clientDetails.accessTokenValidity() == null ? -1 : clientDetails.accessTokenValidity());
- ps.setInt(12, clientDetails.refreshTokenValidity() == null ? -1 : clientDetails.refreshTokenValidity());
-
- ps.setString(13, clientDetails.getDescription());
- ps.setBoolean(14, clientDetails.archived());
- ps.setBoolean(15, clientDetails.trusted());
- }
+ return jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, clientDetails.getClientId());
+ ps.setString(2, clientDetails.getClientSecret());
+ ps.setString(3, clientDetails.getName());
+
+ ps.setString(4, clientDetails.getClientUri());
+ ps.setString(5, clientDetails.getIconUri());
+ ps.setString(6, clientDetails.resourceIds());
+
+ ps.setString(7, clientDetails.scope());
+ ps.setString(8, clientDetails.grantTypes());
+ ps.setString(9, clientDetails.getRedirectUri());
+
+ ps.setString(10, clientDetails.roles());
+ ps.setInt(11, clientDetails.accessTokenValidity() == null ? -1 : clientDetails.accessTokenValidity());
+ ps.setInt(12, clientDetails.refreshTokenValidity() == null ? -1 : clientDetails.refreshTokenValidity());
+
+ ps.setString(13, clientDetails.getDescription());
+ ps.setBoolean(14, clientDetails.archived());
+ ps.setBoolean(15, clientDetails.trusted());
});
}
@Override
public int saveOauthCode(final OauthCode oauthCode) {
final String sql = " insert into oauth_code(code,username,client_id) values (?,?,?)";
- return jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, oauthCode.code());
- ps.setString(2, oauthCode.username());
- ps.setString(3, oauthCode.clientId());
- }
+ return jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, oauthCode.code());
+ ps.setString(2, oauthCode.username());
+ ps.setString(3, oauthCode.clientId());
});
}
@@ -97,13 +91,10 @@ public class OauthJdbcRepository extends AbstractJdbcRepository implements Oauth
@Override
public int deleteOauthCode(final OauthCode oauthCode) {
final String sql = " delete from oauth_code where code = ? and client_id = ? and username = ?";
- return jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, oauthCode.code());
- ps.setString(2, oauthCode.clientId());
- ps.setString(3, oauthCode.username());
- }
+ return jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, oauthCode.code());
+ ps.setString(2, oauthCode.clientId());
+ ps.setString(3, oauthCode.username());
});
}
@@ -117,13 +108,10 @@ public class OauthJdbcRepository extends AbstractJdbcRepository implements Oauth
@Override
public int deleteAccessToken(final AccessToken accessToken) {
final String sql = " delete from oauth_access_token where client_id = ? and username = ? and authentication_id = ?";
- return jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, accessToken.clientId());
- ps.setString(2, accessToken.username());
- ps.setString(3, accessToken.authenticationId());
- }
+ return jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, accessToken.clientId());
+ ps.setString(2, accessToken.username());
+ ps.setString(3, accessToken.authenticationId());
});
}
@@ -132,20 +120,17 @@ public class OauthJdbcRepository extends AbstractJdbcRepository implements Oauth
final String sql = "insert into oauth_access_token(token_id,token_expired_seconds,authentication_id," +
"username,client_id,token_type,refresh_token_expired_seconds,refresh_token) values (?,?,?,?,?,?,?,?) ";
- return jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, accessToken.tokenId());
- ps.setInt(2, accessToken.tokenExpiredSeconds());
- ps.setString(3, accessToken.authenticationId());
+ return jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, accessToken.tokenId());
+ ps.setInt(2, accessToken.tokenExpiredSeconds());
+ ps.setString(3, accessToken.authenticationId());
- ps.setString(4, accessToken.username());
- ps.setString(5, accessToken.clientId());
- ps.setString(6, accessToken.tokenType());
+ ps.setString(4, accessToken.username());
+ ps.setString(5, accessToken.clientId());
+ ps.setString(6, accessToken.tokenType());
- ps.setInt(7, accessToken.refreshTokenExpiredSeconds());
- ps.setString(8, accessToken.refreshToken());
- }
+ ps.setInt(7, accessToken.refreshTokenExpiredSeconds());
+ ps.setString(8, accessToken.refreshToken());
});
}
diff --git a/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/UsersJdbcAuthzRepository.java b/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/UsersJdbcAuthzRepository.java
index f9ebb053663b6677527e962f23163c6a78c3fba4..f1f1dbe02cf3c12f7df5cfa53d78861d5bd4c557 100644
--- a/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/UsersJdbcAuthzRepository.java
+++ b/authz/src/main/java/com/monkeyk/os/infrastructure/jdbc/UsersJdbcAuthzRepository.java
@@ -4,6 +4,8 @@ import com.monkeyk.os.domain.users.Roles;
import com.monkeyk.os.domain.users.Users;
import com.monkeyk.os.domain.users.UsersAuthzRepository;
import org.apache.commons.lang.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import org.springframework.jdbc.core.PreparedStatementSetter;
import org.springframework.stereotype.Repository;
@@ -22,9 +24,10 @@ import java.util.List;
@Repository("usersJdbcAuthzRepository")
public class UsersJdbcAuthzRepository extends AbstractJdbcRepository implements UsersAuthzRepository {
+ private static final Logger LOG = LoggerFactory.getLogger(UsersJdbcAuthzRepository.class);
- private static UsersRowMapper usersRowMapper = new UsersRowMapper();
- private static RolesRowMapper rolesRowMapper = new RolesRowMapper();
+ private final UsersRowMapper usersRowMapper = new UsersRowMapper();
+ private final RolesRowMapper rolesRowMapper = new RolesRowMapper();
@Override
@@ -54,19 +57,20 @@ public class UsersJdbcAuthzRepository extends AbstractJdbcRepository implements
@Override
public int saveUsers(final Users users) {
- String sql = " insert into users(guid,create_time, username,password) values (?,?,?,?) ";
- this.jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setString(1, users.guid());
- ps.setTimestamp(2, new Timestamp(users.createTime().getTime()));
- ps.setString(3, users.username());
-
- ps.setString(4, users.password());
- }
+ String sql = " insert into users(guid,create_time, username,password, password_salt) values (?,?,?,?,?) ";
+ int row = this.jdbcTemplate.update(sql, ps -> {
+ ps.setString(1, users.guid());
+ ps.setTimestamp(2, new Timestamp(users.createTime().getTime()));
+ ps.setString(3, users.username());
+
+ ps.setString(4, users.password());
+ ps.setString(5, users.passwordSalt());
});
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Insert into users -> row: {}", row);
+ }
- return this.jdbcTemplate.queryForObject("select id from users where guid = ?", new Object[]{users.guid()}, Integer.class);
+ return this.jdbcTemplate.queryForObject("select id from users where guid = ?", Integer.class, new Object[]{users.guid()});
}
@Override
@@ -78,13 +82,13 @@ public class UsersJdbcAuthzRepository extends AbstractJdbcRepository implements
@Override
public void insertUserRoles(final int userId, final int rolesId) {
String sql = "insert into user_roles(users_id,roles_id) values (?,?) ";
- this.jdbcTemplate.update(sql, new PreparedStatementSetter() {
- @Override
- public void setValues(PreparedStatement ps) throws SQLException {
- ps.setInt(1, userId);
- ps.setInt(2, rolesId);
- }
+ int row = this.jdbcTemplate.update(sql, ps -> {
+ ps.setInt(1, userId);
+ ps.setInt(2, rolesId);
});
+ if (LOG.isDebugEnabled()) {
+ LOG.debug("Insert into user_roles -> row: {}", row);
+ }
}
@Override
@@ -99,6 +103,6 @@ public class UsersJdbcAuthzRepository extends AbstractJdbcRepository implements
public List
+ * JWT token 增强实现
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+@Component
+public class JwtTokenEnhancer implements InitializingBean {
+
+ private static final Logger LOG = LoggerFactory.getLogger(JwtTokenEnhancer.class);
+
+
+ /**
+ * Jwt hamc key ; 长度至少32位
+ * TODO: 不同的部署环境请使用不同的hmac key
+ *
+ * @since 2.0.0
+ */
+ @Value("${authz.token.jwt.hmac.key:Bl0depAUL2DRPZnR0DJThK9a9KSJF4Xr}")
+ private String jwtHmacKey;
+
+
+ private HmacKey hmacKey;
+
+
+ public JwtTokenEnhancer() {
+ }
+
+
+ /**
+ * enhance jti
+ *
+ * @param jti token
+ * @param subject payload subject
+ * @param audience payload audience
+ * @param expiredSeconds token expired seconds
+ * @return jwt
+ */
+ public String enhance(String jti, String subject, String audience, int expiredSeconds) {
+ return this.enhance(jti, subject, audience, expiredSeconds, Collections.emptyMap());
+ }
+
+
+ /**
+ * enhance jti
+ *
+ * @param jti token
+ * @param subject payload subject
+ * @param audience payload audience
+ * @param expiredSeconds token expired seconds
+ * @param extPayloadMap ext payload map
+ * @return jwt
+ */
+ public String enhance(String jti, String subject, String audience, int expiredSeconds, Map
+ * 根据配置参数 使用grant_type=password方式来获取access_token
+ 请求URI: 使用grant_type=authorization_code 方式来获取access_token, 需要先获取code
+ 请求URI: 使用grant_type=token 方式来获取access_token, implicit模式; 需要登录
+ 请求URI: 通过 redirect_uri的 URL hash 传递access_token信息 使用grant_type=client_credentials 方式来获取access_token, 不需要username, password, 不支持
+ refresh_token
+ 请求URI: 用于在access_token要过期时换取新的access_token (grant_type需要有refresh_token)
+ 请求URI: 获取当前系统时间, 需要access_token的 resource-id 为 mobile-resource 才能访问
+ 请求URI: 使用access_token获取用户信息, 需要access_token的 resource-id 为 os-resource 且用户Role包含 User
+ 才能访问
+ 请求URI:
+ * authz.store.credentials.alg=MD5
+ *
+ *
+ * @since 2.0.0
+ */
+ @Value("${authz.store.credentials.alg:" + Sha256Hash.ALGORITHM_NAME + "}")
+ private String storeCredentialsAlg;
+
+ /**
+ * 使用指定算法 进行密码的加密与匹配
+ */
+ @Bean
+ public CredentialsMatcher credentialsMatcher() {
+ HashedCredentialsMatcher credentialsMatcher = new HashedCredentialsMatcher();
+ credentialsMatcher.setHashAlgorithmName(this.storeCredentialsAlg);
+// credentialsMatcher.setStoredCredentialsHexEncoded(false);
+ return credentialsMatcher;
+ }
+
+ /**
+ * 扩展的 SHIRO Realm
+ * 使用JDBC实现, 并添加 逻辑删除 (archived = 0) 的处理
+ */
+ @Bean
+ public AuthorizingRealm jdbcRealm(DataSource dataSource) {
+ MkkJdbcRealm realm = new MkkJdbcRealm();
+ realm.setName("jdbcRealm");
+ realm.setDataSource(dataSource);
+ realm.setCredentialsMatcher(credentialsMatcher());
+ realm.setPermissionsLookupEnabled(true);
+ return realm;
+ }
+
+ /**
+ * 使用基于内存的缓存 SHIRO 相关数据
+ */
+ @Bean
+ public AbstractCacheManager shiroCacheManager() {
+ return new MemoryConstrainedCacheManager();
+ }
+
+ /**
+ * SHIRO SecurityManager 配置
+ */
+ @Bean
+ public DefaultWebSecurityManager securityManager(Realm realm) {
+ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
+ securityManager.setRealm(realm);
+ securityManager.setCacheManager(shiroCacheManager());
+ return securityManager;
+ }
+
+
+ /**
+ * SHIRO安全机制拦截器 Filter实现, 注意id必须与 web.xml 中的 shiroFilter 一致
+ */
+ @Bean
+ public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) {
+ ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();
+ factoryBean.setSecurityManager(securityManager);
+ factoryBean.setLoginUrl("/login");
+ factoryBean.setSuccessUrl("/index");
+ factoryBean.setUnauthorizedUrl("/unauthorized");
+ //权限控制, map必须要有顺序
+ Map
+ * authz.store.credentials.alg=MD5
+ *
+ *
+ * @since 2.0.0
+ */
+ @Value("${authz.store.credentials.alg:" + Sha256Hash.ALGORITHM_NAME + "}")
+ private String storeCredentialsAlg;
+
+
+ private PasswordEncoder passwordEncoder;
+
+
+ public AuthzPasswordEncoder() {
+ }
+
+
+ /**
+ * 加密
+ *
+ * @param rasPassword 原始密码
+ * @param salt 盐 or null
+ * @return 加密后的
+ */
+ public String encode(String rasPassword, ByteSource salt) {
+ return this.passwordEncoder.encode(rasPassword, salt);
+ }
+
+ /**
+ * 校验密码是否正确
+ *
+ * @param rawPassword 原始密码
+ * @param encodedPassword 加密的密码
+ * @param salt 盐 or null
+ * @return true 校验正确,其他 false
+ */
+ public boolean matches(String rawPassword, String encodedPassword, ByteSource salt) {
+ return this.passwordEncoder.matches(rawPassword, encodedPassword, salt);
+ }
+
+
+ @Override
+ public void afterPropertiesSet() throws Exception {
+ Assert.notNull(this.storeCredentialsAlg, "storeCredentialsAlg is null");
+
+ switch (this.storeCredentialsAlg) {
+ case Sha256Hash.ALGORITHM_NAME:
+ this.passwordEncoder = new ShaPasswordEncoder(256);
+ break;
+ case Sha384Hash.ALGORITHM_NAME:
+ this.passwordEncoder = new ShaPasswordEncoder(384);
+ break;
+ case Sha512Hash.ALGORITHM_NAME:
+ this.passwordEncoder = new ShaPasswordEncoder(512);
+ break;
+ case Sha1Hash.ALGORITHM_NAME:
+ this.passwordEncoder = new ShaPasswordEncoder(1);
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("[Deprecated] alg: {} is unsafe, not recommended for use (try use SHA-256 or SHA-512 much better)", Sha1Hash.ALGORITHM_NAME);
+ }
+ break;
+ case Md5Hash.ALGORITHM_NAME:
+ this.passwordEncoder = new MD5PasswordEncoder();
+ if (LOG.isWarnEnabled()) {
+ LOG.warn("[Deprecated] alg: {} is unsafe, not recommended for use (try use SHA-256 or SHA-512 much better)", Md5Hash.ALGORITHM_NAME);
+ }
+ break;
+ default:
+ throw new IllegalArgumentException("Unsupport storeCredentialsAlg: "
+ + this.storeCredentialsAlg + ", please checking property 'authz.store.credentials.alg' ");
+ }
+ }
+}
diff --git a/authz/src/main/java/com/monkeyk/os/domain/users/PasswordHandler.java b/authz/src/main/java/com/monkeyk/os/domain/users/PasswordHandler.java
index d316eaaba8973ed25c04ba4c63bb1f82ee76e0dc..95867ba0e99cbc0475e6aee94761318d809ed25f 100644
--- a/authz/src/main/java/com/monkeyk/os/domain/users/PasswordHandler.java
+++ b/authz/src/main/java/com/monkeyk/os/domain/users/PasswordHandler.java
@@ -3,6 +3,7 @@ package com.monkeyk.os.domain.users;
import java.io.UnsupportedEncodingException;
import java.math.BigInteger;
+import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
@@ -13,6 +14,7 @@ import java.security.NoSuchAlgorithmException;
* 使用MD5
*
* @author Shengzhao Li
+ * @deprecated use AuthzPasswordEncoder.java replaced from 2.0.0
*/
public abstract class PasswordHandler {
@@ -30,12 +32,7 @@ public abstract class PasswordHandler {
throw new IllegalStateException("MD5 algorithm not available. Fatal (should be in the JDK).");
}
- try {
- byte[] bytes = digest.digest(password.getBytes("UTF-8"));
- return String.format("%032x", new BigInteger(1, bytes));
- } catch (UnsupportedEncodingException e) {
- throw new IllegalStateException("UTF-8 encoding not available. Fatal (should be in the JDK).");
- }
-
+ byte[] bytes = digest.digest(password.getBytes(StandardCharsets.UTF_8));
+ return String.format("%032x", new BigInteger(1, bytes));
}
}
diff --git a/authz/src/main/java/com/monkeyk/os/domain/users/password/MD5PasswordEncoder.java b/authz/src/main/java/com/monkeyk/os/domain/users/password/MD5PasswordEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..b8ef47718e0edc4ccf79768e83664fe9fedd903f
--- /dev/null
+++ b/authz/src/main/java/com/monkeyk/os/domain/users/password/MD5PasswordEncoder.java
@@ -0,0 +1,28 @@
+package com.monkeyk.os.domain.users.password;
+
+import org.apache.shiro.crypto.hash.Md5Hash;
+import org.apache.shiro.util.ByteSource;
+
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+
+/**
+ * 2023/9/25 16:23
+ *
+ * @author Shengzhao Li
+ * @since 2.0.0
+ */
+public class MD5PasswordEncoder implements PasswordEncoder {
+
+ @Override
+ public String encode(String rawPassword, ByteSource salt) {
+ Md5Hash md5Hash = new Md5Hash(rawPassword, salt);
+ return md5Hash.toHex();
+ }
+
+ @Override
+ public boolean matches(String rawPassword, String encodedPassword, ByteSource salt) {
+ String encode = this.encode(rawPassword, salt);
+ return MessageDigest.isEqual(encode.getBytes(StandardCharsets.UTF_8), encodedPassword.getBytes(StandardCharsets.UTF_8));
+ }
+}
diff --git a/authz/src/main/java/com/monkeyk/os/domain/users/password/PasswordEncoder.java b/authz/src/main/java/com/monkeyk/os/domain/users/password/PasswordEncoder.java
new file mode 100644
index 0000000000000000000000000000000000000000..99aa064215d9356768796feb7206b0756925864f
--- /dev/null
+++ b/authz/src/main/java/com/monkeyk/os/domain/users/password/PasswordEncoder.java
@@ -0,0 +1,36 @@
+package com.monkeyk.os.domain.users.password;
+
+import org.apache.shiro.util.ByteSource;
+
+/**
+ * 2023/9/25 16:19
+ * authz.token.generator.type 来决定是用 jwt 还是 md5值
+ * 可选值:md5 或 jwt (默认)
+ *
+ * @since 2.0.0
+ */
+ @Value("${authz.token.generator.type:jwt}")
+ private String tokenGeneratorType;
+
+
+ /**
+ * 增加 token 为 jwt 格式
+ *
+ * @since 2.0.0
+ */
+ @Autowired
+ private JwtTokenEnhancer jwtTokenEnhancer;
+
+
@Override
public ClientDetails loadClientDetails(String clientId) {
LOG.debug("Load ClientDetails by clientId: {}", clientId);
@@ -210,12 +235,12 @@ public class OauthServiceImpl implements OauthService {
return oauthRepository.findAccessTokenByRefreshToken(refreshToken, clientId);
}
- /*
- * Get AccessToken
- * Generate a new AccessToken from existed(exclude token,refresh_token)
- * Update access_token,refresh_token, expired.
- * Save and remove old
- * */
+ /**
+ * Get AccessToken
+ * Generate a new AccessToken from existed(exclude token,refresh_token)
+ * Update access_token,refresh_token, expired.
+ * Save and remove old
+ */
@Override
public AccessToken changeAccessTokenByRefreshToken(String refreshToken, String clientId) throws OAuthSystemException {
final AccessToken oldToken = loadAccessTokenByRefreshToken(refreshToken, clientId);
@@ -227,8 +252,12 @@ public class OauthServiceImpl implements OauthService {
newAccessToken.updateByClientDetails(details);
final String authId = authenticationIdGenerator.generate(clientId, oldToken.username(), null);
+ String tokenId = oAuthIssuer.accessToken();
+ if (needEnhanceJwt()) {
+ tokenId = jwtTokenEnhancer.enhance(tokenId, oldToken.username(), clientId, newAccessToken.tokenExpiredSeconds());
+ }
newAccessToken.authenticationId(authId)
- .tokenId(oAuthIssuer.accessToken())
+ .tokenId(tokenId)
.refreshToken(oAuthIssuer.refreshToken());
oauthRepository.deleteAccessToken(oldToken);
@@ -246,14 +275,22 @@ public class OauthServiceImpl implements OauthService {
return clientDetails != null;
}
- private AccessToken createAndSaveAccessToken(ClientDetails clientDetails, boolean includeRefreshToken, String username, String authenticationId) throws OAuthSystemException {
+ private AccessToken createAndSaveAccessToken(ClientDetails clientDetails, boolean includeRefreshToken, String username, String authenticationId)
+ throws OAuthSystemException {
+
AccessToken accessToken = new AccessToken()
.clientId(clientDetails.getClientId())
.username(username)
- .tokenId(oAuthIssuer.accessToken())
+// .tokenId(tokenId)
.authenticationId(authenticationId)
.updateByClientDetails(clientDetails);
+ String tokenId = oAuthIssuer.accessToken();
+ if (needEnhanceJwt()) {
+ tokenId = jwtTokenEnhancer.enhance(tokenId, username, clientDetails.getClientId(), accessToken.tokenExpiredSeconds());
+ }
+ accessToken.tokenId(tokenId);
+
if (includeRefreshToken) {
accessToken.refreshToken(oAuthIssuer.refreshToken());
}
@@ -273,4 +310,15 @@ public class OauthServiceImpl implements OauthService {
}
+ /**
+ * 判断是否需要增强token 用 jwt
+ *
+ * @return true yes
+ * @since 2.0.0
+ */
+ private boolean needEnhanceJwt() {
+ return Constants.JWT.equalsIgnoreCase(this.tokenGeneratorType);
+ }
+
+
}
diff --git a/authz/src/main/java/com/monkeyk/os/service/impl/UserServiceImpl.java b/authz/src/main/java/com/monkeyk/os/service/impl/UserServiceImpl.java
index 99e865a441199e89bf973181f7fcea2d2a7068b1..23138dd9632301fa4e66538e04da84da248edcb5 100644
--- a/authz/src/main/java/com/monkeyk/os/service/impl/UserServiceImpl.java
+++ b/authz/src/main/java/com/monkeyk/os/service/impl/UserServiceImpl.java
@@ -24,6 +24,9 @@ public class UserServiceImpl implements UserService {
@Autowired
private UsersAuthzRepository usersAuthzRepository;
+ @Autowired
+ private UsersFormSaver usersFormSaver;
+
@Override
public UsersOverviewDto loadUsersOverviewDto(String username) {
List
-
- oauth2-shiro
+ 2.0
+
+
+
+
+ 获取access_token (grant_type=password)
+ public
+
+
+
+
+ /oauth/token POST
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+ client_id
+ {client_id}
+ 是
+
+
+
+ client_secret
+ {client_secret}
+ 是
+
+
+
+ grant_type
+ password
+ 是
+ 固定值
+
+
+ scope
+ {scope}
+ 是
+ read or write
+
+
+ username
+ {username}
+ 是
+ 用户名
+
+
+
+ password
+ {password}
+ 是
+ 用户密码
+ http://localhost:8080/oauth/token?client_id=test-client&client_secret=Test@2015$$&grant_type=password&scope=read&username=test&password=Test@2015#
+ curl --location 'http://localhost:8080/oauth/token' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'client_id=mobile-client' \
+--data-urlencode 'client_secret=Mobile@2015$$' \
+--data-urlencode 'grant_type=password' \
+--data-urlencode 'username=test' \
+--data-urlencode 'password=Test@2015#' \
+--data-urlencode 'scope=read'
+
+
+
+ 响应
+
+
+
+
+ {
+ "access_token": "eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiNDllOTQ0N2E2ZGJlMTUyZTBkMmE2YjYzNDRmZmQyNWQiLCJpYXQiOjE2OTU4NjY1NDAsImV4cCI6MTY5NTkwOTc0MCwiYXVkIjoibW9iaWxlLWNsaWVudCJ9.WjB1lvsccXXBJiOeHMuvo-kZmpfpi1YQgU8NuGYmR70",
+ "refresh_token": "46a2017568aee3875a42f7c2234f4b3d",
+ "token_type": "Bearer",
+ "expires_in": 43199
+}
+
+ {"error":"invalid_grant","error_description":"Bad credentials"}
+ 获取access_token (grant_type=authorization_code)
+ public
+
+
+
+
+ /oauth/token POST
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+ client_id
+ {client_id}
+ 是
+
+
+
+ client_secret
+ {client_secret}
+ 是
+
+
+
+ grant_type
+ authorization_code
+ 是
+ 固定值
+
+
+ code
+ {code}
+ 是
+
+
+
+
+ redirect_uri
+ {redirect_uri}
+ 是
+
+ http://localhost:8080/oauth/token?client_id=test-client&client_secret=Test@2015$$&grant_type=authorization_code&code=ac0bd18863b07adfb518cc6e6dfcfcab&redirect_uri=http%3A%2F%2Flocalhost%3A7777%2Fspring-oauth-client%2Fauthorization_code_callback
+ curl --location 'http://localhost:8080/oauth/token' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'client_id=test-client' \
+--data-urlencode 'client_secret=Test@2015$$' \
+--data-urlencode 'grant_type=authorization_code' \
+--data-urlencode 'redirect_uri=http://localhost:7777/spring-oauth-client/authorization_code_callback' \
+--data-urlencode 'code=52aa9d9cb8e62649e887e745fda94fa7'
+
+
+ 响应
+
+
+
+
+ {
+ "access_token": "eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiY2I2ZmIzYTFmNzA0OGU3NDYxZjcwYWI2OGNkYTk1ZjUiLCJpYXQiOjE2OTU4NjYzODIsImV4cCI6MTY5NTkwOTU4MiwiYXVkIjoidGVzdC1jbGllbnQifQ.NqJe-j7p3UC2gJlBJ-tKB4GrFsW9OR-GyxMfm4LIfwQ",
+ "refresh_token": "019b043ddcf5994220617b6795c5216a",
+ "token_type": "Bearer",
+ "expires_in": 43199
+}
+
+ {"error":"invalid_grant","error_description":"Invalid code '26964e42c667b5d42f89a1255766630a'"}
+ 获取access_token (grant_type=token)
+ public
+
+
+
+
+ /oauth/token GET [deprecated]
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+ client_id
+ {client_id}
+ 是
+
+
+
+ grant_type
+ token
+ 是
+ 固定值
+
+
+ scope
+ {scope}
+ 是
+ read or write
+
+
+
+ redirect_uri
+ {redirect_uri}
+ 是
+
+ http://localhost:8080/oauth/authorize?response_type=token&scope=read write&client_id=test-client&redirect_uri=http%3A%2F%2Flocalhost%3A7777%2Fspring-oauth-client%2Fauthorization_code_callback
+
+
+
+ 响应
+
+
+
+
+ http://localhost:7777/spring-oauth-client/authorization_code_callback#access_token=eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiZmI2YWM1M2E5YjRlMjFkMjcyMmU4Y2FjMDBjODkyNGUiLCJpYXQiOjE2OTU4Njc3MjMsImV4cCI6MTY5NTkxMDkyMywiYXVkIjoidGVzdC1jbGllbnQifQ.i7WyVE_08DeKeq_SpI-C2sqTaKDXt-wKck1L_L_aW98&token_type=Bearer&expires_in=43199
+ 获取access_token (grant_type=client_credentials)
+ public
+
+
+
+
+ /oauth/token POST
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+ client_id
+ {client_id}
+ 是
+
+
+
+ client_secret
+ {client_secret}
+ 是
+
+
+
+ grant_type
+ client_credentials
+ 是
+ 固定值
+
+
+
+ scope
+ {scope}
+ 是
+ read or write
+ http://localhost:8080/oauth/token?client_id=test-client&client_secret=Test@2015$$&grant_type=client_credentials&scope=read
+
+ curl --location 'http://localhost:8080/oauth/token' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'client_id=test-client' \
+--data-urlencode 'client_secret=Test@2015$$' \
+--data-urlencode 'grant_type=client_credentials' \
+--data-urlencode 'scope=read'
+
+
+ 响应
+
+
+
+
+ {
+ "access_token": "eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0LWNsaWVudCIsImp0aSI6IjlkNTZhMjFhYzNhZGMzMWQyYzRjZDJlOWEyNTNkY2RmIiwiaWF0IjoxNjk1ODY2NjA5LCJleHAiOjE2OTU5MDk4MDksImF1ZCI6InRlc3QtY2xpZW50In0.brapFTd_HiPfrlKZWOK9MXOFKrDRD7v2dqXnGU7nkjI",
+ "token_type": "Bearer",
+ "expires_in": 43199
+}
+
+ {"error":"invalid_client","error_description":"Invalid client_id'test-xxx'"}
+ 刷新access_token (grant_type=refresh_token)
+ public
+
+
+
+
+ /oauth/token POST
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+ client_id
+ {client_id}
+ 是
+
+
+
+ client_secret
+ {client_secret}
+ 是
+
+
+
+ grant_type
+ refresh_token
+ 是
+ 固定值
+
+
+
+ refresh_token
+ {refresh_token}
+ 是
+
+ http://localhost:8080/oauth/token?client_id=test-client&client_secret=Test@2015$$&grant_type=refresh_token&refresh_token=b36f4978a1724aa8af8960f58abe3ba1
+
+ curl --location 'http://localhost:8080/oauth/token' \
+--header 'Content-Type: application/x-www-form-urlencoded' \
+--data-urlencode 'client_id=test-client' \
+--data-urlencode 'client_secret=Test@2015$$' \
+--data-urlencode 'grant_type=refresh_token' \
+--data-urlencode 'refresh_token=8c46797a0101800626270ce6579c84fa'
+
+
+ 响应
+
+
+
+
+ {
+ "access_token": "eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiZTUyYTExZGU4Y2EwNGUwOTI1Y2RhNDMxNDYwN2NmZGUiLCJpYXQiOjE2OTU4NjY2NzksImV4cCI6MTY5NTkwOTg3OSwiYXVkIjoidGVzdC1jbGllbnQifQ.qLkvnAartpxkiFfeMwnzrK61ihJtXAu6ml5tFU8O-NU",
+ "refresh_token": "bb277d6ba38bbf5d6facae92eb29e286",
+ "token_type": "Bearer",
+ "expires_in": 43199
+}
+
+ {"error":"invalid_grant","error_description":"Invalid refresh_token: 8e91a56f53857688a3ffd8c7cfd311cfss"}
+
+
+ 获取当前系统时间(resource-id: mobile-resource)
+
+
+
+ /mobile/system_time GET
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+
+ 无
+ http://localhost:8083/mobile/system_time?access_token=eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiNDllOT...
+
+ curl --location 'http://localhost:8083/mobile/system_time' \
+--header 'Authorization: Bearer eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiNDllOTQ0N2E2ZGJlMTUyZTBkMmE2YjYzNDRmZmQyNWQiLCJpYXQiOjE2OTU4NjY1NDAsImV4cCI6MTY5NTkwOTc0MCwiYXVkIjoibW9iaWxlLWNsaWVudCJ9.WjB1lvsccXXBJiOeHMuvo-kZmpfpi1YQgU8NuGYmR70'
+
+
+
+
+ 响应
+
+
+
+
+ {
+ "time": 1695628213913
+}
+
+ {"error":"invalid_token","error_description":"Invalid access_token: 95c3afd44c5d87301dc3034b20b3fc75s"}
+ 获取当前用户信息 (resource-id: os-resource; Role: User)
+
+
+
+ /rs/username GET
+
+
+
+ 请求示例:
+
+
+
+
+ 参数名
+ 参数值
+ 必须?
+ 备注
+
+
+
+ 无
+ http://localhost:8083/rs/username?access_token=eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIi...
+
+ curl --location 'http://localhost:8083/rs/username' \
+--header 'Authorization: Bearer eyJraWQiOiJvYXV0aDItc2hpcm8ta2V5aWQiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwianRpIjoiZTUyYTExZGU4Y2EwNGUwOTI1Y2RhNDMxNDYwN2NmZGUiLCJpYXQiOjE2OTU4NjY2NzksImV4cCI6MTY5NTkwOTg3OSwiYXVkIjoidGVzdC1jbGllbnQifQ.qLkvnAartpxkiFfeMwnzrK61ihJtXAu6ml5tFU8O-NU'
+
+
+
+
+ 响应
+
+
+
+
+ {
+ "clientId": "test-client",
+ "username": "test"
+}
+
+ {"error":"invalid_token","error_description":"Invalid client by token: 95c3afd44c5d87301dc3034b20b3fc75"}
+
+