spring security oauth2 搭建認證中心demo

尋找的路上發表於2021-12-14

oauth2 介紹

​ oauth2 協議應該是開發者們耳熟能詳的協議了,這裡就不做過多的介紹了,具體介紹如何在spring security中搭建oauth2的認證服務。Spring-Security-OAuth2是對OAuth2的一種實現,在spring security的官方文件中也有對如何接入oauth2有詳細的說明,https://docs.spring.io/spring-security/site/docs/5.5.3/reference/html5/#oauth2 接下來,我們需要對它進行學習。一般來說,OAuth2.0的服務提供方涵蓋兩個服務,即授權服務 (Authorization Server,也叫認證服務) 和資源服務 (Resource Server),這2個服務可以在同一個應用程式中實現,也可以分開在2個應用程式中實現。這節我們先來看看oauth2.0的認證服務如何搭建。

如何搭建

​ 關於這個問題,大家可以先看看spring security的官方文件,再結合文件給出的demo,最後再結合度娘與谷歌進行查閱資料。

先有spring security的基礎(web安全配置)

​ 首先先搭建spring security的環境,然後再整合spring security oauth2。這裡我們就簡單配置一下spring security,其中有不明白的配置,可以參考前面的文章。
其中主要是WebSecurityConfig配置,

@Configuration
@EnableGlobalMethodSecurity(securedEnabled = true,prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    
    // 注入密碼編碼器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
    
    // 注入認證管理器
    @Bean
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
    //安全攔截機制(最重要)
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/r/r1").hasAnyAuthority("p1")
                .antMatchers("/login*").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
        ;
    }
}

自定義一個UserDetailsService實現類,用於使用者的認證,這裡我們採用從資料庫中讀取使用者,驗證使用者

@Service
public class SpringDataUserDetailsService implements UserDetailsService {

    @Autowired
    UserDao userDao;

    //根據 賬號查詢使用者資訊
    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        //將來連線資料庫根據賬號查詢使用者資訊
        UserDto userDto = userDao.getUserByUsername(username);
        if(userDto == null){
            //如果使用者查不到,返回null,由provider來丟擲異常
            return null;
        }
        //根據使用者的id查詢使用者的許可權
        List<String> permissions = userDao.findPermissionsByUserId(userDto.getId());
        //將permissions轉成陣列
        String[] permissionArray = new String[permissions.size()];
        permissions.toArray(permissionArray);
        //將userDto轉成json
        String principal = JSON.toJSONString(userDto);
        UserDetails userDetails = User.withUsername(principal).password(userDto.getPassword()).authorities(permissionArray).build();
        return userDetails;
    }
}

附上UserDao實現類,直接採用 spring framwork提供的jdbcTemplate來運算元據庫,進行sql查詢及結果對映。

@Repository
public class UserDao {

    @Autowired
    JdbcTemplate jdbcTemplate;

    //根據賬號查詢使用者資訊
    public UserDto getUserByUsername(String username){
        String sql = "select id,username,password,fullname,mobile from t_user where username = ?";
        //連線資料庫查詢使用者
        List<UserDto> list = jdbcTemplate.query(sql, new Object[]{username}, new BeanPropertyRowMapper<>(UserDto.class));
        if(list !=null && list.size()==1){
            return list.get(0);
        }
        return null;
    }

    //根據使用者id查詢使用者許可權
    public List<String> findPermissionsByUserId(String userId){
        String sql = "SELECT * FROM t_permission WHERE id IN(\n" +
                "\n" +
                "SELECT permission_id FROM t_role_permission WHERE role_id IN(\n" +
                "  SELECT role_id FROM t_user_role WHERE user_id = ? \n" +
                ")\n" +
                ")\n";

        List<PermissionDto> list = jdbcTemplate.query(sql, new Object[]{userId}, new BeanPropertyRowMapper<>(PermissionDto.class));
        List<String> permissions = new ArrayList<>();
        list.forEach(c -> permissions.add(c.getCode()));
        return permissions;
    }
}

整合spring security oauth2

管理token令牌

​ 我們知道oauth2主要是靠生成token來進行使用者的驗證,那麼spring security oauth2生成的token是存在哪裡呢?
TokenStore具體有如下好幾種實現方式,可以將token通過jdbc存放在資料庫中,記憶體中,也可以是redis中,這裡我們選擇放在記憶體中。

