天天看點

springboot+oauth2初嘗試

一、Oauth2

1.1、Oauth2

OAuth(Open Authorization,開放授權)是為使用者資源的授權定義了一個安全、開放及簡單的标準,第三方無需知道使用者的賬号及密碼,就可擷取到使用者的授權資訊。即允許使用者授權第三方應用通路他們存儲在另外的服務提供者上的資訊,而不需要将使用者名和密碼提供給第三方應用或分享他們資料的所有内容。

OAuth2.0是OAuth協定的延續版本,但不向後相容OAuth 1.0即完全廢止了OAuth1.0。

1.2、OAuth 2.0主要角色

①客戶應用 (Client Application) :通常是一個Web或者無線應用, 它需要通路使用者的受保護資源

②資源伺服器 (Resource Server) :是一個web站點或者web service API,使用者的受保護資料儲存于此

③授權伺服器 (Authorized Server) :在客戶應用成功認證并獲得授權之後,向客戶應用頒發通路令牌 Access Token

④資源擁有者 (Resource Owner) :資源的擁有人, 想要分享某些資源給第三方應用

Oauth2角色詳細分為如下:

①Third-party application:第三方應用程式,本文中又稱"用戶端"(client)

②HTTP service:HTTP服務提供商,本文中簡稱"服務提供商"

a、Authorization server:認證伺服器,即服務提供商專門用來處理認證的伺服器。

b、Resource server:資源伺服器,即服務提供商存放使用者生成的資源的伺服器。它與認證伺服器,可以是同一台伺服器,也可以是不同的伺服器。

③Resource Owner:資源所有者,本文中又稱"使用者"(user)。

④User Agent:使用者代理,本文中就是指浏覽器。

1.3、OAuth 2.0授權類型(Flows)

OAuth2.0 定義了 四種授權模式:

①授權碼Authorization Code:授權碼模式,結合普通伺服器端應用使用。

②簡化Implicit:簡化模式,結合移動應用或 Web App 使用。

③使用者名密碼Resource Owner Credentials:密碼模式,适用于受信任用戶端應用,例如同個組織的内部或外部應用。

④用戶端憑證Client Credentials:用戶端模式,适用于用戶端調用主服務API型應用(比如百度API Store)

1.4、OAuth2 授權流程

擷取微信使用者資訊示例來說

①微信使用者a,通路第三方應用分享到微信中的活動頁面,第三方應用即向微信授權伺服器 發起授權請求以擷取該微信使用者a在微信伺服器上的姓名,頭像等基本資訊(私有資源)

②微信授權伺服器接收到第三方應用的授權請求(包含第三方應用的回調位址的),并引導使用者确認授權(也可以選擇使用者靜默授權)後,傳回授權許可(code)給到第三方應用(根據授權請求傳入的回調位址)

③第三方應用拿到授權許可code後,再次向微信授權伺服器發起通路令牌的請求(攜帶身份app_id等)

④微信授權伺服器驗證第三方應用的身份以及授權許可code,驗證通過後将下發通路令牌access_code,此外還有重新整理令牌以及令牌過期時間等資訊給到第三方應用

⑤第三方應用拿到通路令牌後向微信資源伺服器發起請求資源,即請求微信使用者a的姓名,頭像,地域等基本資訊

⑥微信資源伺服器根據通路令牌,傳回微信使用者a的基本資訊給到第三方應用。

1.5、授權服務

授權伺服器的工作由資源伺服器提供,主要提供兩類接口。

①擷取授權接口:接受第三方應用的授權請求,引導使用者到資源伺服器完成登陸授權的過程

②擷取通路令牌接口:使用授權接口提供的許可憑證來頒發使用者的通路令牌給到第三方應用,或者又第三方應用更新過期的通路令牌。

①AuthorizationEndpoint:用來作為請求者獲得授權的服務,預設的URL是/oauth/authorize.

②TokenEndpoint:用來作為請求者獲得令牌(Token)的服務,預設的URL是/oauth/token.

二、springboot2內建oauth2

2.1、建立SpringBoot項目

開發工具:idea2019.3

Jdk版本:java1.8

調試工具:postman

目錄結構:一個認證服務oauth、一個資源服務resource

oauth有登入和使用者查詢相關接口,resource沒有登入接口,隻有業務接口

2.2、springboot2內建oauth2

Oauth2執行個體可以分為簡易的分為三個步驟:配置資源伺服器、配置認證伺服器、配置spring security。

pom檔案添加oauth2依賴如下

