Cấu hình Máy chủ Xác thực OAuth trong Spring Security

Phụ thuộc gói cơ bản

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-security</artifactId>
</dependency>
================================== Trong Spring Boot ==================================
<dependency>
   <groupId>org.springframework.security.oauth</groupId>
   <artifactId>spring-security-oauth2</artifactId>
   <version>2.3.5.RELEASE</version>
</dependency>
================================== Hoặc trong Spring Cloud ==================================
<dependency>
   <groupId>org.springframework.cloud</groupId>
   <artifactId>spring-cloud-starter-oauth2</artifactId>
</dependency>

Ba phần cấu hình chính

Cấu hình máy chủ xác thực yêu cầu kế thừa lớp AuthorizationServerConfigurerAdapter và ghi đè các phương thức bên trong để tùy chỉnh logic tạo token. Mã nguồn của lớp này như sau:

public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

    // Cấu hình ràng buộc bảo mật, chủ yếu là bật/tắt các endpoint mặc định
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
	}

    // Cấu hình thông tin chi tiết của client
	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	}

    // Cấu hình endpoint truy cập token và dịch vụ token của máy chủ xác thực, có thể thay thế URL endpoint mặc định, cấu hình các loại ủy quyền được hỗ trợ
	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
	}

}
  1. Cấu hình thông tin chi tiết client

ClientDetailsServiceConfigurer có thể sử dụng bộ nhớ trong (in-memory) hoặc JDBC để triển khai dịch vụ chi tiết client (ClientDetailsService). ClientDetailsService chịu trách nhiệm tìm kiếm ClientDetails, và ClientDetails có một số thuộc tính quan trọng như sau:

  • clientId: (Bắt buộc) Dùng để định danh client.
  • secret: (Dành cho client đáng tin cậy) Mật khẩu bảo mật của client, nếu có.
  • scope: Dùng để giới hạn phạm vi truy cập của client, nếu để trống (mặc định) thì client có toàn bộ phạm vi truy cập.
  • authorizedGrantTypes: Các loại ủy quyền mà client có thể sử dụng, mặc định là trống.
  • authorities: Các quyền (dựa trên Spring Security authorities) mà client có thể sử dụng.

