第十節(jié) spring could security實(shí)現(xiàn)OAuth2

使用spring could security實(shí)現(xiàn)OAuth2來控制服務(wù)中api的安全

  • 使用spring could oauth
OAuth2 在服務(wù)提供者上可分為兩類:
    授權(quán)認(rèn)證服務(wù):AuthenticationServer
   資源獲取服務(wù):ResourceServer
兩者也可以在同一個(gè)服務(wù)上,但就微服務(wù)而言,應(yīng)該在不同的服務(wù)中,本項(xiàng)目api-server為資源獲取服務(wù)security-server為授權(quán)認(rèn)證服務(wù)
  • 授權(quán)認(rèn)證服務(wù)的作用,可參考上一節(jié):
1. 獲取第三方應(yīng)用發(fā)送的授權(quán)碼(code)以及第三方應(yīng)用標(biāo)識(shí)
2. 根據(jù)授權(quán)碼及標(biāo)識(shí)進(jìn)行校驗(yàn)
3. 校驗(yàn)通過,發(fā)送令牌(Access Token)

簡單用上一節(jié)的例子,第一步就是點(diǎn)擊微信第三方登陸的url,請(qǐng)求參數(shù)帶有client_id(第三方用戶的id(可理解為賬號(hào)))client_secret(第三方應(yīng)用和授權(quán)服務(wù)器之間的安全憑證(可理解為密碼)。除此還會(huì)帶有redirect_uri中的回調(diào)鏈接,微信服務(wù)會(huì)生成相關(guān)用戶憑證,并在其回調(diào)鏈接上附帶code
第二步中,授權(quán)服務(wù)器(微信),首先會(huì)校驗(yàn)第三方服務(wù)器(比如簡書平臺(tái))的真實(shí)可靠信接著會(huì)根據(jù)授權(quán)碼(code)進(jìn)行校驗(yàn)客戶是否已認(rèn)證
第三步,通過第二步授權(quán)碼code認(rèn)證通過后,生成token,通過回調(diào)地址返回(MD5類型,uuid類型,jwt類型等)
  • 令牌的生成和管理
創(chuàng)建AccessToken,并保存,以備后續(xù)請(qǐng)求訪問都可以認(rèn)證成功并獲取到資源
AccessToken還有一個(gè)潛在功能,就是使用jwt生成token時(shí)候,可以用來加載一些信息,把一些相關(guān)權(quán)限等包含在AccessToken中

創(chuàng)建方法:
1. 可實(shí)現(xiàn)AuthorizationServerTokenServices 接口提供了對(duì)AccessToken的相關(guān)操作創(chuàng)建、刷新、獲取
2. spring就默認(rèn)為我們提供了一個(gè)默認(rèn)的DefaultTokenServices,提供一些基礎(chǔ)的操作token
保存方法,創(chuàng)建AccessToken完之后,除了發(fā)放給第三方,肯定還得保存起來:
1. inMemoryTokenStore:這個(gè)是OAuth2默認(rèn)采用的實(shí)現(xiàn)方式。在單服務(wù)上可以體現(xiàn)出很好特效(即并發(fā)量不大,并且它在失敗的時(shí)候不會(huì)進(jìn)行備份),大多項(xiàng)目都可以采用此方法。畢竟存在內(nèi)存,而不是磁盤中,調(diào)試簡易。
2. JdbcTokenStore:這個(gè)是基于JDBC的實(shí)現(xiàn),令牌(Access Token)會(huì)保存到數(shù)據(jù)庫。這個(gè)方式,可以在多個(gè)服務(wù)之間實(shí)現(xiàn)令牌共享。
3. JwtTokenStore:jwt全稱 JSON Web Token。這個(gè)實(shí)現(xiàn)方式不用管如何進(jìn)行存儲(chǔ)(內(nèi)存或磁盤),因?yàn)樗梢园严嚓P(guān)信息數(shù)據(jù)編碼存放在令牌里。JwtTokenStore 不會(huì)保存任何數(shù)據(jù),但是它在轉(zhuǎn)換令牌值以及授權(quán)信息方面與 DefaultTokenServices 所扮演的角色是一樣的。但有兩個(gè)缺點(diǎn):
撤銷一個(gè)已經(jīng)授權(quán)的令牌會(huì)很困難,因此只適用于處理一個(gè)生命周期較短的以及撤銷刷新令牌。
 令牌占用空間大,如果加入太多用戶憑證信息,會(huì)存在傳輸冗余 
  • 端點(diǎn)接入
    授權(quán)認(rèn)證是使用AuthorizationEndpoint這個(gè)端點(diǎn)來進(jìn)行控制,一般使用AuthorizationServerEndpointsConfigurer 來進(jìn)行配置。