<!-- 引入security依賴-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>

        <!-- 引入oauth2 依賴-->
        <!-- 由于一些注解和API從spring security5.0中移除,是以需要導入下面的依賴包  -->
        <dependency>
            <groupId>org.springframework.security.oauth.boot</groupId>
            <artifactId>spring-security-oauth2-autoconfigure</artifactId>
            <version>2.1.0.RELEASE</version>
        </dependency>
           

2.2.1、配置資源伺服器

@Slf4j
@Configuration
@EnableResourceServer
public class Oauth2ResourceServerConfig extends ResourceServerConfigurerAdapter {

    @Autowired
    private CustomAuthenticationEntryPoint customAuthenticationEntryPoint;

    @Autowired
    private CustomAccessDeniedHandler customAccessDeniedHandler;

    @Autowired
    private RoleMapper roleMapper;

    @Autowired
    private PermissionMapper permissionMapper;

    @Override
    public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
        resources.resourceId("oauth2-resource1").stateless(true);

        //若認證和資源分開則使用下面
        /*RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setCheckTokenEndpointUrl("http://localhost:8080/oauth/token");
        tokenService.setClientId("client2");
        tokenService.setClientSecret("1");

        resources.tokenServices(tokenService);*/
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        log.info("請求資源服務-----------------------------------------------------");
        http
                .exceptionHandling()
                .authenticationEntryPoint(customAuthenticationEntryPoint)
                .accessDeniedHandler(customAccessDeniedHandler)
                .and()
                //requestMatchers放在最前面,對需要比對的的規則進行自定義與過濾
                .requestMatchers().antMatchers("/oauth2/**", "/user/**")
                .and()
                .authorizeRequests().antMatchers("/oauth2/logout","/user/getUserList").authenticated();
                //其餘接口沒有角色限制,但需要經過認證,隻要攜帶token就可以放行
                //.anyRequest()
                //.authenticated();

        //動态加載資料庫中角色權限(接口和角色動态調整),在SpringSecurity校驗權限的時候,會自動将權限前面加ROLE_,是以我們需要 将我們資料庫中配置的ROLE_截取掉。
        List<Role> roleList = roleMapper.getRoleList();
        for (Role role : roleList) {
            List<Permission> permissionList = permissionMapper.getPermissionListByRoleId(role.getId());
            for (Permission permission : permissionList) {
                http.authorizeRequests()
                        .antMatchers(permission.getUrl())
                        //自動加ROLE_
                        .hasRole(role.getName());
            }
        }
    }
}

           

2.2.2、配置認證伺服器