Thông tin chi tiết client (ClientDetails) có thể được cập nhật khi ứng dụng đang chạy, bằng cách truy cập vào dịch vụ lưu trữ nền tảng (ví dụ: nếu lưu thông tin client trong một bảng cơ sở dữ liệu quan hệ, bạn có thể sử dụng JdbcClientDetailsService hoặc tự triển khai giao diện ClientRegistrationService (cùng với việc triển khai giao diện ClientDetailsService).

  1. Cấu hình endpoint truy cập token và dịch vụ token

Đối tượng AuthorizationServerEndpointsConfigurer có thể hoàn thành việc cấu hình dịch vụ token và endpoint token.

Cấu hình các loại ủy quyền (Grant Types)

AuthorizationServerEndpointsConfigurer quyết định các loại ủy quyền được hỗ trợ bằng cách thiết lập các thuộc tính sau:

  • authenticationManager: Trình quản lý xác thực, khi bạn chọn loại ủy quyền mật khẩu (password) của chủ sở hữu tài nguyên, hãy thiết lập thuộc tính này để inject một đối tượng AuthenticationManager.
  • userDetailsService: Nếu bạn thiết lập thuộc tính này, điều đó có nghĩa là bạn có một triển khai của giao diện UserDetailsService của riêng mình, hoặc bạn có thể đặt nó vào phạm vi toàn cục (ví dụ: đối tượng cấu hình GlobalAuthenticationManagerConfigurer), khi bạn thiết lập điều này, quy trình của loại ủy quyền "refresh_token" sẽ bao gồm một kiểm tra để đảm bảo tài khoản này vẫn còn hiệu lực, nếu bạn vô hiệu hóa tài khoản này.
  • authorizationCodeServices: Thuộc tính này dùng để thiết lập dịch vụ mã ủy quyền (tức là một instance của AuthorizationCodeServices), chủ yếu được sử dụng cho loại ủy quyền "authorization_code".
  • implicitGrantService: Thuộc tính này dùng để thiết lập loại ủy quyền ngầm (implicit), dùng để quản lý trạng thái của loại ủy quyền ngầm.
  • tokenGranter: Khi bạn thiết lập thuộc tính này (tức là một triển khai của giao diện TokenGranter), việc ủy quyền sẽ được hoàn toàn do bạn kiểm soát và sẽ bỏ qua các thuộc tính trên. Thuộc tính này thường được sử dụng cho mục đích mở rộng, tức là bốn loại ủy quyền tiêu chuẩn không đáp ứng được nhu cầu của bạn.
Cấu hình URL endpoint ủy quyền

Các endpoint URL mặc định có 6 endpoint:

  • /oauth/authorize: Endpoint ủy quyền.
  • /oauth/token: Endpoint token.
  • /oauth/confirm_access: Endpoint xác nhận ủy quyền của người dùng.
  • /oauth/error: Endpoint thông báo lỗi của dịch vụ ủy quyền.
  • /oauth/check_token: Endpoint phân tích token được sử dụng bởi máy chủ tài nguyên.
  • /oauth/token_key: Endpoint cung cấp khóa công khai, nếu bạn sử dụng token JWT.

Tất cả các endpoint này đều có thể được thay đổi đường dẫn bằng cách cấu hình, trong AuthorizationServerEndpointsConfigurer có một phương thức tên là pathMapping() dùng để cấu hình URL endpoint, nó có hai tham số:

  • Tham số thứ nhất: kiểu string, URL mặc định.
  • Tham số thứ hai: kiểu string, URL thay thế.

Cả hai tham số đều là chuỗi bắt đầu bằng "/".

  1. Cấu hình ràng buộc bảo mật cho endpoint token

AuthorizationServerSecurityConfigurer: Dùng để cấu hình ràng buộc bảo mật cho endpoint token (Token Endpoint), ví dụ như cấu hình máy chủ tài nguyên cần xác thực token tại /oauth/check_token.

Mã nguồn chi tiết

  • Lớp cấu hình máy chủ xác thực
@Configuration
@EnableAuthorizationServer
public class OAuthAuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    private PasswordEncoder encoder;
    private TokenStore store;
    private ClientDetailsService clientService;
    private AuthenticationManager authManager;

    @Autowired
    public OAuthAuthorizationServerConfig(PasswordEncoder encoder, TokenStore store, ClientDetailsService clientService, AuthenticationManager authManager) {
        this.encoder = encoder;
        this.store = store;
        this.clientService = clientService;
        this.authManager = authManager;
    }

    /**
     * Ràng buộc bảo mật cho endpoint token
     *
     * @param security AuthorizationServerSecurityConfigurer
     */
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                // Mở `/oauth/token_key`
                .tokenKeyAccess("permitAll()")
                // Mở `/oauth/check_token`
                .checkTokenAccess("permitAll()")
                .allowFormAuthenticationForClients();
    }

    /**
     * Dịch vụ chi tiết client, tạm thời cấu hình trong bộ nhớ, sau này sẽ chuyển sang lưu trong database
     *
     * @param clients ClientDetailsServiceConfigurer
     * @throws Exception Exception
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("my-client")
                .secret(this.encoder.encode("secret123"))
                // Để kiểm tra, nên bật tất cả các loại, trong thực tế hãy chọn theo nhu cầu kinh doanh
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(864000)
                .scopes("read_profile")
                // false sẽ chuyển đến trang ủy quyền, được sử dụng trong chế độ mã ủy quyền
                .autoApprove(false)
                // Xác thực địa chỉ callback
                .redirectUris("http://localhost:8081/login/oauth2/code/custom");
    }

    /**
     * Cấu hình endpoint truy cập token và dịch vụ truy cập token
     *
     * @param endpoints AuthorizationServerEndpointsConfigurer
     */
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                // Chế độ mật khẩu
               .authenticationManager(authManager)
                // Chế độ mã ủy quyền
                .authorizationCodeServices(authorizationCodeService())
                // Quản lý token
                .tokenServices(tokenService());

    }

    /**
     * Dịch vụ quản lý token
     *
     * @return TokenServices
     */
    @Bean
    public AuthorizationServerTokenServices tokenService() {
        DefaultTokenServices services = new DefaultTokenServices();
        // Dịch vụ chi tiết client
        services.setClientDetailsService(clientService);
        // Hỗ trợ làm mới token
        services.setSupportRefreshToken(true);
        // Chiến lược lưu trữ token
        services.setTokenStore(store);
        // Thời gian sống mặc định của token là 2 giờ
        services.setAccessTokenValiditySeconds(7200);
        // Thời gian sống mặc định của token làm mới là 2 ngày
        services.setRefreshTokenValiditySeconds(259200);
        return services;
    }

    /**
     * Cách lưu trữ mã ủy quyền trong chế độ mã ủy quyền, tạm thời sử dụng bộ nhớ
     *
     * @return AuthorizationCodeServices
     */
    @Bean
    public AuthorizationCodeServices authorizationCodeService() {
        return new InMemoryAuthorizationCodeServices();
    }
}
  • Lớp cấu hình lưu trữ token
@Configuration
public class TokenConfig {