1. 端點(diǎn)(endpoints)的相關(guān)屬性配置:
authenticationManager:認(rèn)證管理器。若我們上面的Grant Type設(shè)置為password,則需設(shè)置一個(gè)AuthenticationManager對(duì)象
userDetailsService:若是我們實(shí)現(xiàn)了UserDetailsService,來管理用戶信息,那么得設(shè)我們的userDetailsService對(duì)象
authorizationCodeServices:授權(quán)碼服務(wù)。若我們上面的Grant Type設(shè)置為authorization_code,那么得設(shè)一個(gè)AuthorizationCodeServices對(duì)象
tokenStore:這個(gè)就是我們上面說到,把我們想要是實(shí)現(xiàn)的Access Token類型設(shè)置
accessTokenConverter:Access Token的編碼器。也就是JwtAccessTokenConverter
tokenEnhancer:token的拓展。當(dāng)使用jwt時(shí)候,可以實(shí)現(xiàn)TokenEnhancer來進(jìn)行jwt對(duì)包含信息的拓展
tokenGranter:當(dāng)默認(rèn)的Grant Type已經(jīng)不夠我們業(yè)務(wù)邏輯,實(shí)現(xiàn)TokenGranter 接口,授權(quán)將會(huì)由我們控制,并且忽略Grant Type的幾個(gè)屬性。
2. 端點(diǎn)(endpoints)的授權(quán)url:
要授權(quán)認(rèn)證,肯定得由url請(qǐng)求,才可以傳輸。因此OAuth2提供了配置授權(quán)端點(diǎn)的URL。
AuthorizationServerEndpointsConfigurer ,還是這個(gè)配置對(duì)象進(jìn)行配置,其中由一個(gè)pathMapping()方法進(jìn)行配置授權(quán)端點(diǎn)URL路徑
默認(rèn)實(shí)現(xiàn)
/oauth/authorize:授權(quán)端點(diǎn)
/oauth/token:令牌端點(diǎn)
/oauth/confirm_access:用戶確認(rèn)授權(quán)提交端點(diǎn)
/oauth/error:授權(quán)服務(wù)錯(cuò)誤信息端點(diǎn)
/oauth/check_token:用于資源服務(wù)訪問的令牌解析端點(diǎn)
/oauth/token_key:提供公有密匙的端點(diǎn),如果使用JWT令牌的話

使用Oauth2的授權(quán)碼模式實(shí)戰(zhàn)

  1. 首先創(chuàng)建一個(gè)安全服務(wù)spring security,用于控制身份驗(yàn)證和授權(quán)。
  • 增加pom依賴
 <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>
  • 在啟動(dòng)類上啟用@EnableAuthorizationServer表示啟用授權(quán)服務(wù)器,可參照如下配置:
//啟用資源服務(wù)器
@SpringBootApplication
@RestController
@EnableAuthorizationServer
@ComponentScan("com.xzg.security.service")
public class SecurityApp {
public static void main(String[] args) {
    SpringApplication.run(SecurityApp.class, args);
}
}
  • 配置web security服務(wù)
