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,3 +21,24 @@ Authorization: Basic ZGVmYXVsdDphZG1pbjEyMw== | ||
| 21 | tenant-id: {{adminTenentId}} | 21 | tenant-id: {{adminTenentId}} |
| 22 | 22 | ||
| 23 | grant_type=authorization_code&redirect_uri=https://www.iocoder.cn | 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,8 +47,6 @@ public class OAuth2OpenController { | ||
| 47 | 47 | ||
| 48 | // POST oauth/check_token CheckTokenEndpoint | 48 | // POST oauth/check_token CheckTokenEndpoint |
| 49 | 49 | ||
| 50 | -// DELETE oauth/token ConsumerTokenServices#revokeToken | ||
| 51 | - | ||
| 52 | @Resource | 50 | @Resource |
| 53 | private OAuth2GrantService oauth2GrantService; | 51 | private OAuth2GrantService oauth2GrantService; |
| 54 | @Resource | 52 | @Resource |
| @@ -56,20 +54,38 @@ public class OAuth2OpenController { | @@ -56,20 +54,38 @@ public class OAuth2OpenController { | ||
| 56 | @Resource | 54 | @Resource |
| 57 | private OAuth2ApproveService oauth2ApproveService; | 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 | @PostMapping("/token") | 68 | @PostMapping("/token") |
| 60 | @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") | 69 | @ApiOperation(value = "获得访问令牌", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") |
| 61 | @ApiImplicitParams({ | 70 | @ApiImplicitParams({ |
| 62 | @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), | 71 | @ApiImplicitParam(name = "grant_type", required = true, value = "授权类型", example = "code", dataTypeClass = String.class), |
| 63 | @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), | 72 | @ApiImplicitParam(name = "code", value = "授权范围", example = "userinfo.read", dataTypeClass = String.class), |
| 64 | @ApiImplicitParam(name = "redirect_uri", value = "重定向 URI", example = "https://www.iocoder.cn", dataTypeClass = String.class), | 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 | @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 | 78 | @OperateLog(enable = false) // 避免 Post 请求被记录操作日志 |
| 68 | public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request, | 79 | public CommonResult<OAuth2OpenAccessTokenRespVO> postAccessToken(HttpServletRequest request, |
| 69 | @RequestParam("grant_type") String grantType, | 80 | @RequestParam("grant_type") String grantType, |
| 70 | @RequestParam(value = "code", required = false) String code, // 授权码模式 | 81 | @RequestParam(value = "code", required = false) String code, // 授权码模式 |
| 71 | @RequestParam(value = "redirect_uri", required = false) String redirectUri, // 授权码模式 | 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 | OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); | 90 | OAuth2GrantTypeEnum grantTypeEnum = OAuth2GrantTypeEnum.getByGranType(grantType); |
| 75 | if (grantTypeEnum == null) { | 91 | if (grantTypeEnum == null) { |
| @@ -80,23 +96,24 @@ public class OAuth2OpenController { | @@ -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 | switch (grantTypeEnum) { | 105 | switch (grantTypeEnum) { |
| 92 | case AUTHORIZATION_CODE: | 106 | case AUTHORIZATION_CODE: |
| 93 | accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); | 107 | accessTokenDO = oauth2GrantService.grantAuthorizationCodeForAccessToken(client.getClientId(), code, redirectUri, state); |
| 94 | break; | 108 | break; |
| 95 | case PASSWORD: | 109 | case PASSWORD: |
| 110 | + accessTokenDO = oauth2GrantService.grantPassword(username, password, client.getClientId(), scopes); | ||
| 96 | break; | 111 | break; |
| 97 | case CLIENT_CREDENTIALS: | 112 | case CLIENT_CREDENTIALS: |
| 113 | + accessTokenDO = oauth2GrantService.grantClientCredentials(client.getClientId(), scopes); | ||
| 98 | break; | 114 | break; |
| 99 | case REFRESH_TOKEN: | 115 | case REFRESH_TOKEN: |
| 116 | + accessTokenDO = oauth2GrantService.grantRefreshToken(refreshToken, client.getClientId()); | ||
| 100 | break; | 117 | break; |
| 101 | default: | 118 | default: |
| 102 | throw new IllegalArgumentException("未知授权类型:" + grantType); | 119 | throw new IllegalArgumentException("未知授权类型:" + grantType); |
| @@ -105,6 +122,24 @@ public class OAuth2OpenController { | @@ -105,6 +122,24 @@ public class OAuth2OpenController { | ||
| 105 | return success(OAuth2OpenConvert.INSTANCE.convert(accessTokenDO)); | 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 | // GET oauth/authorize AuthorizationEndpoint TODO | 143 | // GET oauth/authorize AuthorizationEndpoint TODO |
| 109 | @GetMapping("/authorize") | 144 | @GetMapping("/authorize") |
| 110 | @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") | 145 | @ApiOperation(value = "获得授权信息", notes = "适合 code 授权码模式,或者 implicit 简化模式;在 authorize.vue 单点登录界面被【获取】调用") |
| @@ -229,4 +264,12 @@ public class OAuth2OpenController { | @@ -229,4 +264,12 @@ public class OAuth2OpenController { | ||
| 229 | return UserTypeEnum.ADMIN.getValue(); | 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
| @@ -29,4 +29,7 @@ public class OAuth2OpenAccessTokenRespVO { | @@ -29,4 +29,7 @@ public class OAuth2OpenAccessTokenRespVO { | ||
| 29 | @JsonProperty("expires_in") | 29 | @JsonProperty("expires_in") |
| 30 | private Long expiresIn; | 30 | private Long expiresIn; |
| 31 | 31 | ||
| 32 | + @ApiModelProperty(value = "授权范围", example = "user_info", notes = "如果多个授权范围,使用空格分隔") | ||
| 33 | + private String scope; | ||
| 34 | + | ||
| 32 | } | 35 | } |
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,6 +16,7 @@ public interface OAuth2OpenConvert { | ||
| 16 | OAuth2OpenAccessTokenRespVO respVO = convert0(bean); | 16 | OAuth2OpenAccessTokenRespVO respVO = convert0(bean); |
| 17 | respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); | 17 | respVO.setTokenType(SecurityFrameworkUtils.AUTHORIZATION_BEARER.toLowerCase()); |
| 18 | respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); | 18 | respVO.setExpiresIn(OAuth2Utils.getExpiresIn(bean.getExpiresTime())); |
| 19 | + respVO.setScope(OAuth2Utils.buildScopeStr(bean.getScopes())); | ||
| 19 | return respVO; | 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,6 +37,7 @@ public class SecurityConfiguration { | ||
| 37 | registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll(); | 37 | registry.antMatchers(buildAdminApi("/system/sms/callback/**")).permitAll(); |
| 38 | // OAuth2 API | 38 | // OAuth2 API |
| 39 | registry.antMatchers(buildAdminApi("/system/oauth2/token")).permitAll(); | 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 | package cn.iocoder.yudao.module.system.service.auth; | 1 | package cn.iocoder.yudao.module.system.service.auth; |
| 2 | 2 | ||
| 3 | import cn.iocoder.yudao.module.system.controller.admin.auth.vo.*; | 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 | import javax.validation.Valid; | 6 | import javax.validation.Valid; |
| 6 | 7 | ||
| @@ -14,6 +15,15 @@ import javax.validation.Valid; | @@ -14,6 +15,15 @@ import javax.validation.Valid; | ||
| 14 | public interface AdminAuthService { | 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 | * @param reqVO 登录信息 | 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,12 +63,33 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||
| 63 | private SmsCodeApi smsCodeApi; | 63 | private SmsCodeApi smsCodeApi; |
| 64 | 64 | ||
| 65 | @Override | 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 | public AuthLoginRespVO login(AuthLoginReqVO reqVO) { | 87 | public AuthLoginRespVO login(AuthLoginReqVO reqVO) { |
| 67 | // 判断验证码是否正确 | 88 | // 判断验证码是否正确 |
| 68 | verifyCaptcha(reqVO); | 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 | // 创建 Token 令牌,记录登录日志 | 94 | // 创建 Token 令牌,记录登录日志 |
| 74 | return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); | 95 | return createTokenAfterLoginSuccess(user.getId(), reqVO.getUsername(), LoginLogTypeEnum.LOGIN_USERNAME); |
| @@ -125,27 +146,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | @@ -125,27 +146,6 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||
| 125 | captchaService.deleteCaptchaCode(reqVO.getUuid()); | 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 | private void createLoginLog(Long userId, String username, | 149 | private void createLoginLog(Long userId, String username, |
| 150 | LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { | 150 | LoginLogTypeEnum logTypeEnum, LoginResultEnum loginResult) { |
| 151 | // 插入登录日志 | 151 | // 插入登录日志 |
| @@ -187,7 +187,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | @@ -187,7 +187,7 @@ public class AdminAuthServiceImpl implements AdminAuthService { | ||
| 187 | @Override | 187 | @Override |
| 188 | public AuthLoginRespVO socialBindLogin(AuthSocialBindLoginReqVO reqVO) { | 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 | socialUserService.bindSocialUser(AuthConvert.INSTANCE.convert(user.getId(), getUserType().getValue(), reqVO)); | 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,7 +18,17 @@ import java.util.List; | ||
| 18 | */ | 18 | */ |
| 19 | public interface OAuth2GrantService { | 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 | OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, | 32 | OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, |
| 23 | String clientId, List<String> scopes); | 33 | String clientId, List<String> scopes); |
| 24 | 34 | ||
| @@ -53,4 +63,51 @@ public interface OAuth2GrantService { | @@ -53,4 +63,51 @@ public interface OAuth2GrantService { | ||
| 53 | OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, | 63 | OAuth2AccessTokenDO grantAuthorizationCodeForAccessToken(String clientId, String code, |
| 54 | String redirectUri, String state); | 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 | package cn.iocoder.yudao.module.system.service.oauth2; | 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 | import cn.hutool.core.util.StrUtil; | 5 | import cn.hutool.core.util.StrUtil; |
| 6 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 4 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; | 7 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2AccessTokenDO; |
| 5 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2CodeDO; | 8 | import cn.iocoder.yudao.module.system.dal.dataobject.auth.OAuth2CodeDO; |
| 9 | +import cn.iocoder.yudao.module.system.dal.dataobject.user.AdminUserDO; | ||
| 6 | import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; | 10 | import cn.iocoder.yudao.module.system.enums.ErrorCodeConstants; |
| 11 | +import cn.iocoder.yudao.module.system.service.auth.AdminAuthService; | ||
| 7 | import org.springframework.stereotype.Service; | 12 | import org.springframework.stereotype.Service; |
| 8 | 13 | ||
| 9 | import javax.annotation.Resource; | 14 | import javax.annotation.Resource; |
| @@ -23,6 +28,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { | @@ -23,6 +28,8 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { | ||
| 23 | 28 | ||
| 24 | @Resource | 29 | @Resource |
| 25 | private OAuth2TokenService oauth2TokenService; | 30 | private OAuth2TokenService oauth2TokenService; |
| 31 | + @Resource | ||
| 32 | + private AdminAuthService adminAuthService; | ||
| 26 | 33 | ||
| 27 | @Override | 34 | @Override |
| 28 | public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, | 35 | public OAuth2AccessTokenDO grantImplicit(Long userId, Integer userType, |
| @@ -65,4 +72,36 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { | @@ -65,4 +72,36 @@ public class OAuth2GrantServiceImpl implements OAuth2GrantService { | ||
| 65 | codeDO.getClientId(), codeDO.getScopes()); | 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 | package cn.iocoder.yudao.module.system.util.oauth2; | 1 | package cn.iocoder.yudao.module.system.util.oauth2; |
| 2 | 2 | ||
| 3 | import cn.hutool.core.collection.CollUtil; | 3 | import cn.hutool.core.collection.CollUtil; |
| 4 | +import cn.hutool.core.util.StrUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.util.http.HttpUtils; | 5 | import cn.iocoder.yudao.framework.common.util.http.HttpUtils; |
| 5 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; | 6 | import cn.iocoder.yudao.framework.security.core.util.SecurityFrameworkUtils; |
| 6 | 7 | ||
| @@ -58,7 +59,7 @@ public class OAuth2Utils { | @@ -58,7 +59,7 @@ public class OAuth2Utils { | ||
| 58 | vars.put("expires_in", getExpiresIn(expireTime)); | 59 | vars.put("expires_in", getExpiresIn(expireTime)); |
| 59 | } | 60 | } |
| 60 | if (CollUtil.isNotEmpty(scopes)) { | 61 | if (CollUtil.isNotEmpty(scopes)) { |
| 61 | - vars.put("scope", CollUtil.join(scopes, " ")); | 62 | + vars.put("scope", buildScopeStr(scopes)); |
| 62 | } | 63 | } |
| 63 | for (String key : additionalInformation.keySet()) { | 64 | for (String key : additionalInformation.keySet()) { |
| 64 | Object value = additionalInformation.get(key); | 65 | Object value = additionalInformation.get(key); |
| @@ -86,4 +87,12 @@ public class OAuth2Utils { | @@ -86,4 +87,12 @@ public class OAuth2Utils { | ||
| 86 | return (expireTime.getTime() - System.currentTimeMillis()) / 1000; | 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,7 +34,7 @@ import static org.mockito.ArgumentMatchers.eq; | ||
| 34 | import static org.mockito.Mockito.*; | 34 | import static org.mockito.Mockito.*; |
| 35 | 35 | ||
| 36 | @Import(AdminAuthServiceImpl.class) | 36 | @Import(AdminAuthServiceImpl.class) |
| 37 | -public class AuthServiceImplTest extends BaseDbUnitTest { | 37 | +public class AdminAuthServiceImplTest extends BaseDbUnitTest { |
| 38 | 38 | ||
| 39 | @Resource | 39 | @Resource |
| 40 | private AdminAuthServiceImpl authService; | 40 | private AdminAuthServiceImpl authService; |
| @@ -63,7 +63,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -63,7 +63,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 63 | } | 63 | } |
| 64 | 64 | ||
| 65 | @Test | 65 | @Test |
| 66 | - public void testLogin0_success() { | 66 | + public void testAuthenticate_success() { |
| 67 | // 准备参数 | 67 | // 准备参数 |
| 68 | String username = randomString(); | 68 | String username = randomString(); |
| 69 | String password = randomString(); | 69 | String password = randomString(); |
| @@ -75,19 +75,19 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -75,19 +75,19 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 75 | when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); | 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 | assertPojoEquals(user, loginUser); | 80 | assertPojoEquals(user, loginUser); |
| 81 | } | 81 | } |
| 82 | 82 | ||
| 83 | @Test | 83 | @Test |
| 84 | - public void testLogin0_userNotFound() { | 84 | + public void testAuthenticate_userNotFound() { |
| 85 | // 准备参数 | 85 | // 准备参数 |
| 86 | String username = randomString(); | 86 | String username = randomString(); |
| 87 | String password = randomString(); | 87 | String password = randomString(); |
| 88 | 88 | ||
| 89 | // 调用, 并断言异常 | 89 | // 调用, 并断言异常 |
| 90 | - AssertUtils.assertServiceException(() -> authService.login0(username, password), | 90 | + AssertUtils.assertServiceException(() -> authService.authenticate(username, password), |
| 91 | AUTH_LOGIN_BAD_CREDENTIALS); | 91 | AUTH_LOGIN_BAD_CREDENTIALS); |
| 92 | verify(loginLogService).createLoginLog( | 92 | verify(loginLogService).createLoginLog( |
| 93 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | 93 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) |
| @@ -97,7 +97,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -97,7 +97,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 97 | } | 97 | } |
| 98 | 98 | ||
| 99 | @Test | 99 | @Test |
| 100 | - public void testLogin0_badCredentials() { | 100 | + public void testAuthenticate_badCredentials() { |
| 101 | // 准备参数 | 101 | // 准备参数 |
| 102 | String username = randomString(); | 102 | String username = randomString(); |
| 103 | String password = randomString(); | 103 | String password = randomString(); |
| @@ -107,7 +107,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -107,7 +107,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 107 | when(userService.getUserByUsername(eq(username))).thenReturn(user); | 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 | AUTH_LOGIN_BAD_CREDENTIALS); | 111 | AUTH_LOGIN_BAD_CREDENTIALS); |
| 112 | verify(loginLogService).createLoginLog( | 112 | verify(loginLogService).createLoginLog( |
| 113 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | 113 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) |
| @@ -117,7 +117,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -117,7 +117,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 117 | } | 117 | } |
| 118 | 118 | ||
| 119 | @Test | 119 | @Test |
| 120 | - public void testLogin0_userDisabled() { | 120 | + public void testAuthenticate_userDisabled() { |
| 121 | // 准备参数 | 121 | // 准备参数 |
| 122 | String username = randomString(); | 122 | String username = randomString(); |
| 123 | String password = randomString(); | 123 | String password = randomString(); |
| @@ -129,7 +129,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | @@ -129,7 +129,7 @@ public class AuthServiceImplTest extends BaseDbUnitTest { | ||
| 129 | when(userService.isPasswordMatch(eq(password), eq(user.getPassword()))).thenReturn(true); | 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 | AUTH_LOGIN_USER_DISABLED); | 133 | AUTH_LOGIN_USER_DISABLED); |
| 134 | verify(loginLogService).createLoginLog( | 134 | verify(loginLogService).createLoginLog( |
| 135 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) | 135 | argThat(o -> o.getLogType().equals(LoginLogTypeEnum.LOGIN_USERNAME.getType()) |