    @Bean
    public TokenStore tokenStorage() {
        return new InMemoryTokenStore();
    }
}
  • Cấu hình bảo mật Spring Security
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    private CustomUserDetailsService userDetailsService;
    private PasswordEncoder passwordEncoder;

    @Autowired
    public SecurityConfig(CustomUserDetailsService userDetailsService, PasswordEncoder passwordEncoder) {
        this.userDetailsService = userDetailsService;
        this.passwordEncoder = passwordEncoder;
    }

    /**
     * Cơ chế chặn bảo mật (quan trọng nhất)
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.csrf().disable()
                .authorizeRequests()
                .antMatchers("/oauth/**").permitAll()
                .anyRequest().authenticated()
                .and().formLogin();
    }

    /**
     * Cấu hình quản lý xác thực (không bắt buộc)
     * Kết nối truy vấn dữ liệu người dùng, so sánh mật khẩu với cơ sở dữ liệu
     *
     * @param auth AuthenticationManagerBuilder
     * @throws Exception Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder);
    }

    /**
     * Cấu hình vùng quản lý xác thực, cần thiết cho chế độ mật khẩu
     *
     * @return AuthenticationManager
     * @throws Exception Exception
     */
    @Bean
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }
}
  • Cấu hình mã hóa mật khẩu
@Configuration
public class SecurityConfig {

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
  • Lớp lấy thông tin người dùng
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private PasswordEncoder passwordEncoder;

    @Autowired
    public CustomUserDetailsService(PasswordEncoder passwordEncoder) {
        this.passwordEncoder = passwordEncoder;
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // Giả lập một người dùng, thay thế logic lấy từ database
        AppUser user = new AppUser();
        user.setUsername(username);
        user.setPassword(this.passwordEncoder.encode("password123"));

        return new User(username, user.getPassword(), user.isEnabled(),
                user.isAccountNonExpired(), user.isCredentialsNonExpired(),
                user.isAccountNonLocked(), AuthorityUtils.commaSeparatedStringToAuthorityList("USER"));
    }
}

@Data
public class AppUser implements Serializable {
    private static final long serialVersionUID = 3497935890426858541L;

    private String username;
    private String password;
    private boolean accountNonExpired = true;
    private boolean accountNonLocked = true;
    private boolean credentialsNonExpired = true;
    private boolean enabled = true;
}

Vấn đề có thể xảy ra

  • Vấn đề phổ biến nhất, lỗi phụ thuộc vòng lặp khi khởi động dự án, cần chú ý rằng các Bean được viết trong lớp cấu hình hiện tại không thể được @Autowired trong chính lớp đó.
  • Lỗi tràn bộ nhớ đệm (Stack Overflow) khi đăng nhập.

Gỡ lỗi:

Đây là do tên phương thức authenticationManager được sử dụng khi cấu hình AuthenticationManager trong chế độ mật khẩu. Thay đổi thành authenticationManagerBean sẽ giải quyết vấn đề.

Kiểm tra

Chế độ mã ủy quyền để lấy token

  1. Lấy mã ủy quyền trước Mở trình duyệt và truy cập http://127.0.0.1:8080/oauth/authorize?client_id=my-client&response_type=code&redirect_uri=http://localhost:8081/login/oauth2/code/custom

Trình duyệt sẽ chuyển đến trang đăng nhập, sau đó người dùng đăng nhập. Sau khi đăng nhập thành công, chọn ủy quyền người dùng để lấy mã ủy quyền tương ứng, mã này sẽ được hiển thị trong URL chuyển hướng. Như hình ảnh dưới đây:

  1. Lấy code

  2. Xác thực người dùng

  3. ủy quyền

  4. Nhận được code

  5. Sử dụng giá trị code này để truy cập endpoint /oauth/token để lấy token, như hình ảnh dưới đây:

Mã code này sẽ không thể lấy được token nếu có lỗi, vì nó sẽ được lưu trong bộ nhớ của chương trình, và mã code này chỉ có thể lấy token một lần.

Chế độ mật khẩu

/oauth/token?client_id=my-client&client_secret=secret123&grant_type=password&username=user1&password=password123

Chế độ này rất đơn giản, nhưng cũng có nghĩa là trực tiếp tiết lộ thông tin nhạy cảm của người dùng cho phía client, vì vậy chế độ này thường chỉ được sử dụng khi client là do chính chúng ta phát triển.

Có hai cách viết client ở đây:

  1. Viết trực tiếp trong tham số yêu cầu
  2. Viết trong header yêu cầu

Thực tế, khi truyền trong header, định dạng sẽ là, thêm Authorization vào header, giá trị là Basic cộng với giá trị client_id:client_secret (được định nghĩa trong phương thức configure(ClientDetailsServiceConfigurer clients) của lớp AuthorizationServerConfigurer) được mã hóa base64 (http://tool.oschina.net/encrypt?type=3).

Chế độ client

/oauth/token?client_id=my-client&client_secret=secret123&grant_type=client_credentials

Thẻ: Spring Boot Spring Security OAuth2 Java Authorization Server

Đăng vào ngày 25 tháng 6 lúc 09:21