@Configuration
public class WebSecurityConfigurer extends WebSecurityConfigurerAdapter {
    @Autowired
    private BaseUserDetailService baseUserDetailService;
    //Spring Security 4.x -> 5.x  會(huì)無法直接注入AuthenticationManager,下面解決
    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    /**
     * 用戶驗(yàn)證
     * @param auth
     */
    @Override
    public void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.authenticationProvider(daoAuthenticationProvider());
        auth.userDetailsService(baseUserDetailService) .passwordEncoder(passwordEncoder());
    }
    /**
     * @param http
     * WebSecurityConfigurerAdapter和ResourceServerConfigurerAdapter二者是分工協(xié)作的
     * @throws Exception
     * WebSecurityConfigurerAdapter不攔截oauth要開放的資源
     */
    @Override
    public void configure(HttpSecurity http) throws Exception {
        http    // 配置登陸頁/login并允許訪問
                .formLogin().permitAll()
                // 登出頁
                .and().logout().logoutUrl("/logout").logoutSuccessUrl("/")
                // 其余所有請(qǐng)求全部需要鑒權(quán)認(rèn)證
                .and().authorizeRequests().anyRequest().authenticated()
                // 由于使用的是JWT,我們這里不需要csrf
                .and().csrf().disable();
    }
    @Bean
    public DaoAuthenticationProvider daoAuthenticationProvider(){
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        // 設(shè)置userDetailsService
        provider.setUserDetailsService(baseUserDetailService);
        // 禁止隱藏用戶未找到異常
        provider.setHideUserNotFoundExceptions(false);
        // 使用BCrypt(BCryptPasswordEncoder方法采用SHA-256 +隨機(jī)鹽+密鑰)進(jìn)行密碼的hash
        provider.setPasswordEncoder(passwordEncoder());
        return provider;
    }
    @Bean
    public PasswordEncoder passwordEncoder(){
        return  new BCryptPasswordEncoder();
    }
}
  • 配置oauth2授權(quán)服務(wù)器
/**
 * @author xzg
 * 授權(quán)認(rèn)證服務(wù):AuthenticationServer
 */
@Configuration
public class OAuth2Config extends AuthorizationServerConfigurerAdapter {
    @Autowired
    private BaseUserDetailService userDetailsService;

    @Autowired
    private AuthenticationManager authenticationManager;

    /**
     * @param endpointsConfigurer
     * 用來配置授權(quán)(authorization)以及令牌(token)的訪問端點(diǎn)和令牌服務(wù)(token services)。
     * @throws Exception
     * 配置令牌 管理 (jwtAccessTokenConverter)
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints.authenticationManager(authenticationManager)
                // 配置JwtAccessToken轉(zhuǎn)換器
                .accessTokenConverter(jwtAccessTokenConverter())
                // refresh_token需要userDetailsService
                //走password的就是用AuthorizationServerEndpointsConfigurer中配置的userDetailsService來進(jìn)行認(rèn)證
                .reuseRefreshTokens(false)
                .userDetailsService(userDetailsService);
        //.tokenStore(getJdbcTokenStore());
    }

    /**
     * @param clientDetailsServiceConfigurer
     * ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(wù)(ClientDetailsService)
     * 客戶端詳情信息在這里進(jìn)行初始化,你能夠把客戶端詳情信息寫死在這里或者是通過數(shù)據(jù)庫來存儲(chǔ)調(diào)取詳情信息。
     * @throws Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clientDetailsServiceConfigurer) throws Exception {
        // Using hardcoded inmemory mechanism because it is just an example
        clientDetailsServiceConfigurer
                .inMemory()//使用方法代替in-memory、JdbcClientDetailsService、jwt
//        client_id: 用來標(biāo)識(shí)客戶的Id。第三方用戶的id(可理解為賬號(hào))
                .withClient("client")
//        client_secret:第三方應(yīng)用和授權(quán)服務(wù)器之間的安全憑證(可理解為密碼)
                //(需要值得信任的客戶端)客戶端安全碼
                .secret(BCryptUtil.encodePassword("password"))
                .accessTokenValiditySeconds(7200)
                .authorizedGrantTypes("authorization_code", "refresh_token", "client_credentials", "implicit", "password")
//                .scopes("app");
                .authorities("ROLE_USER")
                .scopes("apiAccess");
    }


    /**
     * @param security
     *  AuthorizationServerSecurityConfigurer:用來配置令牌端點(diǎn)(Token Endpoint)的安全約束
     * @throws Exception
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // 開啟/oauth/token_key驗(yàn)證端口無權(quán)限訪問
                .tokenKeyAccess("permitAll()")
                // 開啟/oauth/check_token驗(yàn)證端口認(rèn)證權(quán)限訪問
                .checkTokenAccess("isAuthenticated()")
                .passwordEncoder(new BCryptPasswordEncoder())
//        請(qǐng)求/oauth/token的,如果配置支持allowFormAuthenticationForClients的,且url中有client_id和client_secret的會(huì)走ClientCredentialsTokenEndpointFilter
                .allowFormAuthenticationForClients();
    }
    /**
     * 使用非對(duì)稱加密算法來對(duì)Token進(jìn)行簽名
     * @return
     */
    @Bean
    public JwtAccessTokenConverter jwtAccessTokenConverter() {

        final JwtAccessTokenConverter converter = new JwtAccessToken();
        // 導(dǎo)入證書
        KeyStoreKeyFactory keyStoreKeyFactory =
                new KeyStoreKeyFactory(new ClassPathResource("keystore.jks"), "password".toCharArray());

        converter.setKeyPair(keyStoreKeyFactory.getKeyPair("selfsigned"));

        return converter;
    }
}
  • 為了測(cè)試springsecurity方便,去掉spring默認(rèn)的用戶密碼替換硬編碼(用戶密碼client和password)。可根據(jù)業(yè)務(wù)調(diào)整