@Configuration
public class TokenConfig {

    @Bean
    public TokenStore tokenStore() {
        return new InMemoryTokenStore();
    }
}

在AuthorizationServer(具體後面會講)中定義AuthorizationServerTokenServices

@Bean
public AuthorizationServerTokenServices tokenService() {
    DefaultTokenServices service=new DefaultTokenServices();
    service.setClientDetailsService(clientDetailsService);
    service.setSupportRefreshToken(true);
    service.setTokenStore(tokenStore);
    service.setAccessTokenValiditySeconds(7200); // 令牌預設有效期2小時
    service.setRefreshTokenValiditySeconds(259200); // 重新整理令牌預設有效期3天
    return service;
}

自定義一個繼承AuthorizationServerConfigurerAdapter類的實現類AuthorizationServer

​ 我們來看下AuthorizationServerConfigurerAdapter類,發現它只是實現了AuthorizationServerConfigurer介面,但是介面中的幾個方法還是空實現,需要我們去實現它,檢視AuthorizationServerConfigurer介面的方法說明。

public interface AuthorizationServerConfigurer {

   /**
    * Configure the security of the Authorization Server, which means in practical terms the /oauth/token endpoint. The
    * /oauth/authorize endpoint also needs to be secure, but that is a normal user-facing endpoint and should be
    * secured the same way as the rest of your UI, so is not covered here. The default settings cover the most common
    * requirements, following recommendations from the OAuth2 spec, so you don't need to do anything here to get a
    * basic server up and running.
    * 
    * @param security a fluent configurer for security features
    */
    // 設定訪問授權服務的介面安全配置,特別是認證服務的/oauth/authorize,/oauth/token這兩個介面,用來配置令牌端點的安全約束
   void configure(AuthorizationServerSecurityConfigurer security) throws Exception;

   /**
    * Configure the {@link ClientDetailsService}, e.g. declaring individual clients and their properties. Note that
    * password grant is not enabled (even if some clients are allowed it) unless an {@link AuthenticationManager} is
    * supplied to the {@link #configure(AuthorizationServerEndpointsConfigurer)}. At least one client, or a fully
    * formed custom {@link ClientDetailsService} must be declared or the server will not start.
    * 
    * @param clients the client details configurer
    */
    // 用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情資訊在這裡進行初始化,你能夠把客戶端詳情資訊寫死在這裡或者是通過資料庫來儲存調取詳情資訊。
   void configure(ClientDetailsServiceConfigurer clients) throws Exception;

   /**
    * Configure the non-security features of the Authorization Server endpoints, like token store, token
    * customizations, user approvals and grant types. You shouldn't need to do anything by default, unless you need
    * password grants, in which case you need to provide an {@link AuthenticationManager}.
    * 
    * @param endpoints the endpoints configurer
    */
    // 用來配置令牌(token)的訪問端點和令牌服務(token services)。
   void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception;

}
  • ClientDetailsServiceConfigurer:用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情資訊在
    這裡進行初始化,你能夠把客戶端詳情資訊寫死在這裡或者是通過資料庫來儲存調取詳情資訊。
  • AuthorizationServerEndpointsConfigurer:用來配置令牌(token)的訪問端點和令牌服務(token
    services)。
  • AuthorizationServerSecurityConfigurer:用來配置令牌端點的安全約束.

我們需要自定義一個類來實現這3個方法

ClientDetailsServiceConfigurer 配置客戶端配置

​ 檢視ClientDetailsService的實現類,發現它是可以使用基於記憶體或者資料庫來查詢具體接入認證服務的client。具體的clientService有如下屬性:

  • clientId:(必須的)用來標識客戶的Id。
  • secret:(需要值得信任的客戶端)客戶端安全碼,如果有的話。
  • scope:用來限制客戶端的訪問範圍,具體是哪些可以自行定義。
  • authorizedGrantTypes:此客戶端可以使用的授權型別,預設為空
  • authorities:此客戶端可以使用的許可權(基於Spring Security authorities)。

​ 這裡我們先使用基於記憶體的實現來配置一個客戶端,配置如下,後續可以改造成類似於驗證使用者那樣,來基於資料庫進行讀取驗證client.

