spring cloud oauth2 单点登录及资源权限控制简单例子

oauth2 分为认证中心和资源服务中心,做个简单例子

一.认证中心 新增认证服务模块项目

 1.引入pom

  
            org.springframework.cloud
            spring-cloud-starter-oauth2
            2.0.1.RELEASE
        

2. 配置EnableAuthorizationServer

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter  {
    @Autowired
    AuthenticationManager authenticationManager;


    private static final String DEMO_RESOURCE_ID = "some_res";


    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // password 持多种编码,通过密码的前缀区分编码方式
        String finalSecret = "{bcrypt}" + new BCryptPasswordEncoder().encode("123456");
        //配置两个客户端,一个用于password认证一个用于client认证

        //client模式,没有用户的概念,直接与认证服务器交互,用配置中的客户端信息去申请accessToken,
        // 客户端有自己的client_id,client_secret对应于用户的username,password,而客户端也拥有自己的authorities,
        // 当采取client模式认证时,对应的权限也就是客户端自己的authorities
        clients.inMemory().withClient("client_1")
                .resourceIds(DEMO_RESOURCE_ID)
                .redirectUris("http://localhost:7589/login")
                //.authorizedGrantTypes("authorization_code","password", "refresh_token")
                //password模式,自己本身有一套用户体系,在认证时需要带上自己的用户名和密码,以及客户端的client_id,client_secret
                // 此时,accessToken所包含的权限是用户本身的权限,而不是客户端的权限
                .authorizedGrantTypes("authorization_code","password")
                .accessTokenValiditySeconds(3600) // 设置 token 过期时间
                .scopes("all")
                .autoApprove(true)
                .secret(finalSecret)


                .and().withClient("client_2")
                .resourceIds(DEMO_RESOURCE_ID)
                .redirectUris("http://localhost:7589/login")
                //.authorizedGrantTypes("authorization_code","password", "refresh_token")
                .authorizedGrantTypes("authorization_code", "implicit","refresh_token")
                .accessTokenValiditySeconds(3600) // 设置 token 过期时间
                .scopes("all")
                .autoApprove(false)
                .secret(finalSecret)
                ;
    }

    @Bean
    public TokenStore tokenStore() {
//        return new JdbcTokenStore(dataSource);
        return new JwtTokenStore(accessTokenConverter());
    }

    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("123");
        return converter;
    }


    @Override
    public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception {

        oauthServer
                // 允许客户表单认证,不加的话/oauth/token无法访问
                .allowFormAuthenticationForClients()
                // 对于CheckEndpoint控制器[框架自带的校验]的/oauth/token端点允许所有客户端发送器请求而不会被Spring-security拦截
                // 开启/oauth/token_key验证端口无权限访问
                .tokenKeyAccess("permitAll()")
                // 要访问/oauth/check_token必须设置为permitAll(),但这样所有人都可以访问了,设为isAuthenticated()又导致访问不了,这个问题暂时没找到解决方案
                // 开启/oauth/check_token验证端口认证权限访问
                .checkTokenAccess("permitAll()");
    }

    //定义授权和令牌端点以及令牌服务
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // redisTokenStore
//        endpoints.tokenStore(new RedisTokenStore(redisConnectionFactory))
//                .authenticationManager(authenticationManager)
//                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);

        endpoints
                //指定token存储位置
                .tokenStore(tokenStore())
                // 配置JwtAccessToken转换器
                .tokenEnhancer(accessTokenConverter())
                //指定认证管理器
                .authenticationManager(authenticationManager)
                .allowedTokenEndpointRequestMethods(HttpMethod.GET, HttpMethod.POST);
        ;
        // 配置tokenServices参数
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        tokenServices.setTokenStore(endpoints.getTokenStore());
        // 是否支持刷新
        tokenServices.setSupportRefreshToken(true);
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        // 20分钟
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.MINUTES.toSeconds(20));
        endpoints.tokenServices(tokenServices);
    }


}

