美文网首页springbootSpring Cloud
OAuth2 协议与微服务安全

OAuth2 协议与微服务安全

作者: 乌鲁木齐001号程序员 | 来源:发表于2020-07-13 17:55 被阅读0次

    OAuth2 角色 & 流程

    OAuth2 角色 & 流程.png
    角色
    • 认证服务器:客户端说它是张三,认证服务器就要认证到底是不是张三;
    • 客户端服务器(可以是 Postman):拿着令牌去访问资源服务器(微服务);
    • 资源服务器(微服务):拿着令牌去认证服务器认证;
    流程
    • 客户端应用带着:客户端应用本身的信息,访问客户端应用的用户的信息,向认证服务器申请 Token;
    • 认证服务器验证客户端应用携带的信息的正确性,如果通过验证,发给客户端应用一个 Token,然后在本地维护一个 Token 和用户信息的映射;
    • 客户端应用带着 Token 去访问资源服务器,资源服务器拿着 Token 去认证服务器认证,认证成功的话,可以知道这个 Token 代表的用户名,然后继续向下走业务逻辑;

    Spring Security 中 OAuth2 的相关类

    org.springframework.security.authentication.AuthenticationManager
    • 用来告诉认证服务器,合法的用户有哪些;
    org.springframework.security.core.Authentication
    • 抽象出不同的认证方式的公共概念;

    认证服务器 | 配置步骤

    大致思路

    通过配置,要让认证服务器知道:
    1) 哪些客户端应用会访问我
    2) 哪些用户是合法用户
    3) 谁能找我验证这个令牌

    pom 依赖
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-oauth2</artifactId>
    </dependency>
    
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>${spring-cloud.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    
    认证服务器配置
    package com.lixinlei.security.auth.oauth2.config;
    
    import javax.sql.DataSource;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
    import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
    import org.springframework.security.oauth2.provider.token.TokenStore;
    import org.springframework.security.oauth2.provider.token.store.JdbcTokenStore;
    //import org.springframework.session.jdbc.config.annotation.web.http.EnableJdbcHttpSession;
    
    /**
     * 对认证服务器的配置要包括:
     * 1.哪些客户端应用会访问我
     * 2.哪些用户是合法用户
     * 3.谁能找我验证这个令牌
     */
    //@EnableJdbcHttpSession
    @Configuration
    @EnableAuthorizationServer // 当前应用是作为认证授权服务器存在的
    public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
    
        /**
         * 用来告诉认证服务器,合法的用户有哪些
         * 在 {@link OAuth2WebSecurityConfig#authenticationManagerBean()} 中暴露出来的 Bean
         */
        @Autowired
        private AuthenticationManager authenticationManager;
    
        /**
         * 在 {@link OAuth2WebSecurityConfig#passwordEncoder()} 中暴露出来的 Bean
         */
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        /**
         * 自己的实现,合法用户的来源:{@link UserDetailsServiceImpl}
         */
        @Autowired
        private UserDetailsService userDetailsService;
    
    //    @Autowired
    //    private DataSource dataSource;
    
    //    @Bean
    //    public TokenStore tokenStore() {
    //        return new JdbcTokenStore(dataSource);
    //    }
    
        /**
         * 1. 哪些客户端应用会访问我(认证服务)
         * @param clients
         * @throws Exception
         */
        @Override
        public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    //        clients.jdbc(dataSource);
            clients.inMemory()
                    .withClient("orderApp") // 可以让哪个客户端应用访问我这个认证服服务
                    .secret(passwordEncoder.encode("123456")) // 客户端应用的密码
                    .scopes("read", "write") // 将应用 orderApp 与它的权限挂钩,相当于 ACL,意思是 orderApp 拿着这个 Token 访问 order-server 能干什么
                    .accessTokenValiditySeconds(3600) // 发出去的 Token 的有效期,单位:s
                    .resourceIds("order-server") // 发出去的 Token 可以访问哪些资源服务器
                    .authorizedGrantTypes("password") // OAuth2 有四种授权方式:password
    
                    .and()
    
                    // 配置一个资源服务器
                    .withClient("orderService")
                    .secret(passwordEncoder.encode("123456"))
                    .scopes("read")
                    .accessTokenValiditySeconds(3600)
                    .resourceIds("order-server")
                    .authorizedGrantTypes("password");
    
        }
    
        /**
         * 2. 哪些用户是合法用户
         * @param endpoints
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
            endpoints.authenticationManager(authenticationManager);
        }
    
    
        /**
         * 3. 谁能找我验证这个令牌
         * @param security
         * @throws Exception
         */
        @Override
        public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
            // 填了一个权限表达式,意思是来验 Token 的请求一定是通过身份认证的,就是要带着用户名密码,
            // 要么是 orderApp:123456,要么是 orderService:123456
            security.checkTokenAccess("isAuthenticated()");
        }
    
    }
    
    认证服务器配置的支撑配置
    package com.lixinlei.security.auth.oauth2.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    /**
     * 就是要配置 AuthenticationManager,让 AuthenticationManager 告诉认证服务器合法的用户有谁
     */
    @Configuration
    @EnableWebSecurity // 让安全配置生效
    public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        // 获取合法的用户信息的,需要自己实现一下这个接口,根据用户名获取用户信息
        @Autowired
        private UserDetailsService userDetailsService;
    
        // Spring Security 封装的加密工具
        @Bean
        public BCryptPasswordEncoder passwordEncoder() {
            return new BCryptPasswordEncoder();
        }
    
        /**
         * 构建 AuthenticationManager
         * 通过 userDetailsService 获取用户信息,
         * 通过 passwordEncoder() 把获取到的用户密码,和用户输入的密码比对
         * @param auth
         * @throws Exception
         */
        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth
                .userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder());
        }
    
        /**
         * 把上面配置的 AuthenticationManager 暴露成一个 Bean
         * @return
         * @throws Exception
         */
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            return super.authenticationManagerBean();
        }
    
    }
    
    自己实现的,合法用户的来源
    • 认证服务器要根据这个实现,查库,作为认证用户是否合法的依据;
    package com.lixinlei.security.auth.oauth2.config;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.core.userdetails.User;
    import org.springframework.security.core.userdetails.UserDetails;
    import org.springframework.security.core.userdetails.UserDetailsService;
    import org.springframework.security.core.userdetails.UsernameNotFoundException;
    import org.springframework.security.crypto.password.PasswordEncoder;
    import org.springframework.stereotype.Component;
    
    @Component
    public class UserDetailsServiceImpl implements UserDetailsService {
    
        @Autowired
        private PasswordEncoder passwordEncoder;
    
        /**
         * 正真的业务实现,这里的用户来源一定是要读数据库的,这里目前是硬编码了一个用户
         * @param username
         * @return UserDetails 封装了 Spring 所需要的用户信息
         * @throws UsernameNotFoundException
         */
        @Override
        public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
            return User
                    .withUsername(username)
                    .password(passwordEncoder.encode("123456")) // 模拟从数据库里拿出来的密码都是密文的
                    .authorities("ROLE_ADMIN") // 理解成角色
                    .build();
        }
    
    }
    

    认证服务器 | 发挥作用

    向认证服务器发请求
    • 认证服务器需要知道,谁,通过什么客户端应用,向我申请令牌;
    • 要通过 HTTP Basic Auth 来传,是哪个应用在申请令牌,应用的密码是什么,这些在 configure(ClientDetailsServiceConfigurer clients) 方法中配置的;
    • 通过 HTTP Request Body(Form)来传,是谁在通过客户端应用,向我申请令牌,传的 key 分别是:username,password,grant_type,scope,传的用户的合法性是通过和 UserDetailsServiceImpl 提供的用户数据比对来确定的;
      • grant_type:password,
      • scope:read write,一定是在 scopes("read", "write") 中指定的字符串中选定的;
    认证服务器的响应
    • 在 Token 的有效期内,每次向认证服务器发申请 Token 的请求,返回的 expires_in 都会减少;
    {
        "access_token": "d125244e-1472-435f-a7b4-3df6a159dde2",
        "token_type": "bearer",
        "expires_in": 3550,
        "scope": "read write"
    }
    

    资源服务器

    资源服务器要知道的四件事
    • 让服务知道,自己是 OAuth2 协议中的资源服务器,知道这个以后,其才会在自己的前面加一个过滤器,取校验令牌;
    • 让服务知道,自己是什么资源服务器,比如认证服务器中配置了,.resourceIds("order-server"),那么就需要让资源服务器知道自己就是 order-server
    • 让服务器知道,验令牌的时候,去哪验,带哪些信息;
    • 去认证服务器验完令牌之后,怎么知道,带着令牌来访问我的用户是哪个用户;

    资源服务器 | 配置

    大致思路
    • 先配置,使得微服务本身成为 OAuth2 的资源服务器;
    • 再配置,去认证服务器验证 Token 的时候的安全信息;
    • 再改造微服务本身原有的接口,使得微服务的原有接口,可以在验证 Token 之后,知道是谁在拿着 Token 访问我;
    资源服务器本身的配置
    package com.lixinlei.security.order.oauth2.config;
    
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
    import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
    import org.springframework.security.oauth2.config.annotation.web.configurers.ResourceServerSecurityConfigurer;
    
    @Configuration
    /**
     * 开启 @EnableResourceServer 之后,就是告诉 order 这个服务,它本身是作为 OAuth2 中的资源服务器;
     * 开启 @EnableResourceServer 之后,默认所有发往 order 这个服务的 HTTP 请求,
     * order 服务都会在请求的 HTTP Header 中找 Token,如果找不到 Token,就不让过,当需要设置哪些请求要 Token,哪些不要的时候,就可以覆盖方法:
     * configure(HttpSecurity http)
     */
    @EnableResourceServer
    public class OAuth2ResourceServerConfig extends ResourceServerConfigurerAdapter {
    
        /**
         * 申明我这个 order 服务就是在认证服务器中配置的 order-server,给 orderApp 客户端应用发的 Token 只能访问我;
         */
        @Override
        public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
            resources.resourceId("order-server");
        }
    
        /**
         * 除了 /haha 这个请求不需要 Token,其他的请求都需要 Token
         */
        @Override
        public void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().antMatchers("/haha").permitAll()
                    .anyRequest().authenticated();
        }
    
    }
    
    验证 Token 的时候,需要的安全信息配置
    package com.lixinlei.security.order.oauth2.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.oauth2.provider.authentication.OAuth2AuthenticationManager;
    import org.springframework.security.oauth2.provider.token.RemoteTokenServices;
    import org.springframework.security.oauth2.provider.token.ResourceServerTokenServices;
    
    @Configuration
    @EnableWebSecurity
    public class OAuth2WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
        /**
         * 让 order 服务知道自己在 OAuth2 协议中是什么服务器,
         * orderService 是在认证服务器中配置的名字;
         * @return
         */
        @Bean
        public ResourceServerTokenServices tokenServices() {
            RemoteTokenServices tokenServices = new RemoteTokenServices();
            tokenServices.setClientId("orderService");
            tokenServices.setClientSecret("123456");
            tokenServices.setCheckTokenEndpointUrl("http://localhost:9090/oauth/check_token");
            return tokenServices;
        }
    
        @Bean
        @Override
        public AuthenticationManager authenticationManagerBean() throws Exception {
            OAuth2AuthenticationManager authenticationManager = new OAuth2AuthenticationManager();
            authenticationManager.setTokenServices(tokenServices());
            return authenticationManager;
        }
    
    }
    
    改造微服务原本的接口
    • 如果想直接在参数中,用 User 对象接着,而不是 username,需要自己实现 UserDetailsService,然后注入 AccessTokenConverter 中,然后 AccessTokenConverter 注入 RemoteTokenServices 中;
    @PostMapping
    public OrderInfo create(@RequestBody OrderInfo info, @AuthenticationPrincipal String username) {
        log.info("user is " + username);
    //      PriceInfo price = restTemplate.getForObject("http://localhost:9060/prices/"
    //                + info.getProductId(), PriceInfo.class);
    //      log.info("price is " + price.getPrice());
        return info;
    }
    

    资源服务器 | 生效

    • 配置好了认证服务和资源服务之后,原先直接可以调用成功的 order 服务将不能成功调用;
    • 需要先向认证服务器申请 token;
    • 需要把 token 添加到请求头的 Authorization 中,因为 Basic Auth 在 Authorization 中填过 Username 和 Password 之后,也是 Base64 加密后,加载请求头 Authorization 中的,所以,为了区分 Basic Auth 和 OAuth2,需要在 OAuth2 的请求头 Authorization 中填 token 的时候,加上前缀 bearer,Basic Auth 的前缀是 Basic

    相关文章

      网友评论

        本文标题:OAuth2 协议与微服务安全

        本文链接:https://www.haomeiwen.com/subject/uufacktx.html