// 用來配置客戶端詳情服務(ClientDetailsService),客戶端詳情資訊在
//這裡進行初始化,你能夠把客戶端詳情資訊寫死在這裡或者是通過資料庫來儲存調取詳情資訊
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    // clients.withClientDetails(clientDetailsService);
    clients.inMemory()// 使用in‐memory儲存
            .withClient("c1")// client_id
            .secret(new BCryptPasswordEncoder().encode("secret"))
            .resourceIds("res1")
            .authorizedGrantTypes("authorization_code",
                    "password","client_credentials","implicit","refresh_token")// 該client允許的授權型別  authorization_code,password,refresh_token,implicit,client_credentials
            .scopes("all")// 允許的授權範圍
            .autoApprove(false)
            //加上驗證回撥地址
            .redirectUris("http://www.baidu.com");
}

AuthorizationServerEndpointsConfigurer 令牌端點配置

​ AuthorizationServerEndpointsConfigurer 這個物件的例項可以完成令牌服務以及令牌endpoint配置。
​ 主要需要配置一下幾個屬性:

  • userDetailsService: 使用自定義的UserDetailsService,驗證系統使用者

  • authenticationManager: 認證管理器(非必填)

  • authorizationCodeServices: 這個主要用於 "authorization_code" 授權碼型別模式下,申請的授權碼的存放方式

  • tokenServices: 設定token的實現類

  • allowedTokenEndpointRequestMethods: 允許訪問端點的http請求方式

    @Autowired
    private AuthorizationCodeServices authorizationCodeServices;
    @Autowired
    private AuthenticationManager authenticationManager;
    @Bean
    public AuthorizationCodeServices authorizationCodeServices() {
        //設定授權碼模式的授權碼如何存取,暫時採用記憶體方式
        return new InMemoryAuthorizationCodeServices();
    }
    
    // 用來配置令牌(token)的訪問端點和令牌服務(token
    //services)。
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                .userDetailsService(springDataUserDetailsService)
                .authenticationManager(authenticationManager)
                .authorizationCodeServices(authorizationCodeServices)
                .tokenServices(tokenService())
                .allowedTokenEndpointRequestMethods(HttpMethod.POST);
    }
    

oauth2的具體端點可以在org.springframework.security.oauth2.provider.endpoint 這個包下進行檢視,包括:

  • /oauth/authorize :授權端點
  • /oauth/token: 獲取令牌端點
  • /oauth/confirm_access:使用者確認授權提交端點,包括生成的預設授權頁面

AuthorizationServerSecurityConfigurer 令牌端點的安全約束

AuthorizationServerSecurityConfigurer:用來配置令牌端點(Token Endpoint)的安全約束,可以限定哪些url資源可以訪問我們的認證服務端點,主要有以下幾個屬性配置:

  • tokenKeyAccess:tokenkey這個endpoint當使用JwtToken且使用非對稱加密時,資源服務用於獲取公鑰而開放的,這裡指這個
    endpoint完全公開。
  • checkTokenAccess:checkToken這個endpoint完全公開
  • allowFormAuthenticationForClients: 允許表單認證

AuthorizationServer中配置如下.

// 用來配置令牌端點的安全約束
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    security
            // tokenkey這個endpoint當使用JwtToken且使用非對稱加密時,資源服務用於獲取公鑰而開放的,這裡指這個endpoint完全公開
            .tokenKeyAccess("permitAll()")
            // checkToken這個endpoint完全公開
            .checkTokenAccess("permitAll()")
            // 允許表單認證
            .allowFormAuthenticationForClients();
}

示例

這裡我們主要演示一下根據授權碼型別去獲取token:

1、啟動服務後,訪問獲取授權碼地址:

http://localhost:53020/uaa/oauth/authorize?client_id=c1&response_type=code&scope=all&redirect_uri=http://www.baidu.com 因為還沒有登入過,所以spring security會幫我們重定向到預設的登入頁

我們使用賬號hyz/hyz進行登入後,spring security oauth2會幫我們重定向到使用者確認授權提交端點,返回授權頁面,

預設為Deny,我們點選Approve進行授權,會重定向到www.baidu.com頁面,並且會攜帶授權碼code引數

接著我們再根據授權碼,去認證中心訪問獲取token端點

就返回access_token等資訊了

​ 到這裡認證服務主要的基本配置就介紹完了,希望大家有一點收穫。後續會繼續寫資源服務,結合閘道器如何繼承等文章。最後再附上本文的demo程式碼原始碼供參考:https://github.com/githubtwo/distributed-sercurity

相關文章