3.配置用户认证权限

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class AuthWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private SsoUserDetailsService ssoUserDetailsService;

    /**
     * password 支持多种编码,通过密码的前缀区分编码方式,推荐
     */
    @Bean
    PasswordEncoder passwordEncoder() {
        return PasswordEncoderFactories.createDelegatingPasswordEncoder();
    }

    /**
     * 这一步的配置是必不可少的,否则SpringBoot会自动配置一个AuthenticationManager,覆盖掉内存中的用户
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        AuthenticationManager manager = super.authenticationManagerBean();
        return manager;
    }

    @Autowired
    public void config(AuthenticationManagerBuilder auth) throws Exception {
        //设置UserDetailsService以及密码规则
        auth.userDetailsService(ssoUserDetailsService).passwordEncoder(passwordEncoder());
    }


}
@Component
public class SsoUserDetailsService implements UserDetailsService {
    @Autowired
    private PasswordEncoder passwordEncoder;
    @Override
    public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {

        if("qzq".equals(s)){

            return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL,ROLE_ADMIN"));
        }else{
            return new User( s, passwordEncoder.encode("123456"), AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_NORMAL"));
        }
    }
}

4.最后配置yml  本地测试的话 context-path一定要配置

server:
  port: 7585
  servlet:
    #这里一定要配置,否则本地IP访问授权会不成
    context-path: /sso


spring:
  application:
    name: cloud-auth-service


logging:
  level:
    root: debug



 二,接下来新增客户端,这里客户端包括了资源权限认证

1.同样引入pom

2.配置WebSecurityConfigurerAdapter

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableOAuth2Sso
public class AuthWebSecurityConfigurerAdapter extends WebSecurityConfigurerAdapter {
    @Autowired
    private TokenStore tokenStore;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .antMatcher("/**").authorizeRequests()
                .anyRequest().authenticated()
                .withObjectPostProcessor(new ObjectPostProcessor() {

                    @Override
                    public  O postProcess(O fsi) {
                        // 权限获取自定义配置
                        fsi.setSecurityMetadataSource(new PermissionFilterInvocationSecurityMetadataSource());
                        return fsi;
                    }
                })
                .accessDecisionManager(accessDecisionManager());


    }

    private AccessDecisionManager accessDecisionManager() {

        WebExpressionVoter webExpressionVoter = new WebExpressionVoter();
        webExpressionVoter.setExpressionHandler(new OAuth2WebSecurityExpressionHandler());
        // 授权逻辑自定义配置
        return new AffirmativeBased(Lists.newArrayList(new PermissionsVoter(), new RoleVoter(),
                new AuthenticatedVoter(), webExpressionVoter));
    }

    @Primary
    @Bean
    public DefaultTokenServices tokenServices() {

        DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
        defaultTokenServices.setTokenStore(tokenStore);
        return defaultTokenServices;
    }
}

2.配置动态资源权限

public class PermissionFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    private static final Map> urlMap = new HashMap<>();
    static {
        //测试用 可以从数据库获取
        AntPathRequestMatcher url = new AntPathRequestMatcher("/api/testResourcePower");
        urlMap.put(url,Lists.newArrayList(new SecurityConfig("ROLE_ADMIN")));
    }

    public PermissionFilterInvocationSecurityMetadataSource() {
    }

    /**
     * 转换权限列表
     */
    private Map> requestMatcherCollectionMap() {
        return urlMap;
    }


    @Override
    public Collection getAttributes(Object object) throws IllegalArgumentException {

        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        for (Map.Entry> entry : requestMatcherCollectionMap().entrySet()) {
            if (entry.getKey().matches(request)) {
                return entry.getValue();
            }
        }
        return null;
    }

    @Override
    public Collection getAllConfigAttributes() {
        return requestMatcherCollectionMap().values()
                .stream().flatMap(Collection::stream)
                .collect(Collectors.toList());
    }

    @Override
    public boolean supports(Class clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }
}
@Slf4j
public class PermissionsVoter implements AccessDecisionVoter {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return Objects.nonNull(attribute.getAttribute());
    }

    @Override
    public boolean supports(Class clazz) {
        return true;
    }

    @Override
    public int vote(Authentication authentication, Object object, Collection attributes) {

        if (CollectionUtils.isEmpty(attributes)) {
            return ACCESS_DENIED;
        }
        // 用户授权的权限
        Collection grantedAuthorities;
        if (Objects.isNull(authentication)
                || CollectionUtils.isEmpty(grantedAuthorities = extractAuthorities(authentication))
                || Objects.isNull(object)) {

            log.info("user no authentication!");
            return ACCESS_DENIED;
        }
        for (GrantedAuthority grantedAuthority : grantedAuthorities) {

            String authority;
            if (StringUtils.isNotBlank(authority = grantedAuthority.getAuthority())
                    && match(authority, attributes)) {
                return ACCESS_GRANTED;
            }
        }
        return ACCESS_DENIED;
    }

    private boolean match(String authority, Collection attributes) {

        for (ConfigAttribute configAttribute : attributes) {
            String attribute;
            if (StringUtils.isNotBlank(attribute = configAttribute.getAttribute())
                    && attribute.equals(authority)) {
                return true;
            }
        }
        return false;
    }

    /**
     * 获取用户权限列表
     */
    Collection extractAuthorities(Authentication authentication) {
        return authentication.getAuthorities();
    }
}

3.yml配置

server:
  port: 7589
spring:
  application:
    name: cloud-auth-client
  main:
    allow-bean-definition-overriding: true
auth-server: http://localhost:7585/sso
security:
  oauth2:
    client:
      client-id: client_2
      client-secret: 123456
      user-authorization-uri: ${auth-server}/oauth/authorize
      access-token-uri: ${auth-server}/oauth/token
    resource:
      jwt:
        key-uri: ${auth-server}/oauth/token_key
      scope: all


logging:
  level:
    root: debug

 4.写controller测试  这里路径在PermissionFilterInvocationSecurityMetadataSource动态设置权限了

   //http://localhost:7589/api/testResourcePower
    @GetMapping("/testResourcePower")
    public String testResourcePower() {
        //for debug
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        log.info("authentication: " + authentication.getAuthorities().toString());
        return "testResourcePower";
    }

5.访问路径http://localhost:7589/api/testResourcePower成功跳转到认证中心登录页面,输入用户、密码成功访问接口

 

源码传送门 https://github.com/fdqzq613/myframe.git

 

 

 

 

 

 

 

 

你可能感兴趣的:(分布式)