本片主要讲SpringCloud Oauth2篇的实战改造,如动态权限、集成JWT、更改默认url、数据库加载client信息等改造。 同时,这应该也是我这系列博客的完结篇。 关于Oauth2,我也想说几句: 如果真的要应用到企业级项目当中去,必须要进行充足的准备,因为默认的配置、UI等很多都不是通用的(不适用于各个公司), 但是这套框架还好留了很多适配方法等,因此其需要修改的配置、处理器、方法重写等逻辑确实很多很多。 话不多说,正文开始。
常用的权限校验机制如以下几点:
参考:SpringCloud+SpringSecurity+Oauth2实现角色权限四种方案 - 知乎
| 类型 | 示例 |
|---|---|
| 硬编码 | 如在接口上添加注解:@PreAuthorize("hasAnyRole('ROLE_ADMIN')") |
| HttpSecurity动态增加 | 在资源服务配置类中配置,如:.authorizeRequests().anyRequest().authenticated() |
如果出现用户所拥有的权限出现变化时,上述两种是无法满足的。
百度了几天后网上确实有同学给出了不错的示例,其工作机制如下:

- @Component
- public class VipAccessDecisionManager implements AccessDecisionManager {
-
- /**
- * 决定当前用户是否有权限访问该请求
- **/
- @Override
- public void decide(Authentication authentication, Object object,
- Collection<ConfigAttribute> configAttributes) throws AccessDeniedException, InsufficientAuthenticationException {
- for (ConfigAttribute configAttribute : configAttributes) {
- //将访问所需资源或用户拥有资源进行比对
- String needAuthority = configAttribute.getAttribute();
- if (needAuthority == null) {
- continue;
- }
- for (GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
- if (needAuthority.trim().equals(grantedAuthority.getAuthority())) {
- return;
- }
- }
- }
- throw new AccessDeniedException("抱歉,您没有访问权限");
- }
-
- @Override
- public boolean supports(ConfigAttribute configAttribute) {
- return true;
- }
-
- @Override
- public boolean supports(Class> aClass) {
- return true;
- }
-
- }
- public class VipSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
-
- private static final AntPathMatcher ANT_PATH_MATCHER = new AntPathMatcher();
-
- private Set<PermRoleEntity> permRoleEntitySet;
-
- private final FilterInvocationSecurityMetadataSource superMetadataSource;
- private final VipSecurityOauthService vipSecurityOauthService;
-
- public VipSecurityMetadataSource(FilterInvocationSecurityMetadataSource superMetadataSource,
- VipSecurityOauthService vipSecurityOauthService) {
- this.superMetadataSource = superMetadataSource;
- this.vipSecurityOauthService = vipSecurityOauthService;
- }
-
- private void loadPerms() {
- permRoleEntitySet = vipSecurityOauthService.loadPerms();
- }
-
- /**
- * 返回能访问该请求的所有角色集合
- **/
- @Override
- public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
- loadPerms();
-
- FilterInvocation fi = (FilterInvocation) object;
- String access_uri = fi.getRequestUrl();
-
- for (PermRoleEntity permRoleEntity : permRoleEntitySet) {
- if (ANT_PATH_MATCHER.match(permRoleEntity.getAccessUri(), access_uri))
- return permRoleEntity.getConfigAttributeList();
- }
-
- return superMetadataSource.getAttributes(object);
- }
-
- @Override
- public Collection<ConfigAttribute> getAllConfigAttributes() {
- loadPerms();
-
- Set<ConfigAttribute> attributeSet = new HashSet<>();
- permRoleEntitySet.stream().map(PermRoleEntity::getConfigAttributeList).forEach(attributeSet::addAll);
- return attributeSet;
- }
-
- @Override
- public boolean supports(Class> clazz) {
- return FilterInvocation.class.isAssignableFrom(clazz);
- }
- }
VipSecurityOauthService:权限的动态加载api
这里写成静态的,好参考。
- @Component
- public class VipSecurityOauthService {
-
- /**
- * 动态加载权限-角色信息
- **/
- public Set
loadPerms() { - Set
permRoleEntitySet = new HashSet<>(); - permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/demo/admin").setConfigAttributeList(SecurityConfig.createList("admin")));
- permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/auth/**").setConfigAttributeList(SecurityConfig.createList("admin")));
-
- permRoleEntitySet.add(new PermRoleEntity().setAccessUri("/demo/sp-admin").setConfigAttributeList(SecurityConfig.createList("sp_admin")));
-
- return permRoleEntitySet;
- }
-
- }
PermRoleEntity:url和角色对应关系
- @Data
- @Accessors(chain = true)
- public class PermRoleEntity {
-
- /**
- * 访问的接口
- **/
- private String accessUri;
-
- /**
- * 可访问该接口的角色集合
- **/
- private List<ConfigAttribute> configAttributeList;
- }

获取用户角色为admin的token,进行接口访问
①:用户admin角色的用户访问admin管理的接口

①:用户admin角色的用户访问sp-admin管理的接口

- @Bean
- public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
- return new JwtTokenStore(jwtAccessTokenConverter);
- }
-
- @Bean
- @Primary
- public AuthorizationServerTokenServices defaultTokenServices(TokenStore tokenStore,
- JwtAccessTokenConverter jwtAccessTokenConverter,
- ClientDetailsService clientDetailsService,
- AuthenticationManager authenticationManager) {
- DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
- defaultTokenServices.setSupportRefreshToken(true);
- defaultTokenServices.setTokenStore(tokenStore);
- defaultTokenServices.setTokenEnhancer(jwtAccessTokenConverter);
- defaultTokenServices.setAccessTokenValiditySeconds(3600);
- defaultTokenServices.setRefreshTokenValiditySeconds(7200);
- defaultTokenServices.setClientDetailsService(clientDetailsService);
- defaultTokenServices.setAuthenticationManager(authenticationManager);
-
- return defaultTokenServices;
- }
-
- @Bean
- public JwtAccessTokenConverter jwtAccessTokenConverter() {
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- // 签名密钥
- jwtAccessTokenConverter.setSigningKey("sign_key");
- // 验证密钥
- jwtAccessTokenConverter.setVerifier(new MacSigner("sign_key"));
- return jwtAccessTokenConverter;
- }