@Slf4j
@Configuration
@EnableAuthorizationServer
public class Oauth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager authenticationManager;

    @Resource
    private RedisConnectionFactory redisConnectionFactory;

    @Resource
    private DataSource dataSource;

    @Resource
    private CustomClientDetailsService customClientDetailsService;

    /**
     * 授權碼模式code存儲方式,預設記憶體存儲
     * @return
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        return new JdbcAuthorizationCodeServices(dataSource);
    }

    /**
     * 授權允許存儲方式,預設記憶體存儲
     * @return
     */
    @Bean
    public ApprovalStore approvalStore() {
        return new JdbcApprovalStore(dataSource);
    }

    /**
     * oauth_client_token操作這個資料庫表,每個需要中繼的節點都需要建立這個表
     * OAuth 2 用戶端  client_credentials授權方式獲得的Token
     * @return
     */
    @Bean
    public JdbcClientTokenServices jdbcClientTokenServices() {
        return new JdbcClientTokenServices(dataSource);
    }

    /**
     * token存儲在redis,可以存儲在記憶體中,關系型資料庫中,redis中
     * 注意:如果不儲存access_token,則沒法通過access_token取得使用者資訊
     * @return
     */
    @Bean
    public TokenStore tokenStore() {
        //return new InMemoryTokenStore();
        //return new JdbcTokenStore(dataSource);
        return new RedisTokenStore(redisConnectionFactory);
    }

    /**
     * @description 用來配置用戶端詳情服務(ClientDetailsService),用戶端詳情資訊在這裡初始化
     * 可以把用戶端詳情資訊寫死也可以寫入記憶體或者資料庫中
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        log.info("初始化用戶端詳情------------------------------------------------");
        //使用自定義客戶ClientDetailsService初始化配置
        clients.withClientDetails(customClientDetailsService);
    }

    /**
     * 用來配置令牌端點(Token Endpoint)的安全限制
     * @param security
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        // 自定義異常處理端口
        security.authenticationEntryPoint(new CustomAuthenticationEntryPoint());
        security
                // code授權添加
                .realm("oauth2-resources")
                // 所有人可通路 /oauth/token_key 後面要擷取公鑰, 預設拒絕通路
                .tokenKeyAccess("permitAll()")
                // 認證後可通路 /oauth/check_token,預設拒絕通路, "isAuthenticated()"
                .checkTokenAccess("permitAll()")
                // 支援把secret和clientId寫在url上,否則需要在頭上
                .allowFormAuthenticationForClients();
    }

    /**
     * @description token及使用者資訊存儲到redis,當然你也可以存儲在目前的服務記憶體,不推薦
     * 用來配置授權(authorization)以及令牌(token)的通路端點和令牌服務(token services),還有token的存儲方式(tokenStore)
     * @param endpoints 通路端點
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        //token資訊存到redis,通過authenticationManager開啟密碼模式授權
        endpoints.tokenStore(tokenStore()).authenticationManager(authenticationManager);
        //配置TokenService參數
        DefaultTokenServices tokenServices = new DefaultTokenServices();
        //token持久化容器
        tokenServices.setTokenStore(endpoints.getTokenStore());
        //是否支援refresh_token,預設false
        tokenServices.setSupportRefreshToken(true);
        //用戶端資訊
        tokenServices.setClientDetailsService(endpoints.getClientDetailsService());
        //自定義token生成
        tokenServices.setTokenEnhancer(endpoints.getTokenEnhancer());
        //access_token 的有效時長 (秒), 預設 12 小時;1小時
        tokenServices.setAccessTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
        //refresh_token 的有效時長 (秒), 預設 30 天;1小時
        tokenServices.setRefreshTokenValiditySeconds((int) TimeUnit.HOURS.toSeconds(1));
        //是否複用refresh_token,預設為true(如果為false,則每次請求重新整理都會删除舊的refresh_token,建立新的refresh_token)
        tokenServices.setReuseRefreshToken(false);
        //token相關服務
        endpoints.tokenServices(tokenServices);

        endpoints
                // jdbc存儲方式
                //.tokenStore(tokenStore())
                // 授權允許存儲方式
                .approvalStore(approvalStore())
                // 授權碼模式code存儲方式
                .authorizationCodeServices(authorizationCodeServices());
    }

}

           

2.2.3、配置spring security

@Order(1)
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class Oauth2WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Resource
    private CustomUserDetailsService customUserDetailsService;

    @Resource
    private CustomAuthenticationProvider customAuthenticationProvider;

    @Override
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }

    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    /**
     * 配置登入驗證
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        //自定義使用者userDetailService、加密
        auth.userDetailsService(customUserDetailsService).passwordEncoder(passwordEncoder());
        //加入自定義的安全認證
        auth.authenticationProvider(customAuthenticationProvider);
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        //post請求預設的都開啟了csrf的模式,所有post請求都必須帶有token之類的驗證資訊才可以進入登陸頁面,這邊是禁用csrf模式
        http.csrf().disable();
        http
                //requestMatchers放在最前面,對需要比對的的規則進行自定義與過濾
                .requestMatchers().antMatchers("/oauth/**")
                //攔截上面比對後的url,需要認證後通路
                .and()
                .authorizeRequests().antMatchers("/oauth/**").authenticated()
                .and()
                .formLogin()
                //使用者登入自定義url
                .loginProcessingUrl("/rest/hello")
                //使用者登入成功跳轉url
                .successForwardUrl("/rest/hello")
                //使用者登入失敗跳轉url
                .failureForwardUrl("/rest/hello")
                //使用 spring security 預設登入頁面
                .permitAll();
        //http.addFilterBefore(new MyAuthenticationFilter("/login"), UsernamePasswordAuthenticationFilter.class);
        http.sessionManagement()
                .invalidSessionUrl("/login")
                .maximumSessions(1)
                .expiredUrl("/login");
    }

}

           

2.2.4、調試及結果

下面用postman工具及浏覽器對oauth2執行個體的四種模式進行調試及結果進行展示。

密碼模式

密碼模式需要參數:

username,password,grant_type,client_id,client_secret

grant_type類型password

操作步驟如下

①請求access_token

http://localhost:8080/oauth/token?username=demoUser1&password=123456&grant_type=password&client_id=demoApp&client_secret=demoAppSecret

請求成功結果如下

{

“access_token”: “e47f060a-27ff-4324-94b3-9bf947cb0e10”,

“token_type”: “bearer”,

“refresh_token”: “af6eb304-eb1d-4905-a0c2-efddd836a9f5”,

“expires_in”: 846,

“scope”: “all”

}

②根據獲得的access_token進行資源請求

http://localhost:8080/rest/hello?access_token=e47f060a-27ff-4324-94b3-9bf947cb0e10

請求成功結果如下

③密碼模式适用于使用者對應用程式高度信任的情況。比如是使用者作業系統的一部分。

後續會持續更新。。。