Commit 0900b9e111ec110df765d5c3f7789a0abb3b8785
1 parent
99ba7cce
完成 oauth2 password 密码模式的实现
完成 oauth2 revokeToken 的实现
Showing
11 changed files
with
228 additions
and
44 deletions
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2Controller.http
| ... | ... | @@ -21,3 +21,24 @@ Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== |
| 21 | 21 | tenant-id: {{adminTenentId}} |
| 22 | 22 | |
| 23 | 23 | grant_type=authorization_code&redirect_uri=https://www.iocoder.cn |
| 24 | + | |
| 25 | +### 请求 /system/oauth2/token + password 接口 => 成功 | |
| 26 | +POST {{baseUrl}}/system/oauth2/token | |
| 27 | +Content-Type: application/x-www-form-urlencoded | |
| 28 | +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== | |
| 29 | +tenant-id: {{adminTenentId}} | |
| 30 | + | |
| 31 | +grant_type=password&username=admin&password=admin123&scope=user_info | |
| 32 | + | |
| 33 | +### 请求 /system/oauth2/token + refresh_token 接口 => 成功 | |
| 34 | +POST {{baseUrl}}/system/oauth2/token | |
| 35 | +Content-Type: application/x-www-form-urlencoded | |
| 36 | +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== | |
| 37 | +tenant-id: {{adminTenentId}} | |
| 38 | + | |
| 39 | +grant_type=refresh_token&refresh_token=00895465d6994f72a9d926ceeed0f588 | |
| 40 | + | |
| 41 | +### 请求 /system/oauth2/token + DELETE 接口 => 成功 | |
| 42 | +DELETE {{baseUrl}}/system/oauth2/token?token=ca8a188f464441d6949c51493a2b7596 | |
| 43 | +Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== | |
| 44 | +tenant-id: {{adminTenentId}} | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/OAuth2OpenController.java
| ... | ... | @@ -47,8 +47,6 @@ public class OAuth2OpenController { |
| 47 | 47 | |
| 48 | 48 | // POST oauth/check_token CheckTokenEndpoint |
| 49 | 49 | |
| 50 | -// DELETE oauth/token ConsumerTokenServices#revokeToken | |
| 51 | - | |
| 52 | 50 | @Resource |
| 53 | 51 | private OAuth2GrantService oauth2GrantService; |
| 54 | 52 | @Resource |
| ... | ... | @@ -56,20 +54,38 @@ public class OAuth2OpenController { |
| 56 | 54 | @Resource |
| 57 | 55 | private OAuth2ApproveService oauth2ApproveService; |
| 58 | 56 | |
| 57 | + /** | |
| 58 | + * 对应 Spring Security OAuth 的 TokenEndpoint 类的 postAccessToken 方法 | |
| 59 | + * | |
| 60 | + * 授权码 authorization_code 模式时:code + redirectUri + state 参数 | |
| 61 | + * 密码 password 模式时:username + password + scope 参数 | |
| 62 | + * 刷新 refresh_token 模式时:refreshToken 参数 | |
| 63 | + * 客户端 client_credentials 模式:scope 参数 | |
| 64 | + * 简化 implicit 模式时:不支持 | |
| 65 | + * | |
| 66 | + * 注意,默认需要传递 client_id + client_secret 参数 | |
| 67 | + */ | |
| 59 | 68 | @PostMapping("/token") |
| 60 | 69 | @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") |
| 61 | 70 | @ApiImplicitParams({ |
| 62 | 71 | @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), |
| 63 | 72 | @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), |
| 64 | 73 | @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), |
| 65 | - @ApiImplicitParam(name = "state", example = "123321", dataTypeClass = String.class) | |
| 74 | + @ApiImplicitParam(name = "username", example = "tudou", dataTypeClass = String.class), | |
| 75 | + @ApiImplicitParam(name = "password", example = "cai", dataTypeClass = String.class), // 多个使用空格分隔 | |
| 76 | + @ApiImplicitParam(name = "scope", example = "user_info", dataTypeClass = String.class) | |
| 66 | 77 | }) |
| 67 | 78 | @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 |
| 68 | 79 | public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request, |
| 69 | 80 | @RequestParam("grant_type") String grantType, |
| 70 | 81 | @RequestParam(value = "code", required = false) String code, // 授权码模式 |
| 71 | 82 | @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 |
| 72 | - @RequestParam(value = "state", required = false) String state) { // 授权码模式 | |
| 83 | + @RequestParam(value = "state", required = false) String state, // 授权码模式 | |
| 84 | + @RequestParam(value = "username", required = false) String username, // 密码模式 | |
| 85 | + @RequestParam(value = "password", required = false) String password, // 密码模式 | |
| 86 | + @RequestParam(value = "scope", required = false) String scope, // 密码模式 | |
| 87 | + @RequestParam(value = "refresh_token", required = false) String refreshToken) { // 刷新模式 | |
| 88 | + List<String> scopes = OAuth2Utils.buildScopes(scope); | |
| 73 | 89 | // 授权类型 |
| 74 | 90 | OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); |
| 75 | 91 | if (grantTypeEnum == null) { |
| ... | ... | @@ -80,23 +96,24 @@ public class OAuth2OpenController { |
| 80 | 96 | } |
| 81 | 97 | |
| 82 | 98 | // 校验客户端 |
| 83 | - String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); | |
| 84 | - if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { | |
| 85 | - throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); | |
| 86 | - } | |
| 87 | - OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], grantType, null, null); | |
| 99 | + String[] clientIdAndSecret = obtainBasicAuthorization(request); | |
| 100 | + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], | |
| 101 | + grantType, scopes, redirectUri); | |
| 88 | 102 | |
| 89 | 103 | // 根据授权模式,获取访问令牌 |
| 90 | - OAuth2AccessTokenDO accessTokenDO = null; | |
| 104 | + OAuth2AccessTokenDO accessTokenDO; | |
| 91 | 105 | switch (grantTypeEnum) { |
| 92 | 106 | case AUTHORIZATION_CODE: |
| 93 | 107 | accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); |
| 94 | 108 | break; |
| 95 | 109 | case PASSWORD: |
| 110 | + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); | |
| 96 | 111 | break; |
| 97 | 112 | case CLIENT_CREDENTIALS: |
| 113 | + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); | |
| 98 | 114 | break; |
| 99 | 115 | case REFRESH_TOKEN: |
| 116 | + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); | |
| 100 | 117 | break; |
| 101 | 118 | default: |
| 102 | 119 | throw new IllegalArgumentException("未知授权类型:" + grantType); |
| ... | ... | @@ -105,6 +122,24 @@ public class OAuth2OpenController { |
| 105 | 122 | return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); |
| 106 | 123 | } |
| 107 | 124 | |
| 125 | + @DeleteMapping("/token") | |
| 126 | + @ApiOperation(value = "删除访问令牌") | |
| 127 | + @ApiImplicitParam(name = "token", required = true, value = "访问令牌", example = "biu", dataTypeClass = String.class) | |
| 128 | + @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | |
| 129 | + public CommonResult<Boolean> revokeToken(HttpServletRequest request, | |
| 130 | + @RequestParam("token") String token) { | |
| 131 | + // 校验客户端 | |
| 132 | + String[] clientIdAndSecret = obtainBasicAuthorization(request); | |
| 133 | + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { | |
| 134 | + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); | |
| 135 | + } | |
| 136 | + OAuth2ClientDO client = oauth2ClientService.validOAuthClientFromCache(clientIdAndSecret[0], clientIdAndSecret[1], | |
| 137 | + null, null, null); | |
| 138 | + | |
| 139 | + // 删除访问令牌 | |
| 140 | + return success(oauth2GrantService.revokeToken(client.getClientId(), token)); | |
| 141 | + } | |
| 142 | + | |
| 108 | 143 | // GET oauth/authorize AuthorizationEndpoint TODO |
| 109 | 144 | @GetMapping("/authorize") |
| 110 | 145 | @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") |
| ... | ... | @@ -229,4 +264,12 @@ public class OAuth2OpenController { |
| 229 | 264 | return UserTypeEnum.ADMIN.getValue(); |
| 230 | 265 | } |
| 231 | 266 | |
| 267 | + private String[] obtainBasicAuthorization(HttpServletRequest request) { | |
| 268 | + String[] clientIdAndSecret = HttpUtils.obtainBasicAuthorization(request); | |
| 269 | + if (ArrayUtil.isEmpty(clientIdAndSecret) || clientIdAndSecret.length != 2) { | |
| 270 | + throw exception0(BAD_REQUEST.getCode(), "client_id 或 client_secret 未正确传递"); | |
| 271 | + } | |
| 272 | + return clientIdAndSecret; | |
| 273 | + } | |
| 274 | + | |
| 232 | 275 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/controller/admin/oauth2/vo/open/OAuth2OpenAccessTokenRespVO.java
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/convert/oauth2/OAuth2OpenConvert.java
| ... | ... | @@ -16,6 +16,7 @@ public interface OAuth2OpenConvert { |
| 16 | 16 | OAuth2OpenAccessTokenRespVO respVO = convert0(bean); |
| 17 | 17 | respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); |
| 18 | 18 | respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); |
| 19 | + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); | |
| 19 | 20 | return respVO; |
| 20 | 21 | } |
| 21 | 22 | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/framework/security/config/SecurityConfiguration.java
| ... | ... | @@ -37,6 +37,7 @@ public class SecurityConfiguration { |
| 37 | 37 | registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll(); |
| 38 | 38 | // OAuth2 API |
| 39 | 39 | registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll(); |
| 40 | + registry.antMatchers(buildAdminApi("/system/oauth2/check_token")).permitAll(); | |
| 40 | 41 | } |
| 41 | 42 | |
| 42 | 43 | }; | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthService.java
| 1 | 1 | package cn.iocoder.yudao.module.system.service.auth; |
| 2 | 2 | |
| 3 | 3 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; |
| 4 | +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | |
| 4 | 5 | |
| 5 | 6 | import javax.validation.Valid; |
| 6 | 7 | |
| ... | ... | @@ -14,6 +15,15 @@ import javax.validation.Valid; |
| 14 | 15 | public interface AdminAuthService { |
| 15 | 16 | |
| 16 | 17 | /** |
| 18 | + * 验证账号 + 密码。如果通过,则返回用户 | |
| 19 | + * | |
| 20 | + * @param username 账号 | |
| 21 | + * @param password 密码 | |
| 22 | + * @return 用户 | |
| 23 | + */ | |
| 24 | + AdminUserDO authenticate(String username, String password); | |
| 25 | + | |
| 26 | + /** | |
| 17 | 27 | * 账号登录 |
| 18 | 28 | * |
| 19 | 29 | * @param reqVO 登录信息 | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImpl.java
| ... | ... | @@ -63,12 +63,33 @@ public class AdminAuthServiceImpl implements AdminAuthService { |
| 63 | 63 | private SmsCodeApi smsCodeApi; |
| 64 | 64 | |
| 65 | 65 | @Override |
| 66 | + public AdminUserDO authenticate(String username, String password) { | |
| 67 | + final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | |
| 68 | + // 校验账号是否存在 | |
| 69 | + AdminUserDO user = userService.getUserByUsername(username); | |
| 70 | + if (user == null) { | |
| 71 | + createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); | |
| 72 | + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); | |
| 73 | + } | |
| 74 | + if (!userService.isPasswordMatch(password, user.getPassword())) { | |
| 75 | + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); | |
| 76 | + throw exception(AUTH_LOGIN_BAD_CREDENTIALS); | |
| 77 | + } | |
| 78 | + // 校验是否禁用 | |
| 79 | + if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { | |
| 80 | + createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); | |
| 81 | + throw exception(AUTH_LOGIN_USER_DISABLED); | |
| 82 | + } | |
| 83 | + return user; | |
| 84 | + } | |
| 85 | + | |
| 86 | + @Override | |
| 66 | 87 | public AuthLoginRespVO login(AuthLoginReqVO reqVO) { |
| 67 | 88 | // 判断验证码是否正确 |
| 68 | 89 | verifyCaptcha(reqVO); |
| 69 | 90 | |
| 70 | 91 | // 使用账号密码,进行登录 |
| 71 | - AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); | |
| 92 | + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); | |
| 72 | 93 | |
| 73 | 94 | // 创建 Token 令牌,记录登录日志 |
| 74 | 95 | return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); |
| ... | ... | @@ -125,27 +146,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { |
| 125 | 146 | captchaService.deleteCaptchaCode(reqVO.getUuid()); |
| 126 | 147 | } |
| 127 | 148 | |
| 128 | - @VisibleForTesting | |
| 129 | - AdminUserDO login0(String username, String password) { | |
| 130 | - final LoginLogTypeEnum logTypeEnum = LoginLogTypeEnum.LOGIN_USERNAME; | |
| 131 | - // 校验账号是否存在 | |
| 132 | - AdminUserDO user = userService.getUserByUsername(username); | |
| 133 | - if (user == null) { | |
| 134 | - createLoginLog(null, username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); | |
| 135 | - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); | |
| 136 | - } | |
| 137 | - if (!userService.isPasswordMatch(password, user.getPassword())) { | |
| 138 | - createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.BAD_CREDENTIALS); | |
| 139 | - throw exception(AUTH_LOGIN_BAD_CREDENTIALS); | |
| 140 | - } | |
| 141 | - // 校验是否禁用 | |
| 142 | - if (ObjectUtil.notEqual(user.getStatus(), CommonStatusEnum.ENABLE.getStatus())) { | |
| 143 | - createLoginLog(user.getId(), username, logTypeEnum, LoginResultEnum.USER_DISABLED); | |
| 144 | - throw exception(AUTH_LOGIN_USER_DISABLED); | |
| 145 | - } | |
| 146 | - return user; | |
| 147 | - } | |
| 148 | - | |
| 149 | 149 | private void createLoginLog(Long userId, String username, |
| 150 | 150 | LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { |
| 151 | 151 | // 插入登录日志 |
| ... | ... | @@ -187,7 +187,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { |
| 187 | 187 | @Override |
| 188 | 188 | public AuthLoginRespVO socialBindLogin(AuthSocialBindLoginReqVO reqVO) { |
| 189 | 189 | // 使用账号密码,进行登录。 |
| 190 | - AdminUserDO user = login0(reqVO.getUsername(), reqVO.getPassword()); | |
| 190 | + AdminUserDO user = authenticate(reqVO.getUsername(), reqVO.getPassword()); | |
| 191 | 191 | |
| 192 | 192 | // 绑定社交用户 |
| 193 | 193 | socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO)); | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantService.java
| ... | ... | @@ -18,7 +18,17 @@ import java.util.List; |
| 18 | 18 | */ |
| 19 | 19 | public interface OAuth2GrantService { |
| 20 | 20 | |
| 21 | - // ImplicitTokenGranter | |
| 21 | + /** | |
| 22 | + * 简化模式 | |
| 23 | + * | |
| 24 | + * 对应 Spring Security OAuth2 的 ImplicitTokenGranter 功能 | |
| 25 | + * | |
| 26 | + * @param userId 用户编号 | |
| 27 | + * @param userType 用户类型 | |
| 28 | + * @param clientId 客户端编号 | |
| 29 | + * @param scopes 授权范围 | |
| 30 | + * @return 访问令牌 | |
| 31 | + */ | |
| 22 | 32 | OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, |
| 23 | 33 | String clientId, List<String> scopes); |
| 24 | 34 | |
| ... | ... | @@ -53,4 +63,51 @@ public interface OAuth2GrantService { |
| 53 | 63 | OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, |
| 54 | 64 | String redirectUri, String state); |
| 55 | 65 | |
| 66 | + /** | |
| 67 | + * 密码模式 | |
| 68 | + * | |
| 69 | + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 | |
| 70 | + * | |
| 71 | + * @param username 账号 | |
| 72 | + * @param password 密码 | |
| 73 | + * @param clientId 客户端编号 | |
| 74 | + * @param scopes 授权范围 | |
| 75 | + * @return 访问令牌 | |
| 76 | + */ | |
| 77 | + OAuth2AccessTokenDO grantPassword(String username, String password, | |
| 78 | + String clientId, List<String> scopes); | |
| 79 | + | |
| 80 | + /** | |
| 81 | + * 刷新模式 | |
| 82 | + * | |
| 83 | + * 对应 Spring Security OAuth2 的 ResourceOwnerPasswordTokenGranter 功能 | |
| 84 | + * | |
| 85 | + * @param refreshToken 刷新令牌 | |
| 86 | + * @param clientId 客户端编号 | |
| 87 | + * @return 访问令牌 | |
| 88 | + */ | |
| 89 | + OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId); | |
| 90 | + | |
| 91 | + /** | |
| 92 | + * 客户端模式 | |
| 93 | + * | |
| 94 | + * 对应 Spring Security OAuth2 的 ClientCredentialsTokenGranter 功能 | |
| 95 | + * | |
| 96 | + * @param clientId 客户端编号 | |
| 97 | + * @param scopes 授权范围 | |
| 98 | + * @return 访问令牌 | |
| 99 | + */ | |
| 100 | + OAuth2AccessTokenDO grantClientCredentials(String clientId, List<String> scopes); | |
| 101 | + | |
| 102 | + /** | |
| 103 | + * 移除访问令牌 | |
| 104 | + * | |
| 105 | + * 对应 Spring Security OAuth2 的 ConsumerTokenServices 的 revokeToken 方法 | |
| 106 | + * | |
| 107 | + * @param accessToken 访问令牌 | |
| 108 | + * @param clientId 客户端编号 | |
| 109 | + * @return 是否移除到 | |
| 110 | + */ | |
| 111 | + boolean revokeToken(String clientId, String accessToken); | |
| 112 | + | |
| 56 | 113 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/service/oauth2/OAuth2GrantServiceImpl.java
| 1 | 1 | package cn.iocoder.yudao.module.system.service.oauth2; |
| 2 | 2 | |
| 3 | +import cn.hutool.core.lang.Assert; | |
| 4 | +import cn.hutool.core.util.ObjectUtil; | |
| 3 | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | |
| 4 | 7 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; |
| 5 | 8 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2CodeDO; |
| 9 | +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | |
| 6 | 10 | import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; |
| 11 | +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; | |
| 7 | 12 | import org.springframework.stereotype.Service; |
| 8 | 13 | |
| 9 | 14 | import javax.annotation.Resource; |
| ... | ... | @@ -23,6 +28,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { |
| 23 | 28 | |
| 24 | 29 | @Resource |
| 25 | 30 | private OAuth2TokenService oauth2TokenService; |
| 31 | + @Resource | |
| 32 | + private AdminAuthService adminAuthService; | |
| 26 | 33 | |
| 27 | 34 | @Override |
| 28 | 35 | public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, |
| ... | ... | @@ -65,4 +72,36 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { |
| 65 | 72 | codeDO.getClientId(), codeDO.getScopes()); |
| 66 | 73 | } |
| 67 | 74 | |
| 75 | + @Override | |
| 76 | + public OAuth2AccessTokenDO grantPassword(String username, String password, String clientId, List<String> scopes) { | |
| 77 | + // 使用账号 + 密码进行登录 | |
| 78 | + AdminUserDO user = adminAuthService.authenticate(username, password); | |
| 79 | + Assert.notNull(user, "用户不能为空!"); // 防御性编程 | |
| 80 | + | |
| 81 | + // 创建访问令牌 | |
| 82 | + return oauth2TokenService.createAccessToken(user.getId(), UserTypeEnum.ADMIN.getValue(), clientId, scopes); | |
| 83 | + } | |
| 84 | + | |
| 85 | + @Override | |
| 86 | + public OAuth2AccessTokenDO grantRefreshToken(String refreshToken, String clientId) { | |
| 87 | + return oauth2TokenService.refreshAccessToken(refreshToken, clientId); | |
| 88 | + } | |
| 89 | + | |
| 90 | + @Override | |
| 91 | + public OAuth2AccessTokenDO grantClientCredentials(String clientId, List<String> scopes) { | |
| 92 | + // TODO 芋艿:项目中使用 OAuth2 解决的是三方应用的授权,内部的 SSO 等问题,所以暂时不考虑 client_credentials 这个场景 | |
| 93 | + throw new UnsupportedOperationException("暂时不支持 client_credentials 授权模式"); | |
| 94 | + } | |
| 95 | + | |
| 96 | + @Override | |
| 97 | + public boolean revokeToken(String clientId, String accessToken) { | |
| 98 | + // 先查询,保证 clientId 时匹配的 | |
| 99 | + OAuth2AccessTokenDO accessTokenDO = oauth2TokenService.getAccessToken(accessToken); | |
| 100 | + if (accessTokenDO == null || ObjectUtil.notEqual(clientId, accessTokenDO.getClientId())) { | |
| 101 | + return false; | |
| 102 | + } | |
| 103 | + // 再删除 | |
| 104 | + return oauth2TokenService.removeAccessToken(accessToken) != null; | |
| 105 | + } | |
| 106 | + | |
| 68 | 107 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/main/java/cn/iocoder/yudao/module/system/util/oauth2/OAuth2Utils.java
| 1 | 1 | package cn.iocoder.yudao.module.system.util.oauth2; |
| 2 | 2 | |
| 3 | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.StrUtil; | |
| 4 | 5 | import cn.iocoder.yudao.framework.common.util.http.HttpUtils; |
| 5 | 6 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 6 | 7 | |
| ... | ... | @@ -58,7 +59,7 @@ public class OAuth2Utils { |
| 58 | 59 | vars.put("expires_in", getExpiresIn(expireTime)); |
| 59 | 60 | } |
| 60 | 61 | if (CollUtil.isNotEmpty(scopes)) { |
| 61 | - vars.put("scope", CollUtil.join(scopes, " ")); | |
| 62 | + vars.put("scope", buildScopeStr(scopes)); | |
| 62 | 63 | } |
| 63 | 64 | for (String key : additionalInformation.keySet()) { |
| 64 | 65 | Object value = additionalInformation.get(key); |
| ... | ... | @@ -86,4 +87,12 @@ public class OAuth2Utils { |
| 86 | 87 | return (expireTime.getTime() - System.currentTimeMillis()) / 1000; |
| 87 | 88 | } |
| 88 | 89 | |
| 90 | + public static String buildScopeStr(Collection<String> scopes) { | |
| 91 | + return CollUtil.join(scopes, " "); | |
| 92 | + } | |
| 93 | + | |
| 94 | + public static List<String> buildScopes(String scope) { | |
| 95 | + return StrUtil.split(scope, ' '); | |
| 96 | + } | |
| 97 | + | |
| 89 | 98 | } | ... | ... |
yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AuthServiceImplTest.java renamed to yudao-module-system/yudao-module-system-biz/src/test/java/cn/iocoder/yudao/module/system/service/auth/AdminAuthServiceImplTest.java
| ... | ... | @@ -34,7 +34,7 @@ import static org.mockito.ArgumentMatchers.eq; |
| 34 | 34 | import static org.mockito.Mockito.*; |
| 35 | 35 | |
| 36 | 36 | @Import(AdminAuthServiceImpl.class) |
| 37 | -public class AuthServiceImplTest extends BaseDbUnitTest { | |
| 37 | +public class AdminAuthServiceImplTest extends BaseDbUnitTest { | |
| 38 | 38 | |
| 39 | 39 | @Resource |
| 40 | 40 | private AdminAuthServiceImpl authService; |
| ... | ... | @@ -63,7 +63,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 63 | 63 | } |
| 64 | 64 | |
| 65 | 65 | @Test |
| 66 | - public void testLogin0_success() { | |
| 66 | + public void testAuthenticate_success() { | |
| 67 | 67 | // 准备参数 |
| 68 | 68 | String username = randomString(); |
| 69 | 69 | String password = randomString(); |
| ... | ... | @@ -75,19 +75,19 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 75 | 75 | when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); |
| 76 | 76 | |
| 77 | 77 | // 调用 |
| 78 | - AdminUserDO loginUser = authService.login0(username, password); | |
| 78 | + AdminUserDO loginUser = authService.authenticate(username, password); | |
| 79 | 79 | // 校验 |
| 80 | 80 | assertPojoEquals(user, loginUser); |
| 81 | 81 | } |
| 82 | 82 | |
| 83 | 83 | @Test |
| 84 | - public void testLogin0_userNotFound() { | |
| 84 | + public void testAuthenticate_userNotFound() { | |
| 85 | 85 | // 准备参数 |
| 86 | 86 | String username = randomString(); |
| 87 | 87 | String password = randomString(); |
| 88 | 88 | |
| 89 | 89 | // 调用, 并断言异常 |
| 90 | - AssertUtils.assertServiceException(() -> authService.login0(username, password), | |
| 90 | + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), | |
| 91 | 91 | AUTH_LOGIN_BAD_CREDENTIALS); |
| 92 | 92 | verify(loginLogService).createLoginLog( |
| 93 | 93 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) |
| ... | ... | @@ -97,7 +97,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | 99 | @Test |
| 100 | - public void testLogin0_badCredentials() { | |
| 100 | + public void testAuthenticate_badCredentials() { | |
| 101 | 101 | // 准备参数 |
| 102 | 102 | String username = randomString(); |
| 103 | 103 | String password = randomString(); |
| ... | ... | @@ -107,7 +107,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 107 | 107 | when(userService.getUserByUsername(eq(username))).thenReturn(user); |
| 108 | 108 | |
| 109 | 109 | // 调用, 并断言异常 |
| 110 | - AssertUtils.assertServiceException(() -> authService.login0(username, password), | |
| 110 | + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), | |
| 111 | 111 | AUTH_LOGIN_BAD_CREDENTIALS); |
| 112 | 112 | verify(loginLogService).createLoginLog( |
| 113 | 113 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) |
| ... | ... | @@ -117,7 +117,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 117 | 117 | } |
| 118 | 118 | |
| 119 | 119 | @Test |
| 120 | - public void testLogin0_userDisabled() { | |
| 120 | + public void testAuthenticate_userDisabled() { | |
| 121 | 121 | // 准备参数 |
| 122 | 122 | String username = randomString(); |
| 123 | 123 | String password = randomString(); |
| ... | ... | @@ -129,7 +129,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { |
| 129 | 129 | when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); |
| 130 | 130 | |
| 131 | 131 | // 调用, 并断言异常 |
| 132 | - AssertUtils.assertServiceException(() -> authService.login0(username, password), | |
| 132 | + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), | |
| 133 | 133 | AUTH_LOGIN_USER_DISABLED); |
| 134 | 134 | verify(loginLogService).createLoginLog( |
| 135 | 135 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | ... | ... |