tokenservice: 创建token、刷新token的地方。
获取token

在前文中提到 OAuth2 Client Service 处理请求的时候是无法识别token的,需要远程提交给认证中心(OAuth2 Server)去识别token。
token换成JWT后客户端服务(OAuth2 Client Service)也可以识别token了,均需做如下配置:
- @Bean
- public TokenStore tokenStore(JwtAccessTokenConverter jwtAccessTokenConverter) {
- return new JwtTokenStore(jwtAccessTokenConverter);
- }
-
- @Bean
- public JwtAccessTokenConverter jwtAccessTokenConverter() {
- JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
- // 签名密钥
- jwtAccessTokenConverter.setSigningKey("sign_key");
- // 验证密钥
- jwtAccessTokenConverter.setVerifier(new MacSigner("sign_key"));
- return jwtAccessTokenConverter;
- }
前文中提到的这项配置也可以丢弃了。

访问客户端服务的接口测试:

| 默认url | 作用 |
|---|---|
| /oauth/authorize | 授权端点 |
| /oauth/token | 令牌端点 |
| /oauth/confirm_access | 用户批准授权的端点 |
| /oauth/error | 用于渲染授权服务器的错误 |
| /oauth/check_token | 资源服务器解码access token |
| /oauth/check_token | 当使用JWT的时候,暴露公钥的端点 |
可以按照下面这张方法进行重新转交接口地址:

自定义处理接口代码:
- @RestController
- public class AuthController extends WebApiController {
- private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
-
- private final ConsumerTokenServices consumerTokenServices;
- private final TokenEndpoint tokenEndpoint;
- private final AuthorizationEndpoint authorizationRequest;
- private final WhitelabelApprovalEndpoint whitelabelApprovalEndpoint;
- private final WhitelabelErrorEndpoint whitelabelErrorEndpoint;
-
- public AuthController(ConsumerTokenServices consumerTokenServices, TokenEndpoint tokenEndpoint,
- AuthorizationEndpoint authorizationRequest, WhitelabelApprovalEndpoint whitelabelApprovalEndpoint,
- WhitelabelErrorEndpoint whitelabelErrorEndpoint) {
- this.consumerTokenServices = consumerTokenServices;
- this.tokenEndpoint = tokenEndpoint;
- this.authorizationRequest = authorizationRequest;
- this.whitelabelApprovalEndpoint = whitelabelApprovalEndpoint;
- this.whitelabelErrorEndpoint = whitelabelErrorEndpoint;
- }
-
- /**
- * 自定义登录接口
- */
- @PostMapping(value = "/login")
- public ResponseEntity<String> login(@RequestBody UserLoginDto userLoginDto, Principal principal) throws HttpRequestMethodNotSupportedException {
- Map<String, String> mapDto = ObjectMapperUtil.str2Obj(userLoginDto, new TypeReference<Map<String, String>>() {
- });
- mapDto.put(GrantTypeConstants.GRANT_TYPE, GrantTypeConstants.PASSWORD);
-
- OAuth2AccessToken token;
- try {
- token = tokenEndpoint.postAccessToken(principal, mapDto).getBody();
- if (token == null) {
- throw new ServiceException("登录异常");
- }
- } catch (Exception e) {
- if (e instanceof InvalidGrantException) {
- throw new ServiceException("用户名密码错误");
- } else {
- throw e;
- }
- }
- return response(WebApiResponse.ok(AuthToken.build(token)));
- }
- }

