本文承接上文《从0到1 手把手搭建spring cloud alibaba 微服务大型应用框架(三) (mini-cloud) 搭建认证服务(认证/资源分离版) oauth2.0 (上)》
依然是介绍认证中心搭建细节
上文中介绍了总体流程并且完成了以下环境
##1.创建authentication-center 服务 ✔
##2.将authentication-center 服务集成入spring cloud nacos注册中心 ✔
##3.选择oauth2 认证模式,分离认证服务器以及资源服务器,设计下沉式资源端认证模式 ✔
##4.按照spring 官方内存式认证demo 搭建oauth2 client 认证 ✔
##5.添加测试服务module 并集成如spring cloud nacos 中 ✔
##6.测试服务 作为resource server 进行认证测试 ✔
##7.将配置文件参数纳入nacos 配置中心进行管理
##8.将内存式认证改为实际redis 以及clientdetail ,userdetail 为数据库中获取
##9.将demo 权限认证改为根据当前登陆用户角色动态校验权限
##10.抽取认证服务器与资源服务器共通部分变为common module
今天主要介绍后续的7,8,9环节,10作为共通抽取将单独作为一篇重构篇
##7.将配置文件参数纳入nacos 配置中心进行管理 ###7.1 为什么要纳入nacos 配置中心进行管理 ###7.2 spring cloud nacos 配置中心交互流程 ###7.3 本地环境配置文件说明 ###7.4 创建nacos 配置中心创建共通配置以及各服务对应环境配置文件 ###7.5 区分哪些是共同配置放入nacos 共通文件中 ###7.6 将各自服务独立的配置放入各自对应环境的nacos 配置文件中 ###7.7 启动各个服务测试 ##8.将内存式认证改为实际redis 以及clientdetail ,userdetail 为数据库中获取 ###8.1 如何搭建redis 单机多级集群以及如何接入项目 ###8.2 集成以及使用持久层框架fluent-mybatis ###8.3 创建upms 用户统一权限管理中心服务并开发用户查询接口 ###8.4 集成feign远程调用接口以及feign相关权限设置 ###8.5 将tokenStore 由InMemory方式变为redis 方式 ###8.6 将clientdetail 由InMemory方式变为持久化方式 ###8.7 将userdetail 由InMemory方式变为持久化方式 ##9.权限认证改为根据当前登陆用户角色动态校验权限 ###9.1 什么是动态权限校验 ###9.2 权限系统动态校验流程 ###9.3 创建权限关联表 ###9.4 权限匹配拦截 ###9.5 单元测试造数据并且测试
好了我们按照流程一 一说明
我们目前所有配置都是写在本地.yml 获取properties里面的,虽然可以区分环境设置多个,比如
-dev.yml,-test.yml,-prod.yml,然后打包时可以打包成对应配置jar或者war,这样一般情况是可以的,但是如果是一些可能需要动态变化的变量,不如一些不在数据库管理的一些黑白名单,中间件的扩容或者ip变化等,如果每次修改时都要从新打包太麻烦了,因此就要引入配置中心的概念,将对应环境的配置纳入配置中心管理,如果是eureka 可以是git ,也可以用apollo ,本文是在nacos体系开发的,用的是nacos
简单画了一个逻辑上的交胡流程图
简要顺序说明: 后面会有每一步的详细说明和截图
1.首先nacos配置中心 创建服务A 和B指定的启动环境文件
2.启动A和B连接了nacos配置中心的话,会自动获取到对应各自环境的配置文件内容
3.如果用户对某个配置做了修改,该修改会主动推送到对应的连接应用
我们本文只针对认证中心的本地配置进行nacos管理,其他应用默认也放进nacos ,就不在多余描述
首先看看目前我们认证中心的本地配置结构
我们通用里面基本没有配置,目前都是直接放在dev里面的,dev.yml配置如下
server:
port: 8800
spring:
application:
name: @artifactId@
cloud:
nacos:
discovery:
server-addr: ${NACOS_HOST:127.0.0.1}:${NACOS_PORT:8848}
config:
server-addr: ${spring.cloud.nacos.discovery.server-addr}
file-extension: yml
shared-configs:
- application-${spring.profiles.active}.${spring.cloud.nacos.config.file-extension}
profiles:
active: @profiles.active@
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:192.168.1.59}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_auth}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# Druid数据源配置
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#配置从连接池获取连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnBorrow: false
#配置向连接池归还连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnReturn: false
#打开PsCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
#合并多个DruidDatasource的监控数据
useGlobalDataSourceStat: true
#通过connectProperties属性来打开mergesql功能罗慢sQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500;
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
default:
connectTimeout: 100000
readTimeout: 100000
redis:
mode: sentinel
password: 123456
sentinel:
master: local-master
nodes:
- 192.168.1.177:26379
- 192.168.1.177:26380
- 192.168.1.177:26381
lettuce:
pool:
max-active: 10
max-wait: -1
max-idle: 5
min-idle: 1
database: 7
我们先启动nacos 注册中心
浏览器输入http://localhost:8848/nacos 进入nacos配置中心
我要创建认证中心的dev环境的配置文件
所以命名要servername + env.yml,如图
之前配置文件的下图部分一定要在本地配置文件中,因为只有启动后连接了nacos才能获取里面配置内容
其他部分我们都挪进nacos ,注意spring: 这个不要落下了
authentication-center-dev.yml |
server:
port: 8800
spring:
datasource:
type: com.alibaba.druid.pool.DruidDataSource
# driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://${MYSQL_HOST:192.168.1.59}:${MYSQL_PORT:3306}/${MYSQL_DB:mini_cloud_auth}?characterEncoding=utf8&zeroDateTimeBehavior=convertToNull&useSSL=false&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=GMT%2B8&allowMultiQueries=true&allowPublicKeyRetrieval=true
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
# Druid数据源配置
# 初始连接数
initialSize: 5
# 最小连接池数量
minIdle: 10
# 最大连接池数量
maxActive: 20
# 配置获取连接等待超时的时间
maxWait: 60000
# 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
timeBetweenEvictionRunsMillis: 60000
# 配置一个连接在池中最小生存的时间,单位是毫秒
minEvictableIdleTimeMillis: 300000
# 配置一个连接在池中最大生存的时间,单位是毫秒
maxEvictableIdleTimeMillis: 900000
# 配置检测连接是否有效
validationQuery: SELECT 1
#申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
testWhileIdle: true
#配置从连接池获取连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnBorrow: false
#配置向连接池归还连接时,是否检查连接有效性,true每次都检查;false不检查。做了这个配置会降低性能。
testOnReturn: false
#打开PsCache,并且指定每个连接上PSCache的大小
poolPreparedStatements: true
maxPoolPreparedStatementPerConnectionSize: 20
# 配置监控统计拦截的filters,去掉后监控界面sql无法统计,'wall'用于防火墙
#合并多个DruidDatasource的监控数据
useGlobalDataSourceStat: true
#通过connectProperties属性来打开mergesql功能罗慢sQL记录
connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500;
feign:
sentinel:
enabled: true
okhttp:
enabled: true
httpclient:
enabled: false
client:
config:
default:
connectTimeout: 100000
readTimeout: 100000
# redis:
# mode: sentinel
# password: 123456
# sentinel:
# master: local-master
# nodes:
# - 192.168.1.177:26379
# - 192.168.1.177:26380
# - 192.168.1.177:26381
# lettuce:
# pool:
# max-active: 10
# max-wait: -1
# max-idle: 5
# min-idle: 1
# database: 7
#
redis:
mode: singleten
host: 127.0.0.1
port: 6379
password: 123456
database: 7
lettuce:
pool:
max-active: 10
max-wait: -1
max-idle: 5
min-idle: 1
我们可以针对各个dev 环境应用共通的配置抽出来作为一个application-dev.yml 管理,如上图
我们因为同样的环境一般来说各个应用某些配置是一样的,比如:
redis 配置
mq 配置
分布式文件系统配置
监控配置
环境常量配置
我们将redis 抽出为共通放入,将原来放入authentication-center-dev.yml 中 的redis删掉
我们同上操作,创建authentication-center-test dev以及upms-center-biz-dev.yml
nacos中有了这些配置文件
原来本地的配置文件内容只剩下如下
看起来是不是简洁多了
我们分别启动几个服务看看是否没有问题
都启动了,我们进入nacos中看看是否注册成功
也都没有问题
这段是本文的重点,将之前的认证处理由内存形式改完实际应用中的内存或者数据库形式
我缓存用的redis,网上相关介绍太多了,就不做详细描述,大家自行集成,也可以请参考我之前的文章
《docker-compose 搭建单机版/多机版 redis sentinel 哨兵集群》
《spring boot redis集成 代码不变,通过配置文件一键切换 sentinel模式 ,cluster模式 以及单机模式》
我持久层选用的是的fluent-mybatis,主要是看中了自动生成mapper以及所有场景均可代码实现,网上也是很多介绍,大家自行集成,也可以请参考我之前的文章
《spring cloud alibaba 集成feign 自定义feign权限注解 某接口只允许feign访问 附带所有流程以及代码》
upms module是我们mini-cloud框架中的用户统一权限管理中心
本文不做过多描述,后面会有单独的一篇介绍,这里简要做个介绍就是
upm 服务主要做了两件事:
1. 提供了对用户,角色,权限的管理。
2.提供了一个fegin接口,可以让认证中心通过fegin 通过用户名可以返回用户基本信息,角色信息和权限信息等
请参考我之前文章《spring cloud alibaba 集成feign 自定义feign权限注解 某接口只允许feign访问 附带所有流程以及代码》
上文中我们是以内存形式集成的tokenStore,也就是说我们的登陆认证信息都保存到了本地内存中,实际应用肯定是不可以的,我们现在集成RedisTokenStore
位置
新增了TokenStore bean
/**
* 使用reids 保存token 替换原来的内存存储,并设置前缀为统一mini-cloud-token: 便于查询和管理
* */
@Bean
public TokenStore tokenStore(){
RedisTokenStore redisTokenStore = new RedisTokenStore(redisConnectionFactory);
redisTokenStore.setPrefix(MINI_CLOUD_PREFOX);
return redisTokenStore;
}
然后
就可以了,现在访问试一下,看看是否redis中会有token存入
http://localhost:8800/oauth/token?username=user3&password=123&grant_type=password&scope=read&client_id=test-auth-client&client_secret=123
确认进入了redis
上篇中我们内存的形式
具体改为我们自己的service去处理,service涉及到的表结构我们直接使用官网的schema.sql创建表
具体地址:https://github.com/spring-projects/spring-security-oauth/blob/master/spring-security-oauth2/src/test/resources/schema.sql
创建authentication-center 的对应数据库mini_cloud_auth ,执行sql后的表结构如下:
然后我们将authentication-center 服务连接到mini_cloud_auth库,具体在之前的nacos中管理了
连接之后尝试启动,没问题之后我们创建自己的clientService ,位置
MiniCloudClientDetailServiceImpl.java
package com.minicloud.authentication.service;
import org.springframework.security.oauth2.common.exceptions.InvalidClientException;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
/**
* @Author alan.wang
* @date: 2022-01-18 10:51
*/
@Service
public class MiniCloudClientDetailServiceImpl extends JdbcClientDetailsService {
public MiniCloudClientDetailServiceImpl(DataSource dataSource) {
super(dataSource);
}
@Override
public ClientDetails loadClientByClientId(String clientId) throws InvalidClientException {
return super.loadClientByClientId(clientId);
}
}
原来的内存形式替换为
将数据库中添加一条数据,可以和原来内存数据保持一致
原来的代码
@Bean
public UserDetailsService userDetailsService() {
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
manager.createUser(User.builder().username("user1").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).roles("USER").build());
manager.createUser(User.builder().username("admin").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).roles("USER", "ADMIN").build());
return manager;
}
我们新建 UserDetailsService
package com.minicloud.authentication.service;
import com.minicloud.authentication.model.MiniCloudGrantedAuthority;
import com.minicloud.authentication.model.MiniCloudUserDetails;
import com.minicloud.upms.perms.dto.UpmsPermDTO;
import com.minicloud.upms.user.dto.UpmsUserDTO;
import com.minicloud.upms.user.fegin.UpmsCenterRemoteUserService;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import javax.annotation.Resource;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
/**
* @Author alan.wang
* @date: 2022-01-21 12:02
* @desc: UserDetailsService 实现类,实现自定义userdetails 查询接口
*/
public class MiniCloudUserDetailServiceImpl implements UserDetailsService {
@Resource
private UpmsCenterRemoteUserService upmsCenterRemoteUserService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
//通过upms 服务获取用户基本信息,角色以及权限信息
UpmsUserDTO upmsUserDTO = upmsCenterRemoteUserService.queryUpmsUserByUsername(username);
Set upmsPermDTOSet = new HashSet<>();
List upmsPermDTOSList = upmsUserDTO.getUpmsRoleDTOS().stream().map(upmsRoleDTO -> upmsRoleDTO.getUpmsPermDTOS()).reduce((result, upmsRoleDTOS) -> {
result.addAll(upmsRoleDTOS);
return result;
}).get();
upmsPermDTOSet.addAll(upmsPermDTOSList);
//调整为自定义的GrantedAuthority
List authorities = MiniCloudGrantedAuthority.loadAuthorities(upmsPermDTOSet);
//自定义MiniCloudUserDetails 保存进redisTokenStore
MiniCloudUserDetails userDetails = new MiniCloudUserDetails(upmsUserDTO.getUserId(),upmsUserDTO.getUsername(), upmsUserDTO.getPassword(),authorities );
return userDetails;
}
}
上面代码主要就是通过feign调用upms的findUserbyName 获取用户信息,后面会有详细说明
然后集成进入auth管理
动态权限校验一般是指每个角色有不通的访问权限,例如管理员角色可以将普通用户角色加入黑名单等,角色又是挂载到每个用户身上的,一般一个用户可以又多个用户角色,每个角色绑定多个权限,可以设计成用户登陆时,根据用户账号关联查询到角色和用户,然后针对访问的路径进行校验
SET FOREIGN_KEY_CHECKS=0;
-- ----------------------------
-- Table structure for upms_perm
-- ----------------------------
CREATE TABLE `upms_perm` (
`perm_id` int(6) NOT NULL AUTO_INCREMENT COMMENT '主键',
`perm_url` varchar(255) NOT NULL COMMENT '权限url',
`perm_method` varchar(10) NOT NULL COMMENT '请求方式:get,post,put,delete 等',
`perm_name` varchar(255) NOT NULL COMMENT '权限名称',
`perm_desc` varchar(500) DEFAULT NULL COMMENT '权限描述',
`perm_server` varchar(20) DEFAULT NULL COMMENT '所属服务',
PRIMARY KEY (`perm_id`)
) ENGINE=InnoDB AUTO_INCREMENT=45 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for upms_role
-- ----------------------------
CREATE TABLE `upms_role` (
`role_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`role_name` varchar(64) DEFAULT NULL COMMENT '角色名称',
`role_code` varchar(64) DEFAULT NULL COMMENT '角色code',
`role_desc` varchar(255) DEFAULT NULL COMMENT '角色描述',
PRIMARY KEY (`role_id`)
) ENGINE=InnoDB AUTO_INCREMENT=36 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for upms_role_perm
-- ----------------------------
CREATE TABLE `upms_role_perm` (
`role_id` int(11) NOT NULL DEFAULT '0' COMMENT '角色id',
`perm_id` int(5) NOT NULL COMMENT '权限url',
PRIMARY KEY (`role_id`,`perm_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for upms_user
-- ----------------------------
CREATE TABLE `upms_user` (
`user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`username` varchar(64) DEFAULT NULL COMMENT '用户名',
`password` varchar(255) DEFAULT NULL COMMENT '密码',
PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=39 DEFAULT CHARSET=utf8mb4;
-- ----------------------------
-- Table structure for upms_user_role
-- ----------------------------
CREATE TABLE `upms_user_role` (
`user_id` int(11) NOT NULL COMMENT '用户ID',
`role_id` int(11) NOT NULL COMMENT '角色ID',
PRIMARY KEY (`user_id`,`role_id`),
KEY `user_id` (`user_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
我们结合 4.2权限系统动态校验流程 梳理我们自己开发流程
#1.首先我们登陆需要保存下用户信息,这个必须是 UserDetails的子类,我们需要自定义我们自己的UserDetails 由于我们要校验权限,所以里面需要包含角色和权限
#2.校验的时候我们需要获取到当前登陆用户的角色和权限,匹配当前要访问的url,校验是否登陆
MiniCloudUserDetails.java
package com.minicloud.authentication.model;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;
import org.springframework.security.core.userdetails.User;
import org.w3c.dom.stylesheets.LinkStyle;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
/**
* @Author alan.wang
* @date: 2022-01-21 12:05
* @desc: 保存到oauth 缓存中的数据,携带自定义属性的话需要自己添加
*/
public class MiniCloudUserDetails extends User {
private Integer id;
private Collection miniCloudGrantedAuthorities;
private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;
public MiniCloudUserDetails(Integer id,String username, String password, Collection extends GrantedAuthority> authorities) {
super(username, password, Collections.EMPTY_LIST);
this.id = id;
this.miniCloudGrantedAuthorities = (Collection)authorities;
}
public MiniCloudUserDetails(Integer id,String username, String password, boolean enabled, boolean accountNonExpired, boolean credentialsNonExpired, boolean accountNonLocked, Collection extends GrantedAuthority> authorities) {
super(username, password, enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, Collections.EMPTY_LIST);
this.id = id;
this.miniCloudGrantedAuthorities = (Collection)authorities;
}
public Integer getId() {
return id;
}
public Collection getMiniCloudGrantedAuthorities() {
return miniCloudGrantedAuthorities;
}
public void setMiniCloudGrantedAuthorities(Collection miniCloudGrantedAuthorities) {
this.miniCloudGrantedAuthorities = miniCloudGrantedAuthorities;
}
}
例如:
MiniCloudUserDetails miniCloudUserDetails = (MiniCloudUserDetails)SecurityContextHolder.getContext().getAuthentication().getPrincipal();
需要以下几步
MiniCloudTokenEnhancer.java
package com.minicloud.authentication.config;
import com.minicloud.authentication.model.MiniCloudUserDetails;
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
import org.springframework.security.oauth2.common.OAuth2AccessToken;
import org.springframework.security.oauth2.provider.OAuth2Authentication;
import org.springframework.security.oauth2.provider.token.TokenEnhancer;
import org.springframework.stereotype.Component;
import java.util.HashMap;
import java.util.Map;
/**
* @Author alan.wang
* @date: 2022-01-25 15:06
*/
@Component
public class MiniCloudTokenEnhancer implements TokenEnhancer {
/**
* 自定义用户基本信息
*/
private static final String DETAILS_USER = "user_info";
/**
* 客户端模式
*/
private static final String CLIENT_CREDENTIALS ="client_credentials";
/**
* 协议字段
*/
private static final String DETAILS_LICENSE = "license";
/**
* 激活字段 兼容外围系统接入
*/
private static final String ACTIVE = "active";
/**
* 扩展auth 认证中map 存放token 内容,client 模式不处理
* */
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
if (CLIENT_CREDENTIALS.equals(authentication.getOAuth2Request().getGrantType())) {
return accessToken;
}
final Map additionalInfo = new HashMap<>(8);
MiniCloudUserDetails miniCloudUserDetails = (MiniCloudUserDetails) authentication.getUserAuthentication().getPrincipal();
additionalInfo.put(DETAILS_USER, miniCloudUserDetails);
additionalInfo.put(DETAILS_LICENSE, "made by mini-cloud");
additionalInfo.put(ACTIVE, Boolean.TRUE);
((DefaultOAuth2AccessToken) accessToken).setAdditionalInformation(additionalInfo);
return accessToken;
}
}
MiniCloudUserAuthenticationConverter.java
package com.minicloud.authentication.test.config;
import cn.hutool.core.map.MapUtil;
import com.minicloud.authentication.test.model.MiniCloudUserDetails;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.provider.token.UserAuthenticationConverter;
import org.springframework.util.StringUtils;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
* @Author alan.wang
* @date: 2022-01-25 13:59
*/
public class MiniCloudUserAuthenticationConverter implements UserAuthenticationConverter {
/**
* 不适用标记,即密码在这不给出来
* */
private static final String N_A = "N/A";
@Override
public Map convertUserAuthentication(Authentication authentication) {
Map response = new LinkedHashMap<>();
response.put(USERNAME, authentication.getName());
if (authentication.getAuthorities() != null && !authentication.getAuthorities().isEmpty()) {
response.put(AUTHORITIES, AuthorityUtils.authorityListToSet(authentication.getAuthorities()));
}
return response;
}
/**
*
* 将check_token 中返回的OAuth2Authentication的getPrincipal 重写为我们自己的miniclouddetail
* */
@Override
public Authentication extractAuthentication(Map responseMap) {
if (responseMap.containsKey(USERNAME)) {
Map map = MapUtil.get(responseMap, "user_info", Map.class);
List
以上几步完成后重新启动认证服务会发现返回数据多了我们存储的部分
我们在资源服务中自定义AccessDecisionManager 完成权限校验
MiniCloudAccessDecisionManager.java
package com.minicloud.authentication.test.config;
import com.minicloud.authentication.test.model.MiniCloudUserDetails;
import org.springframework.security.access.AccessDecisionManager;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.ConfigAttribute;
import org.springframework.security.authentication.InsufficientAuthenticationException;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.FilterInvocation;
import java.util.Collection;
/**
* @Author alan.wang
* @date: 2022-01-25 11:48
*/
public class MiniCloudAccessDecisionManager implements AccessDecisionManager {
/**
* @desc :通过获取自定义的MiniCloudUserDetails
* 取得当前登陆人的所有grantedAuthorities 然后一 一和当前访问路径匹配,如果请求方式与url均一致则说明认证成功
* 否则抛出AccessDeniedException 异常
*
* */
@Override
public void decide(Authentication authentication, Object filterInvocation, Collection collection) throws AccessDeniedException, InsufficientAuthenticationException {
String requestUrl = ((FilterInvocation) filterInvocation).getRequestUrl();
String method = ((FilterInvocation) filterInvocation).getRequest().getMethod();
if(authentication.getPrincipal() instanceof String){
throw new AccessDeniedException(authentication.getName()+",无权访问url:"+requestUrl);
}
Collection grantedAuthorities = ((MiniCloudUserDetails)authentication.getPrincipal()).getMiniCloudGrantedAuthorities();
for (MiniCloudGrantedAuthority grantedAuthority : grantedAuthorities) {
if(requestUrl.equals(grantedAuthority.getAuthority())&&method.equals(grantedAuthority.getMethod())){
return;
}
}
throw new AccessDeniedException(authentication.getName()+",无权访问url:"+requestUrl);
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class> aClass) {
return true;
}
}
自定义 AccessDecisionManager 代码描述
1. 我们通过authentication.getPrincipal()获取到当前登陆用户的信息,也就是我们自定义的MiniCloudUserDetails
2.取得当前登陆用户信息里的所有grantedAuthorities
3.取得当前访问url
4.循环遍历匹配grantedAuthorities 中的url以及method,如果有则通过,没有则抛出异常
我们写一个初始化代码初始化一批用户,角色,权限,并挂载上
package com.minicloud.upms;
import com.alibaba.nacos.common.utils.HttpMethod;
import com.minicloud.upms.perms.dto.UpmsPermDTO;
import com.minicloud.upms.perms.dto.UpmsRolePermDTO;
import com.minicloud.upms.perms.service.UpmsPermService;
import com.minicloud.upms.perms.service.UpmsRolePermService;
import com.minicloud.upms.role.dto.UpmsRoleDTO;
import com.minicloud.upms.role.service.UpmsRoleService;
import com.minicloud.upms.user.dto.UpmsUserDTO;
import com.minicloud.upms.user.service.UpmsUserService;
import lombok.extern.slf4j.Slf4j;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
import java.util.stream.Stream;
/**
* @Author alan.wang
* @date: 2022-01-20 13:51
*/
@RunWith(SpringRunner.class)
@SpringBootTest(classes = MiniCloudUPMSApplication.class)
@Slf4j
public class UpmsInitTest {
@Autowired
private UpmsUserService upmsUserService;
@Autowired
private UpmsRoleService upmsRoleService;
@Autowired
private UpmsPermService upmsPermService;
@Autowired
private UpmsRolePermService upmsRolePermService;
/**
*
* 初始化角色
* */
@Test
public void testInitRoles() throws InterruptedException {
UpmsRoleDTO upmsRoleDTO1 = UpmsRoleDTO.builder().roleName("超级管理员").roleCode("SUPER_ADMIN").roleDesc("最大权限").build();
UpmsRoleDTO upmsRoleDTO2 = UpmsRoleDTO.builder().roleName("普通用户1").roleCode("USER1").roleDesc("普通用户1").build();
UpmsRoleDTO upmsRoleDTO3 = UpmsRoleDTO.builder().roleName("普通用户2").roleCode("USER1").roleDesc("普通用户2").build();
List upmsRoleDTOS= Stream.of(upmsRoleDTO1,upmsRoleDTO2,upmsRoleDTO3).collect(Collectors.toList());
List upmsRolesIds = upmsRoleService.saveRoles(upmsRoleDTOS);
log.info("init roles successful:{} ",upmsRolesIds.toArray().toString());
testInitUsers(upmsRolesIds);
testInitPerms(upmsRolesIds);
}
/**
* 初始化接口权限列表
* */
public void testInitPerms(List rolesIds){
//mini-cloud test 服务
UpmsPermDTO upmsPermDTO1 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello").permMethod(HttpMethod.GET).permName("hello接口").permDesc("测试hello get接口").build();
UpmsPermDTO upmsPermDTO2 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello").permMethod(HttpMethod.POST).permName("hello接口").permDesc("测试hello post接口").build();
UpmsPermDTO upmsPermDTO3 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello2").permMethod(HttpMethod.GET).permName("hello2接口").permDesc("测试hello2 get接口").build();
UpmsPermDTO upmsPermDTO4 = UpmsPermDTO.builder().permServer("test").permUrl("/test/hello2").permMethod(HttpMethod.POST).permName("hello2接口").permDesc("测试hello2 post接口").build();
//mini-cloud upms 服务
UpmsPermDTO upmsPermDTO5 = UpmsPermDTO.builder().permServer("upms").permUrl("/user/save").permMethod(HttpMethod.PUT).permName("保存用户接口").permDesc("测试保存用户接口").build();
UpmsPermDTO upmsPermDTO6 = UpmsPermDTO.builder().permServer("upms").permUrl("/findById/{userId}").permMethod(HttpMethod.GET).permName("根据userId 查询user接口").permDesc("测试根据userId 查询user接口").build();
List upmsPermDTOS = Stream.of(upmsPermDTO1,upmsPermDTO2,upmsPermDTO5,upmsPermDTO6,upmsPermDTO3,upmsPermDTO4).collect(Collectors.toList());
List permIds = upmsPermService.savePerms(upmsPermDTOS);
//关联角色1的权限集
testInitRolePerms(rolesIds.get(1),permIds.subList(0,4));
//关联角色2的权限集
testInitRolePerms(rolesIds.get(2),permIds.subList(4,6));
}
/**
* 初始化角色权限关联表
* */
public void testInitRolePerms(Integer roleId,List permsId){
List upmsRolePermDTOS = new ArrayList<>();
permsId.stream().forEach(pid->{
upmsRolePermDTOS.add(UpmsRolePermDTO.builder().roleId(roleId).permId(pid).build());
});
upmsRolePermService.saveRolePerms(upmsRolePermDTOS);
}
/**
*
* 初始化用户
* */
public void testInitUsers(List upmsRolesIds) throws InterruptedException {
UpmsUserDTO upmsUserDTO1 = UpmsUserDTO.builder().username("admin").password("{bcrypt}" + new BCryptPasswordEncoder().encode("admin")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(0)).build()).collect(Collectors.toList())).build();
UpmsUserDTO upmsUserDTO2 = UpmsUserDTO.builder().username("user3").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(1)).build()).collect(Collectors.toList())).build();
UpmsUserDTO upmsUserDTO3 = UpmsUserDTO.builder().username("user4").password("{bcrypt}" + new BCryptPasswordEncoder().encode("123")).upmsRoleDTOS(Stream.of(UpmsRoleDTO.builder().roleId(upmsRolesIds.get(2)).build()).collect(Collectors.toList())).build();
upmsUserService.saveUser(upmsUserDTO1);
upmsUserService.saveUser(upmsUserDTO2);
upmsUserService.saveUser(upmsUserDTO3);
}
}
执行完毕后如下
用户user3 具有/test/hello 路径的GET权限
我们测试一下
可以看到确实匹配到了,也执行完毕。
到此我们所有动态权限校验的代码部分就全部完成了,目前还都是最小闭环,真实应用还需要在此基础上扩展,我们开发中很多代码其实都是冗余的,一个类在多个服务中都会出现,也会出现很多魔法值,下篇会讲如何重构抽取共通部分,等到网关服务集成完毕之后会开源1.0版本代码~有需要得call me