当前位置:   article > 正文

基于spring cloud alibaba中使用security 自定义接口权限,支持动态赋权_spring cloud alibaba security

spring cloud alibaba security

security中自带权限验证注解@PreAuthorize

@RestController
public class HelloController {

  	@RequestMapping(value = "/user",method = RequestMethod.POST)
    @PreAuthorize("hasRole('admin')")
    public String user() {
        return "admin";
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 这个注解相信大家都用过,这句意思就是当请求用户必须有admin权限才可以请求这个接口。

需求

当需求是需要动态管理API接口,比如A用户 只开放ABCD四个接口,B用户只开放EFGH四个接口。
或者是当用户A刚注册只有A接口功能,用户A开通会员就有B接口功能。

实现思路

1、自定义一个接口权限注解,参数包括接口地址、服务ID 。方便管理接口是哪个服务的
2、服务一启动时,自动搜集所有使用这个注解的接口,然后保存到数据库。这样权限的字典表就有了
3、管理员给用户配置接口权限。如用户A 配置A、B、C、D四个接口的权限

具体代码实现,第一部:分收集权限保存到数据库

1、自定义注解@ResourceAuthPer
/**
 * @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 {};
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
2、在controller中使用
@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);
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
3、当服务启动时,收集接口配置的权限。

/**
 * @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());
                                    }
                                }
                            }));
        });
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
4、在网关中监听服务启动后,把收集的接口权限保存到数据库。

/**
 * @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();
                }
            }
        });
    }
}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
5、把收集的接口权限保存到数据库。
@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 "权限同步错误";
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60
  • 61
  • 62
  • 63
  • 64
  • 65
  • 66
  • 67
  • 68
  • 69
  • 70

到此,接口权限就保存到数据库中了。

具体代码实现,第二部分:当用户登录成功后,把配置的权限给security框架管理。

1、实现security的UserDetailsService接口,在登录成功后查询当前用户配置的权限列表,然后给设置到security的User中
@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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39

给用户配置权限的CURD代码就不贴了。

具体代码实现,第三部分:当用户请求的时候验证权限

1、在WebSecurityConfigurer中配置自定义访问决策管理器和权限数据源。
@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();
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
2、自定义访问决策管理器CustomAccessDecisionManager,目的是验证当前用户配置的权限中有没有当前请求接口的权限,有就放行,没有就返回。
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;
    }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
2、自定义权限数据源CustomFilterInvocationSecurityMetadataSource,目的是把当前服务所有的接口收集给定义访问决策管理器CustomAccessDecisionManager中使用。
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
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42

到此就实现了用户接口权限验证。

最后完整流程

1、服务启动 -> 收集URL权限 -> 保存或更新URL权限
2、管理员给用户配置已保存的权限
3、用户登录成功 -> 读取所配置的权限 -> 设置到security的user中
4、用户访问接口 -> security框架会在访问决策管理器AccessDecisionManager验证当前user中有没有对应的url权限

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/weixin_40725706/article/detail/68870
推荐阅读
相关标签
  

闽ICP备14008679号