前文中以ProcessOn举例的QQ第三方登录就提到ProcessOn会向QQ申请一个client_id(客户端凭证),那么QQ第三方登录配置申请的入口必须将数据存放在数据库中,这样才能做到动态的新增、删除等,代码配置写死是不可能的。
注意:需要引入数据源、mysql驱动的依赖,并配置好数据源。
因为官方给的sql是hql的,我用的mysql8,因此做了一些类型上的修改。
- SET NAMES utf8mb4;
- SET FOREIGN_KEY_CHECKS = 0;
-
- -- ----------------------------
- -- Table structure for clientdetails
- -- ----------------------------
- DROP TABLE IF EXISTS `clientdetails`;
- CREATE TABLE `clientdetails` (
- `appId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
- `resourceIds` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `appSecret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `grantTypes` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `redirectUrl` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `access_token_validity` int(0) NULL DEFAULT NULL,
- `refresh_token_validity` int(0) NULL DEFAULT NULL,
- `additionalInformation` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `autoApproveScopes` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- PRIMARY KEY (`appId`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of clientdetails
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for oauth_access_token
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_access_token`;
- CREATE TABLE `oauth_access_token` (
- `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `token` binary(1) NULL DEFAULT NULL,
- `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
- `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `authentication` binary(1) NULL DEFAULT NULL,
- `refresh_token` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- PRIMARY KEY (`authentication_id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_access_token
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for oauth_approvals
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_approvals`;
- CREATE TABLE `oauth_approvals` (
- `userId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `clientId` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `status` varchar(10) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `expiresAt` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0) ON UPDATE CURRENT_TIMESTAMP(0),
- `lastModifiedAt` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP(0)
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_approvals
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for oauth_client_details
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_client_details`;
- CREATE TABLE `oauth_client_details` (
- `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
- `resource_ids` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `client_secret` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `scope` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `authorized_grant_types` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `web_server_redirect_uri` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `authorities` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `access_token_validity` int(0) NULL DEFAULT NULL,
- `refresh_token_validity` int(0) NULL DEFAULT NULL,
- `additional_information` varchar(4096) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `autoapprove` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- PRIMARY KEY (`client_id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_client_details
- -- ----------------------------
- INSERT INTO `oauth_client_details` VALUES ('admin', 'admin', 'tgMj02VG9dkpeKUN5lSWSsKotIt2yTIElkFcvsqLnwGOspNYhe+Teg==', 'test,all', 'authorization_code,client_credentials,password,implicit,refresh_token', 'http://www.baidu.com', 'admin', NULL, NULL, NULL, 'all');
-
- -- ----------------------------
- -- Table structure for oauth_client_token
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_client_token`;
- CREATE TABLE `oauth_client_token` (
- `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `token` binary(1) NULL DEFAULT NULL,
- `authentication_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
- `user_name` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `client_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- PRIMARY KEY (`authentication_id`) USING BTREE
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_client_token
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for oauth_code
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_code`;
- CREATE TABLE `oauth_code` (
- `code` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `authentication` binary(1) NULL DEFAULT NULL
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_code
- -- ----------------------------
-
- -- ----------------------------
- -- Table structure for oauth_refresh_token
- -- ----------------------------
- DROP TABLE IF EXISTS `oauth_refresh_token`;
- CREATE TABLE `oauth_refresh_token` (
- `token_id` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
- `token` binary(1) NULL DEFAULT NULL,
- `authentication` binary(1) NULL DEFAULT NULL
- ) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC;
-
- -- ----------------------------
- -- Records of oauth_refresh_token
- -- ----------------------------
-
- SET FOREIGN_KEY_CHECKS = 1;
在这个表中配置数据即可,参考代码静态配置:

- @Override
- public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
-
- clients.jdbc(dataSource);
-
- }
做好以上配置,认证服务就可以从数据库加载客户端凭证信息了。