zoukankan      html  css  js  c++  java
  • Spring Security + OAuth系统环境搭建(一)

      最近在做权限管理系统的重构工作,系统基于Spring Security + OAuth架构,整体架构、技术和之前调研的结果差不多,架构调研时有在这篇博客做过简单记录“Spring Cloud微服务下的权限架构调研”,博客未更新,和定稿的架构稍有出入,图画的也有点问题。

      前面比较忙,搭建过程没有记录,平时也没有记笔记的习惯,有点惨,写到哪算哪吧~~~

    一、添加pom依赖

      项目是在Spring Cloud基础上改造的,主要记录接入Spring Security + OAuth所做的工作。这里添加security和oauth的相关依赖:

    <!-- security -->
    <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
    </dependency>
    <!-- oauth2 -->
    <dependency>
             <groupId>org.springframework.cloud</groupId>
             <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    二、配置认证服务器

      Auth作为认证服务器:

      1、新建一个认证服务器配置类,代码如下:

    @Configuration
    @EnableAuthorizationServer
    public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
        @Autowired
        private AuthenticationManager authenticationManager;
        @Autowired
        private UserDetailsService userDetailsService;
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
        @Autowired
        private DataSource dataSource;
    
        /**
         * 注册redisTokenStore,用于配置redis作为token仓库
         *
         * @return
         */
        @Bean
        public RedisTokenStore tokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
    
        /**
         * 用来配置授权(authorization)以及令牌(token)的访问端点和令牌服务(token services)。
         *
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.tokenStore(tokenStore())
                    .authenticationManager(authenticationManager)
                    .userDetailsService(userDetailsService);
            endpoints.tokenServices(defaultTokenServices())
                    // 重复使用refresh_token ,指导refresh_token过期,才会产生新的refresh_token
                    .reuseRefreshTokens(true);
    
        }
    
        /**
         * 配置使用客户端详情服务
         *
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
            clients.withClientDetails(clientDetailsService());
    
        }
    
        /**
         * 用来配置令牌端点
         *
         * @param security
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            security.tokenKeyAccess("permitAll()");
            security.checkTokenAccess("isAuthenticated()");
            security.allowFormAuthenticationForClients();
        }
    
        /**
         * 注册JdbcClientDetailsService,从数据库获取客户端详情
         */
        @Bean
        public ClientDetailsService clientDetailsService() {
            return new JdbcClientDetailsService(dataSource);
        }
    
        /**
         * 配置token服务(仓库、客户端详情服务、是否支持refresh_token以及access_token、refresh_token的有效时间)
         */
        @Bean
        @Primary
        public DefaultTokenServices defaultTokenServices() {
            DefaultTokenServices tokenServices = new DefaultTokenServices();
            tokenServices.setTokenStore(tokenStore());
            tokenServices.setSupportRefreshToken(true);
            tokenServices.setClientDetailsService(clientDetailsService());
            tokenServices.setAccessTokenValiditySeconds(60 * 60 * 24); // access_token有效期,默认12小时,-1表示永不过期
            tokenServices.setRefreshTokenValiditySeconds(60 * 60 * 24 * 7);//refresh_token有效期,默认30天
            return tokenServices;
        }
    }

      这里有个东西要注意,在http请求进来之后,首先框架自己会先校验token的正确性。系统oauth采用的是密码模式,熟悉oauth的应该知道,在请求头中必须要带上client_id和client_secret,client_id和client_secret会base64编码后放到请求头的Authorization字段中。这些信息可以在内存、数据库里面维护,这里用的是数据库,就是在clientDetailsService()方法中配置的内容,点开JdbcClientDetailsService的源码可以看到,oauth自己维护了一个oauth_client_details表,所以我们还需要在数据库创建相应的表以及数据。除了oauth_client_details还有维护token相关的表等等,这里没用上。

    public JdbcClientDetailsService(DataSource dataSource) {
            this.updateClientDetailsSql = DEFAULT_UPDATE_STATEMENT;
            this.updateClientSecretSql = "update oauth_client_details set client_secret = ? where client_id = ?";
            this.insertClientDetailsSql = "insert into oauth_client_details (client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove, client_id) values (?,?,?,?,?,?,?,?,?,?,?)";
            this.selectClientDetailsSql = "select client_id, client_secret, resource_ids, scope, authorized_grant_types, web_server_redirect_uri, authorities, access_token_validity, refresh_token_validity, additional_information, autoapprove from oauth_client_details where client_id = ?";
            this.passwordEncoder = NoOpPasswordEncoder.getInstance();
            Assert.notNull(dataSource, "DataSource required");
            this.jdbcTemplate = new JdbcTemplate(dataSource);
            this.listFactory = new DefaultJdbcListFactory(new NamedParameterJdbcTemplate(this.jdbcTemplate));
        }
    JdbcClientDetailsService
    CREATE TABLE `oauth_client_details` (
      `client_id` varchar(48) NOT NULL,
      `resource_ids` varchar(256) DEFAULT NULL,
      `client_secret` varchar(256) DEFAULT NULL,
      `scope` varchar(256) DEFAULT NULL,
      `authorized_grant_types` varchar(256) DEFAULT NULL,
      `web_server_redirect_uri` varchar(256) DEFAULT NULL,
      `authorities` varchar(256) DEFAULT NULL,
      `access_token_validity` int(11) DEFAULT NULL,
      `refresh_token_validity` int(11) DEFAULT NULL,
      `additional_information` varchar(4096) DEFAULT NULL,
      `autoapprove` varchar(256) DEFAULT NULL,
      PRIMARY KEY (`client_id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    oauth_client_details

      2、web配置类

    @Configuration    
    @EnableWebSecurity    // 禁用Spring Boot默认的Security配置,配合@Configuration启用自定义配置
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
        @Autowired
        private UserDetailsService userDetailsService;
        @Autowired
        private MyAccessDecisionManager myAccessDecisionManager;
    
        /*
         * 加密工具
         */
        @Bean
        public PasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /*
         * 认证管理器
         */
        @Override
        @Bean
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
        /*
         * 身份验证配置,用于注入自定义身份验证Bean和密码校验规则
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.userDetailsService(userDetailsService)
                    .passwordEncoder(passwordEncoder());
        }
    
        /**
         * 无需权限校验直接放行的路径
         */
        private final String[] PATH_PASS = {
                // 根据实际情况添加
        };
    
        /**
         * Request层面的配置,对应XML Configuration中的<http>元素
         */
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .authorizeRequests()
                    .antMatchers(PATH_PASS).permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .and()
                    .csrf().disable()
                    .httpBasic();
            // 将自定义的过滤器配置在FilterSecurityInterceptor之前
            http.addFilterBefore(myFilterSecurityInterceptor(), FilterSecurityInterceptor.class);
        }
    
        /**
         * Web层面的配置,一般用来配置无需权限校验的路径,也可以在HttpSecurity中配置,但是在web.ignoring()中配置效率更高。
         * web.ignoring()是一个忽略的过滤器,而HttpSecurity中定义了一个过滤器链,即使permitAll()放行还是会走所有的过滤器,
         * 直到最后一个过滤器FilterSecurityInterceptor认定是可以放行的,才能访问。
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/favor.ioc");
        }
    
        /**
         * 自定义的权限过滤器,后面再写
         */
        @Bean
        public MyFilterSecurityInterceptor myFilterSecurityInterceptor() {
            MyFilterSecurityInterceptor myFilterSecurityInterceptor = new MyFilterSecurityInterceptor();
            myFilterSecurityInterceptor.setMyAccessDecisionManager(myAccessDecisionManager);
            return myFilterSecurityInterceptor;
        }
    }

       3、上面是一些基本配置,后面还需要自定义身份认证,实现UserDetailsService中的loadUserByUsername(String username)方法,在里面实现自己的身份校验逻辑。

    @Service
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            /* 
             * 自定义身份校验逻辑,以及根据具体业务做一些其他的事情,比如在redis维护详细的用户信息等等。
             * 校验完毕之后将用户权限信息放到grantedAuthorities,最后根据具体校验结果生成User对象并返回。
             */
            Set<GrantedAuthority> grantedAuthorities = new HashSet<>();
            boolean enabled = true; // 可用性 :true:可用 false:不可用
            boolean accountNonExpired = true; // 过期性 :true:没过期 false:过期
            boolean credentialsNonExpired = true; // 有效性 :true:凭证有效 false:凭证无效
            boolean accountNonLocked = true; // 锁定性 :true:未锁定 false:已锁定
            User user = new User(username, password,
                    enabled, accountNonExpired, credentialsNonExpired, accountNonLocked, grantedAuthorities);
            return user;
        }
    }

       4、其实到这里基本的框架都已经差不多了,但是因为我们的Auth还作为资源服务器,还需要做对资源服务器的配置。这一块的配置就简单很多。

    @Configuration
    @EnableResourceServer
    public class ResourceServerConfig extends ResourceServerConfigurerAdapter{
    
        /**
         * 无需权限校验直接放行的路径
         */
        private final String[] PATH_PASS = {
                // 根据实际情况添加
        };
        
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.formLogin()
                    .loginPage("/login")
                    .and()
                    .csrf().disable()
                    .authorizeRequests()
                    .antMatchers(AUTH_WHITELIST).permitAll()
                    .anyRequest().authenticated()
                    .and()
                    .httpBasic();
    
        }
    }

      到这里,Auth的环境搭建基本上就算完成了。

    三、配置资源服务器

      在我们的架构中,没有做服务间的权限控制,对外的鉴权工作统一放在网关中处理,所以网关也作为资源服务器。

    @Configuration
    @EnableResourceServer
    @EnableWebSecurity
    public class WebSecurityConfig extends ResourceServerConfigurerAdapter {
        @Autowired
        private RedisConnectionFactory redisConnectionFactory;
        @Autowired
        private MyAccessDecisionManager myAccessDecisionManager;
        
        /**
         * 无需权限校验直接放行的路径
         */
        private final String[] PATH_PASS = {
                // 根据实际情况添加
        };
    
        /**
         * Request层面的配置,对应XML Configuration中的<http>元素
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests()
                    .antMatchers(AUTH_WHITELIST).permitAll()
                    .and()
                    .csrf().disable().exceptionHandling()
            
            // 将自定义的过滤器配置在FilterSecurityInterceptor之前
            http.addFilterBefore(myFilterSecurityInterceptor(), FilterSecurityInterceptor.class);
        }
    
        /**
         * 资源服务Web层面的配置,这里主要配置了一些鉴权相关的入口,例如异常处理
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            OAuth2WebSecurityExpressionHandler expressionHandler = new OAuth2WebSecurityExpressionHandler();
            expressionHandler.setApplicationContext(applicationContext);
            return expressionHandler;
        }
    
        /**
         * 注册redisTokenStore,用于配置redis作为token仓库
         *
         * @return
         */
        @Bean
        public TokenStore tokenStore() {
            return new RedisTokenStore(redisConnectionFactory);
        }
    
        /**
         * 自定义的权限过滤器,后面再写
         */
        @Bean
        public MyFilterSecurityInterceptor myFilterSecurityInterceptor() {
            MyFilterSecurityInterceptor myFilterSecurityInterceptor = new MyFilterSecurityInterceptor();
            myFilterSecurityInterceptor.setMyAccessDecisionManager(myAccessDecisionManager);
            return myFilterSecurityInterceptor;
        }
    
    }

      资源服务器的配置相对还是比较少的,因为需要在网关做鉴权,所以这里也配置了自定义过滤器。另外,还继承ZuulFilter类实现了过滤器,关于过滤器这块后面再统一写。

      到这呢,大体的环境搭建基本已经OK了,基本的功能能够使用了,后面的东西会稍微细一点,慢慢整理。

  • 相关阅读:
    Golang手动分页,按等份拆分数据
    GORM无法映射到结构体上
    VSCODE GOLANG运行多个服务
    解决,MAVEN
    Properties配置文件常见错误写法以及转义字符说明
    Pentaho Data Integration (PDI/Kettle)与Java版本支持关系
    MYSQL之读写分离搭建方案
    Windows下创建软件快速启动命令
    Sonar的一些使用总结
    使用SVG Path绘图
  • 原文地址:https://www.cnblogs.com/Mr-XiaoLiu/p/10051362.html
Copyright © 2011-2022 走看看