@RestController
public class HelloController {
@RequestMapping(value = "/user",method = RequestMethod.POST)
@PreAuthorize("hasRole('admin')")
public String user() {
return "admin";
}
}
当需求是需要动态管理API接口,比如A用户 只开放ABCD四个接口,B用户只开放EFGH四个接口。
或者是当用户A刚注册只有A接口功能,用户A开通会员就有B接口功能。
1、自定义一个接口权限注解,参数包括接口地址、服务ID 。方便管理接口是哪个服务的
2、服务一启动时,自动搜集所有使用这个注解的接口,然后保存到数据库。这样权限的字典表就有了
3、管理员给用户配置接口权限。如用户A 配置A、B、C、D四个接口的权限
/**
* @author czx
* @title: PermissionAuth
* @projectName zhjg
* @description: TODO 接口权限验证注解
* @date 2020/5/1811:35
*/
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ResourceAuthPer {
// 接口中文注释
String value() default "";
// 服务 id
String[] exampleId() default {};
}
@RestController
@RequestMapping("/visitor")
public class VisitorController {
@ApiOperation(value = "来访人员列表")
@RequestMapping(value = "/visited",method = RequestMethod.POST)
@ResourceAuthPer(value = "来访人员列表",exampleId = "visitor-service")
public R visited(@RequestBody Visitor params){
List<Visitor> data = VisitorService.list();
return R.ok().setData(data);
}
}
/**
* @author czx
* @title: PermissionConfig
* @projectName zhjg
* @description: TODO 收集所有URL ROLE
* @date 2020/5/1811:35
*/
@Slf4j
@Component
public class PermissionConfig implements InitializingBean {
@Value("${spring.application.name}")
private String applicationName;
@Autowired
private WebApplicationContext applicationContext;
@Getter
@Setter
private List<PermissionEntityVO> permissionEntities = Lists.newArrayList();
@Bean
@LoadBalanced
public RestTemplate restTemplate(){
return new RestTemplate();
}
@SneakyThrows
@Override
public void afterPropertiesSet(){
RequestMappingHandlerMapping mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
Map<RequestMappingInfo, HandlerMethod> map = mapping.getHandlerMethods();
map.keySet().forEach(mappingInfo -> {
HandlerMethod handlerMethod = map.get(mappingInfo);
ResourceAuthPer method = AnnotationUtils.findAnnotation(handlerMethod.getMethod(), ResourceAuthPer.class);
Optional.ofNullable(method)
.ifPresent(resourcePermission -> mappingInfo
.getPatternsCondition()
.getPatterns()
.forEach(url -> {
String strUrl = URLConvertUtil.capture(url);
String permission = URLConvertUtil.convert(url);
if(ArrayUtil.isNotEmpty(method.exampleId())){
for (String exampleId : method.exampleId()){
permissionEntities.add(PermissionEntityVO
.builder()
.sName(method.value())
.sApplyExampleID(Arrays.asList(exampleId))
.sEnglishName(permission + "_" + exampleId)
.sServiceName(applicationName)
.sUrl(strUrl)
.build());
}
}
}));
});
}
}
/**
* @author czx
* @title: NacosStartListener
* @projectName zhjg
* @description: TODO
* @date 2020/12/2217:52
*/
@Slf4j
@Component
public class NacosStartListener implements ApplicationListener<ApplicationReadyEvent> {
@Autowired
private GatewayControllerEndpoint endpoint;
@Value("${spring.cloud.nacos.discovery.server-addr}")
private String nacosAddress;
@Autowired
private SyncPermissionController controller;
@Autowired
private ServiceNameData serviceNameData;
public HashMap<String,Long> serviceStartTime = new HashMap<>();
// 排除seata服务
public String SAETASERVER = "seata-server";
@SneakyThrows
@Override
public void onApplicationEvent(ApplicationReadyEvent applicationReadyEvent) {
NamingService namingService = NacosFactory.createNamingService(nacosAddress);
endpoint.routes().subscribe(stringObjectMap -> {
String uri = (String) stringObjectMap.get("uri");
String service = uri.substring(5);
if(!service.equals(SAETASERVER)){
try {
log.info("开始监听服务[{}]...",service);
namingService.subscribe(service, (event) ->{
if (event instanceof NamingEvent){
NamingEvent namingEvent = (NamingEvent) event;
List<Instance> list = namingEvent.getInstances();
String serviceName = namingEvent.getServiceName().substring(15);
if(list != null && list.size() != 0){
Long time = null;
if(serviceStartTime != null && serviceStartTime.get(serviceName) != null){
time = MapUtil.getLong(serviceStartTime, serviceName);
}
if(time == null || System.currentTimeMillis() - time > 10000 ){
log.info("服务[{}]状态:上线",serviceName);
controller.syncService(serviceName);
serviceStartTime.put(serviceName,System.currentTimeMillis());
serviceNameData.setServiceList(serviceStartTime);
}
}
}
});
} catch (NacosException e) {
e.printStackTrace();
}
}
});
}
}
@Slf4j
@RestController
@RequestMapping("/sync-permission")
public class SyncPermissionController {
@Value("${spring.application.name}")
private String applicationName;
@Value("${sync.permission:false}")
public boolean permission;
@Autowired
private RestTemplate restTemplate;
@Autowired
private DiscoveryClient client;
public ConcurrentHashMap<String,Integer> status = new ConcurrentHashMap();
@RequestMapping(value = "/syncService")
public void syncService(String serviceId){
if(permission){
this.sync(serviceId);
}else {
log.info("服务[{}]自动同步权限未打开!",serviceId);
}
}
private synchronized String sync(String serviceId){
int temp = 1;
if(!serviceId.equals(applicationName)){
String url = "http://" + serviceId;
try{
log.info("服务[{}]正在同步权限...",serviceId);
if(status.size() > 0 && status.get(serviceId) != null){
temp = status.get(serviceId);
temp ++;
}
// 调用服务保存到数据库,如果存在就更新,不存在就保存
ResponseEntity<String> body = restTemplate.getForEntity(url + "/security/permission/sync", String.class);
if(body.getStatusCodeValue() == 200){
status.put(serviceId,1);
log.info("服务[{}]权限同步成功!",serviceId);
return "权限同步成功";
}else {
log.error("服务[{}]权限同步错误:{}",serviceId,body.getBody());
return "权限同步错误";
}
}catch (Exception e){
log.error("服务[{}]权限同步失败,可能没有启动!",serviceId);
if(temp > 10){
status.put(serviceId,1);
return "权限同步失败";
}else {
try {
status.put(serviceId,temp);
log.error("服务[{}]休息{}s后,第{}次重试.....",serviceId,temp,temp);
Thread.sleep(temp * 1000);
this.sync(serviceId);
} catch (InterruptedException ex) {
ex.printStackTrace();
}
}
}
}
return "权限同步错误";
}
}
到此,接口权限就保存到数据库中了。
@Slf4j
@Component
public class RemoteUserDetailsService implements UserDetailsService {
@Autowired
private RemoteAdminService remoteAdminService;
@Autowired
private PermissionRemoteService permissionRemoteService;
@Override
public UserDetails loadUserByUsername(String userName) throws UsernameNotFoundException {
BdUserDto bdUserDto = remoteAdminService.selectUserByUserName(userName);
// 校验
if (ObjectUtil.isNull(bdUserDto) || StrUtil.isEmpty(bdUserDto.getSUserLoginID())) { // 用户不存在
log.info("登录用户:{} 不存在.", userName);
throw new UsernameNotFoundException("登录用户:" + userName + " 不存在");
}
return getDetail(bdUserDto);
}
private UserDetails getDetail(BdUserDto bdUserDto){
Set<String> permissions = new HashSet<>();
R result = permissionRemoteService.getPermissionByUserId(bdUserDto.getSUserLoginID());
ArrayList data = MapUtil.get(result, "data", ArrayList.class);
if(CollUtil.isNotEmpty(data)){
permissions.addAll(data);
}
String[] roles = new String[0];
if(CollUtil.isNotEmpty(permissions)){
roles = permissions.stream().map(role -> "ROLE_" + role).toArray(String[]::new);
}
Collection<? extends GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(roles);
CustomUserDetailsUser customUserDetailsUser = new CustomUserDetailsUser(bdUserDto,bdUserDto.getSUserName(),bdUserDto.getSPassword(),authorities);
return customUserDetailsUser;
}
}
给用户配置权限的CURD代码就不贴了。
@Slf4j
@Configuration
@EnableWebSecurity
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
@Autowired
private AuthIgnoreConfig authIgnoreConfig;
@SneakyThrows
@Override
protected void configure(HttpSecurity http) {
List<String> permitAll = authIgnoreConfig.getIgnoreUrls();
String[] urls = permitAll.stream().distinct().toArray(String[]::new);
ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry registry = http.authorizeRequests();
registry
.antMatchers(urls)
.permitAll()
.anyRequest()
.authenticated()
.withObjectPostProcessor(new ObjectPostProcessor<FilterSecurityInterceptor>() {
@Override
public <O extends FilterSecurityInterceptor> O postProcess(O o) {
//设置自定义访问决策管理器
o.setAccessDecisionManager(accessDecisionManager());
//设置自定义的权限数据源
o.setSecurityMetadataSource(filterInvocationSecurityMetadataSource());
return o;
}
})
.and().csrf().disable();
}
@Bean
public AccessDecisionManager accessDecisionManager(){
return new CustomAccessDecisionManager();
}
@Bean
public FilterInvocationSecurityMetadataSource filterInvocationSecurityMetadataSource(){
return new CustomFilterInvocationSecurityMetadataSource();
}
}
public class CustomAccessDecisionManager implements AccessDecisionManager {
@Override
public void decide(Authentication authentication, Object o, Collection<ConfigAttribute> collection) throws AccessDeniedException{
// 如果这个url 没有配置权限 直接放行
if(CollUtil.isEmpty(collection)) {
return;
}
ConfigAttribute c;
String needRole;
for(Iterator<ConfigAttribute> iter = collection.iterator(); iter.hasNext(); ) {
c = iter.next();
needRole = c.getAttribute();
for(GrantedAuthority ga : authentication.getAuthorities()) {
if(needRole.trim().equals(ga.getAuthority())) {
return;
}
}
}
throw new AccessDeniedException("没有权限访问");
}
@Override
public boolean supports(ConfigAttribute configAttribute) {
return true;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
public class CustomFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {
private final AntPathMatcher antPathMatcher = new AntPathMatcher();
private final String ROLE = "ROLE_";
@Autowired
private PermissionConfig permissionConfig;
@Autowired
private AuthIgnoreConfig authIgnoreConfig;
@Override
public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
FilterInvocation fi = (FilterInvocation) object;
String url = fi.getRequestUrl();
List<String> ignoreUrls = authIgnoreConfig.getIgnoreUrls();
if(CollUtil.isNotEmpty(ignoreUrls) && ignoreUrls.contains(url)){
// 如果是忽略认证的直接放行
return null;
}
List<PermissionEntityVO> permissionEntities = permissionConfig.getPermissionEntities();
for (PermissionEntityVO vo : permissionEntities){
if(antPathMatcher.match(vo.getSUrl(),url)){
return SecurityConfig.createList(ROLE + vo.getSEnglishName());
}
}
//如果这个url 没有配置权限 直接返回空
return null;
}
@Override
public Collection<ConfigAttribute> getAllConfigAttributes() {
return null;
}
@Override
public boolean supports(Class<?> aClass) {
return false;
}
}
到此就实现了用户接口权限验证。
1、服务启动 -> 收集URL权限 -> 保存或更新URL权限
2、管理员给用户配置已保存的权限
3、用户登录成功 -> 读取所配置的权限 -> 设置到security的user中
4、用户访问接口 -> security框架会在访问决策管理器AccessDecisionManager验证当前user中有没有对应的url权限