@Service
public class BaseUserDetailService implements UserDetailsService {

    private Logger logger = LoggerFactory.getLogger(this.getClass());
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        logger.info("獲取登陸信息:" + username);
        // 調(diào)用FeignClient查詢用戶略...

        //注意:實(shí)際生產(chǎn)應(yīng)該從數(shù)據(jù)庫中獲取,用戶名,密碼(為BCryptPasswordEncoder hash后的密碼)
        if (!"client".equals(username)) {
            logger.error("找不到該用戶,用戶名:" +username);
            throw new UsernameNotFoundException("找不到該用戶,用戶名:" + username);
        }
        //密碼硬編碼
        String password = BCryptUtil.encodePassword("password");
        // 獲取用戶權(quán)限列表
        List<GrantedAuthority> authorities = CreatHardRole.createAuthorities().get();
        // 返回帶有用戶權(quán)限信息的User
        org.springframework.security.core.userdetails.User user = new org.springframework.security.core.userdetails.User(username,
                password, true, true, true, true, authorities);
        return new BaseUserDetail(new BaseUser(username, password), user);
    }
}
  • 其他服務(wù)以及jwt token參考源碼
  • 配置文件
info:
    component:
        Security Server
# password 為生成證書的密碼
server:
    port: 9001
    ssl:
        key-store: classpath:keystore.jks
        key-store-password: password
        key-password: password
# contextPath表示上下文;路徑
    contextPath: /auth
# 暫時(shí)使用硬編碼
security:
    user:
        password: password

logging:
    level:
        org.springframework.security: DEBUG
  • 除此之外,在security-server服務(wù)啟用https,加密傳輸?shù)姆绞?,配置如下?br> 1) 創(chuàng)建證書嵌入到項(xiàng)目中
keytool -genkey -keyalg RSA -alias selfsigned -keystore keystore.jks -ext san=dns:localhost  -storepass password -validity 365 -keysize 2048

執(zhí)行過程如下圖:


制作開發(fā)證書

將證書放入項(xiàng)目,并配置如圖:


圖片.png
  • 注意:使用-ext來定義主題設(shè)備名稱(san). 可以使用瀏覽器或者Openssl下載證書,

啟動(dòng)security-serve測(cè)試

  1. 在瀏覽器中
在瀏覽器中請(qǐng)求:https://localhost:9001/oauth/authorize?client_id=client&response_type=code&redirect_uri=http://www.baidu.com

如果未登陸則spring security會(huì)讓登陸,這里使用程序中的硬編碼

圖片.png

登陸后,選擇授權(quán)authorization,如下圖

authorization

authorization服務(wù)會(huì)轉(zhuǎn)發(fā)到baidu的url并附code,如下圖

圖片.png
  1. 獲取code發(fā)送去獲取token。 下面命令中'Authorization: Basic Y2xpZW50OmNsaWVudHNlY3JldA==' 基于插件。可使用post
curl -X POST -k -H 'Content-Type: application/x-www-form-urlencoded' -i https://localhost:9001/oauth/token --data 'grant_type=authorization_code&client_id=client&redirect_uri=http://www.baidu.com&code=GXz7W9'

響應(yīng)token結(jié)果如下圖


圖片.png
  1. 至此獲取token后,就可以使用token去請(qǐng)求資源服務(wù)器的api了
    下一節(jié)中,資源服務(wù)器和zuul邊緣服務(wù)器一起構(gòu)成整個(gè)微服務(wù)的api網(wǎng)關(guān)的權(quán)限控制

項(xiàng)目地址
spring boot 實(shí)現(xiàn)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容