SpringSecurity Oauth2 - 密码模式完成身份认证获取令牌 [自定义UserDetailsService]

文章目录

    • [1. 授权服务器](#1. 授权服务器)
    • [2. 授权类型](#2. 授权类型)
      • [1. Password (密码模式)](#1. Password (密码模式))
      • [2. Refresh Token(刷新令牌)](#2. Refresh Token(刷新令牌))
      • [3. Client Credentials(客户端凭证模式)](#3. Client Credentials(客户端凭证模式))
    • [3. AuthorizationServerConfigurerAdapter](#3. AuthorizationServerConfigurerAdapter)
    • [4. 自定义 TokenStore 管理令牌](#4. 自定义 TokenStore 管理令牌)
      • [1. TokenStore 的作用](#1. TokenStore 的作用)
      • [2. CustomAuthenticationKeyGenerator](#2. CustomAuthenticationKeyGenerator)
      • [3. CustomRedisTokenStore](#3. CustomRedisTokenStore)
      • [4. TokenStoreAutoConfiguration](#4. TokenStoreAutoConfiguration)
    • [5. 自定义 UserDetailsService 获取认证用户信息](#5. 自定义 UserDetailsService 获取认证用户信息)
      • [1. UserDetailsService 的作用](#1. UserDetailsService 的作用)
      • [2. CustomUserDetailService](#2. CustomUserDetailService)
      • [3. 密码加密配置类 PasswordEncodeConfig](#3. 密码加密配置类 PasswordEncodeConfig)
      • [4. 配置 CustomUserDetailService](#4. 配置 CustomUserDetailService)
    • [6. 配置授权服务器](#6. 配置授权服务器)
    • [7. 启动项目测试获取访问令牌](#7. 启动项目测试获取访问令牌)

1. 授权服务器

Spring Security OAuth2 授权服务器的作用是为各种客户端应用(如Web应用、移动应用、微服务等)提供一个集中式的身份认证和授权服务。授权服务器的主要功能是颁发、管理和验证访问令牌(Access Token)和刷新令牌(Refresh Token),从而确保只有经过授权的客户端才能访问受保护的资源。

1.管理客户端应用

授权服务器允许你注册和管理不同的客户端应用程序(例如,Web应用、移动应用、API客户端)。对于每个客户端应用,授权服务器会为其分配一个唯一的客户端ID(Client ID)和客户端密钥(Client Secret),并定义其授权范围和访问权限。

2. 颁发访问令牌

授权服务器的核心功能之一是颁发访问令牌。当客户端应用请求访问受保护的资源时,它需要先向授权服务器请求一个访问令牌。授权服务器会根据预定义的授权流程(如授权码模式、密码模式等)验证客户端的身份和权限,然后颁发一个访问令牌给客户端。客户端可以使用这个访问令牌来访问资源服务器上的受保护资源。

3. 支持多种授权模式

① 密码模式:适用于用户信任的应用,如移动应用,直接使用用户名和密码获取访问令牌。

② 客户端凭据模式:适用于服务之间的通信,不涉及用户,直接使用客户端凭据获取访问令牌。

③ 刷新令牌:在访问令牌过期时,客户端可以使用刷新令牌获取新的访问令牌,避免用户重复登录。

4. 验证访问令牌

授权服务器不仅颁发访问令牌,还负责验证访问令牌的有效性。资源服务器在收到客户端请求时,可以通过调用授权服务器的令牌检查接口(如 /oauth/check_token)来验证访问令牌的合法性和有效期,从而确保请求者的身份和权限。

5. 管理用户身份和权限

授权服务器与用户身份管理系统(如用户数据库、LDAP等)集成,负责用户的认证和权限管理。当客户端应用请求访问令牌时,授权服务器会验证用户的身份,并根据用户的角色或权限,决定是否颁发访问令牌以及授予哪些权限。

2. 授权类型

OAuth2 授权框架提供了多种授权类型,允许客户端以不同的方式获取访问令牌。每种授权类型都有不同的使用场景和适用条件。

1. Password (密码模式)

密码模式适用于在信任的应用程序中直接向 OAuth2 授权服务器提供用户的用户名和密码。这种模式适用于移动应用或服务器端应用直接与授权服务器交互的场景。用户直接将用户名和密码提供给客户端,客户端使用这些凭据向授权服务器请求访问令牌。

使用步骤

① 用户提供用户名和密码: 用户将其用户名和密码输入到客户端应用中。

② 客户端请求访问令牌: 客户端使用用户名、密码、客户端ID和客户端密钥向授权服务器请求访问令牌。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
Authorization: Basic Base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=password&username=user&password=pass&scope=read

③ 服务器返回访问令牌: 授权服务器验证凭据后,返回访问令牌。

{
    "access_token": "abcdefg12345",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "refresh12345"
}

2. Refresh Token(刷新令牌)

刷新令牌用于获取新的访问令牌,而无需用户重新进行认证。这种模式通常与其他授权模式结合使用,例如密码模式或授权码模式。当访问令牌过期后,需要获取新的访问令牌时使用刷新令牌。适用于需要长期保持用户会话的应用,例如 Web 应用或移动应用。

使用步骤

① 客户端使用刷新令牌请求新访问令牌: 当访问令牌过期时,客户端使用刷新令牌向授权服务器请求新的访问令牌。

POST /oauth/token HTTP/1.1
Host: authorization-server.com
Authorization: Basic Base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=refresh_token&refresh_token=refresh12345

② 服务器返回新的访问令牌: 授权服务器验证刷新令牌后,返回新的访问令牌。

{
    "access_token": "newabcdefg12345",
    "token_type": "bearer",
    "expires_in": 3600,
    "refresh_token": "newrefresh12345"
}

3. Client Credentials(客户端凭证模式)

客户端凭证模式不涉及用户,客户端自身以其身份请求访问令牌。这种模式常用于服务端与服务端之间的通信,例如 API 网关与微服务之间的通信。适用于应用之间的服务调用,通常在后台系统中使用。用于访问与用户无关的资源,或使用应用本身的权限访问资源。

使用步骤

① 客户端请求访问令牌: 客户端使用自己的客户端ID和客户端密钥向授权服务器请求访问令牌。

bash复制代码POST /oauth/token HTTP/1.1
Host: authorization-server.com
Authorization: Basic Base64(client_id:client_secret)
Content-Type: application/x-www-form-urlencoded

grant_type=client_credentials&scope=read

② 服务器返回访问令牌: 授权服务器验证客户端凭证后,返回访问令牌。

{
    "access_token": "clienttoken12345",
    "token_type": "bearer",
    "expires_in": 3600
}

3. AuthorizationServerConfigurerAdapter

java 复制代码
public class AuthorizationServerConfigurerAdapter implements AuthorizationServerConfigurer {

	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
	}

}

1. configure(AuthorizationServerSecurityConfigurer security)

用于配置授权服务器的安全性,如 /oauth/token/oauth/authorize 等端点的安全性配置:

java 复制代码
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
    // 允许客户端表单身份验证
    security.allowFormAuthenticationForClients()
            // 允许所有人访问令牌验证端点
            .checkTokenAccess("permitAll()")
            // 仅允许认证后的用户访问密钥端点
            .tokenKeyAccess("isAuthenticated()");
}

2. configure(ClientDetailsServiceConfigurer clients) 方法

用于配置客户端详细信息服务,这个服务用来定义哪些客户端可以访问授权服务器以及客户端的配置信息。

java 复制代码
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
    // 将客户端信息存储在内存中
    clients.inMemory()
       		// 定义客户端 ID
           .withClient("client-id")
        	// 定义客户端密钥
           .secret(passwordEncoder.encode("client-secret"))
        	// 定义客户端支持的授权模式
           .authorizedGrantTypes("authorization_code", "password", "refresh_token", "client_credentials")
        	// 定义客户端的作用范围
           .scopes("read", "write")
        	// 设置访问令牌的有效期
           .accessTokenValiditySeconds(3600)
        	// 设置刷新令牌的有效期
           .refreshTokenValiditySeconds(7200);
}

3. configure(AuthorizationServerEndpointsConfigurer endpoints) 方法

用于配置授权和令牌的端点,以及令牌服务、令牌存储、用户认证等相关配置。

java 复制代码
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
    // 配置用于密码模式的 `AuthenticationManager`
    endpoints.authenticationManager(authenticationManager)
        	 // 在刷新令牌时使用此服务加载用户信息
             .userDetailsService(userDetailsService)
        	 // 配置令牌的存储策略,例如内存、数据库或 Redis
             .tokenStore(new InMemoryTokenStore());
}

4. 自定义 TokenStore 管理令牌

1. TokenStore 的作用

TokenStore 是 Spring Security OAuth2 中用于管理 OAuth2 令牌(Access Token 和 Refresh Token)的接口。它的主要作用是定义如何生成、存储、读取和删除令牌。TokenStore 的具体实现类决定了令牌的存储方式,例如存储在内存中、数据库中、Redis 中,或以 JWT 的形式进行编码。TokenStore 的主要作用:

① 生成和存储令牌:当客户端请求令牌时,TokenStore 负责生成访问令牌和刷新令牌,并将它们存储在指定的存储介质中(如内存、数据库、Redis 等)。

② 读取令牌:TokenStore 允许根据令牌的值查找和读取存储的令牌。这个功能在资源服务器或授权服务器验证令牌时非常重要。

③ 删除令牌:TokenStore 也提供了删除令牌的方法,例如在用户注销或令牌过期时,授权服务器可以删除对应的访问令牌和刷新令牌。

④ 管理令牌的生命周期:TokenStore 负责管理令牌的生命周期,包括过期时间、刷新操作等。它可以确保访问令牌和刷新令牌在其有效期内使用,并在适当的时候自动过期。

当客户端请求令牌时,授权服务器通过 TokenStore 生成并存储令牌。客户端在后续请求中携带令牌访问受保护的资源时,资源服务器通过 TokenStore 验证令牌的有效性。TokenStore 管理整个令牌生命周期,包括生成、存储、读取、刷新和删除等操作。

java 复制代码
@Bean
public TokenStore tokenStore(RedisConnectionFactory redisConnectionFactory) {
    return new RedisTokenStore(redisConnectionFactory);
}

2. CustomAuthenticationKeyGenerator

在 OAuth2 的认证过程中,DefaultAuthenticationKeyGenerator 通常用于生成唯一的认证键(AuthenticationKey),该键用于标识客户端的认证请求。默认情况下,DefaultAuthenticationKeyGenerator 会基于客户端的 ID、授权类型、作用域等信息生成一个哈希值,作为认证请求的唯一标识。

CustomAuthenticationKeyGenerator 类通过在原有的键生成逻辑上添加一个随机值 UUID.randomUUID().toString() 来确保每次调用时生成的键都是唯一的,即使所有其他参数都相同。这种方法增加了键的随机性,避免了重复的认证请求生成相同的键。

java 复制代码
public class CustomAuthenticationKeyGenerator extends DefaultAuthenticationKeyGenerator {

    private static final String RAND = "keyGeneratorRand";

    @Override
    protected String generateKey(Map<String, String> values) {
        // 加入一个随机的要素,保证每次调用时生成的们的hash都不一样
        values.put(RAND, UUID.randomUUID().toString());
        return super.generateKey(values);
    }
}

3. CustomRedisTokenStore

CustomRedisTokenStore 是一个自定义的令牌存储类,继承自 RedisTokenStoreRedisTokenStore 是 Spring Security OAuth2 提供的一个实现,用于将 OAuth2 的访问令牌和刷新令牌存储在 Redis 中。

java 复制代码
/**
 * 自定义的RedisTokenStore处理
 */
public class CustomRedisTokenStore extends RedisTokenStore {

    public CustomRedisTokenStore(RedisConnectionFactory connectionFactory) {
        super(connectionFactory);
    }

    // 从 Redis 中读取并返回对应的访问令牌
    @Override
    public OAuth2AccessToken readAccessToken(String tokenValue) {
        return super.readAccessToken(tokenValue);
    }

    // 从 Redis 中移除指定的访问令牌
    @Override
    public void removeAccessToken(OAuth2AccessToken accessToken) {
        super.removeAccessToken(accessToken);
    }

    // 使用刷新令牌来删除关联的访问令牌
    @Override
    public void removeAccessTokenUsingRefreshToken(OAuth2RefreshToken refreshToken) {
        super.removeAccessTokenUsingRefreshToken(refreshToken);
    }
}

4. TokenStoreAutoConfiguration

java 复制代码
@Configuration
public class TokenStoreAutoConfiguration {

    @Autowired
    private RedisConnectionFactory connectionFactory;

    @Bean
    public TokenStore tokenStore() {
        // 使用redis存储token
        RedisTokenStore redisTokenStore = new CustomRedisTokenStore(connectionFactory);
        redisTokenStore.setAuthenticationKeyGenerator(new CustomAuthenticationKeyGenerator());
        return redisTokenStore;
    }
}

5. 自定义 UserDetailsService 获取认证用户信息

1. UserDetailsService 的作用

UserDetailsService 是 Spring Security 中的一个核心接口,用于根据用户名获取用户的详细信息。这个接口通常用于处理用户身份验证过程中的用户查找逻辑。具体来说,当用户试图登录应用程序时,Spring Security 会使用 UserDetailsService 来加载用户信息(包括用户名、密码、权限等),以便进行身份验证。

java 复制代码
UserDetails loadUserByUsername(String username) throws UsernameNotFoundException;

UserDetailsUserDetailsService 返回的核心对象,包含了用户的详细信息:

java 复制代码
public interface UserDetails extends Serializable {
    // 返回用户的权限(角色)集合
    Collection<? extends GrantedAuthority> getAuthorities();
    // 返回用户的密码(通常是加密后的)
    String getPassword();
    // 返回用户的用户名
    String getUsername();
    // 指示账户是否未过期,未过期的账户可使用。
    boolean isAccountNonExpired();
    // 指示账户是否未锁定,未锁定的账户可使用。
    boolean isAccountNonLocked();
    // 指示用户的凭据是否未过期,未过期的凭据可使用。
    boolean isCredentialsNonExpired();
    // 指示用户是否已启用,已启用的用户可使用。
    boolean isEnabled();
}

2. CustomUserDetailService

java 复制代码
@Service
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    private UserDao userDao;

    @Autowired
    private PolicyDao policyDao;

    @Autowired
    private RoleDao roleDao;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        // 从数据库中查找用户
        UserEntity userEntity = userDao.queryUserByUserName(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        // 根据用户信息查询角色信息
        List<RoleEntity> roleEntities = roleDao.queryRolesByUserId(userEntity.getId());
        List<String> roleIds = roleEntities.stream().map(RoleEntity::getId).collect(Collectors.toList());
        // 根据角色信息查询权限信息
        List<PolicyEntity> policyEntities = policyDao.queryPolicyByRoleId(roleIds);
        // 查询权限名称
        List<String> policyNames = policyEntities.stream().map(PolicyEntity::getName).collect(Collectors.toList());

        // 构造认证用户权限信息
        List<SimpleGrantedAuthority> grantedAuthorities
                = policyNames.stream().map(policyName -> new SimpleGrantedAuthority(policyName)).collect(Collectors.toList());

        // 将 UserEntity 转换为 UserDetails 对象
        UserDetails userDetails = User.builder()
                .username(userEntity.getUsername())
                .password(userEntity.getPassword())
                .authorities(grantedAuthorities)
                .accountExpired(false)
                .accountLocked(false)
                .disabled(false)
                .build();
        return userDetails ;
    }
}

3. 密码加密配置类 PasswordEncodeConfig

java 复制代码
@Configuration
public class PasswordEncodeConfig {

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

4. 配置 CustomUserDetailService

java 复制代码
@Configuration
@EnableWebSecurity
public class WebSecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailService userDetailsService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(passwordEncoder);
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        super.configure(http);
    }

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

6. 配置授权服务器

java 复制代码
@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private TokenStore tokenStore;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        // 用于配置授权服务器的安全性,如 /oauth/token、/oauth/authorize 等端点的安全性配置。
        // 允许客户端表单身份验证
       security.allowFormAuthenticationForClients()
               // 允许所有人访问令牌验证端点
               .checkTokenAccess("permitAll()")
               // 仅允许认证后的用户访问密钥端点
               .tokenKeyAccess("isAuthenticated");
    }

    /**
     * 对于每个客户端应用,授权服务器会为其分配一个唯一的客户端ID和客户端密钥,并定义其授权范围和访问权限。
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // 用于配置客户端详细信息服务,这个服务用来定义哪些客户端可以访问授权服务器以及客户端的配置信息。
        // 将客户端信息存储在内存中,适合开发和测试环境。
        clients.inMemory()
                // 定义客户端ID
                .withClient("client_id")
                // 定义客户端密钥
                .secret(passwordEncoder.encode("client_secret"))
                // 定义客户端支持的授权模式。
                .authorizedGrantTypes("password","refresh_token","client_credentials")
                // 设置访问令牌的有效期。
                .accessTokenValiditySeconds(3600)
                // 设置刷新令牌的有效期。
                .refreshTokenValiditySeconds(7200)
                // 定义客户端的作用范围。
                .scopes("all");
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        // 用于配置授权和令牌的端点,以及令牌服务、令牌存储、用户认证等相关配置。
        // 配置用于密码模式的 AuthenticationManager。
        endpoints.authenticationManager(authenticationManager)
                // 在刷新令牌时使用此服务加载用户信息。
                .userDetailsService(userDetailsService)
                // 配置令牌的存储策略,例如内存、数据库或 Redis。
                .tokenStore(tokenStore);
    }
}

7. 启动项目测试获取访问令牌

相关推荐
运维Z叔21 分钟前
云安全 | AWS S3存储桶安全设计缺陷分析
android·网络·网络协议·tcp/ip·安全·云计算·aws
阿华的代码王国1 小时前
MySQL ------- 索引(B树B+树)
数据库·mysql
Hello.Reader1 小时前
StarRocks实时分析数据库的基础与应用
大数据·数据库
执键行天涯1 小时前
【经验帖】JAVA中同方法,两次调用Mybatis,一次更新,一次查询,同一事务,第一次修改对第二次的可见性如何
java·数据库·mybatis
liupenglove2 小时前
golang操作mysql利器-gorm
mysql·golang
yanglamei19622 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
叫我:松哥2 小时前
基于Python flask的医院管理学院,医生能够增加/删除/修改/删除病人的数据信息,有可视化分析
javascript·后端·python·mysql·信息可视化·flask·bootstrap
Reese_Cool2 小时前
【C语言二级考试】循环结构设计
android·java·c语言·开发语言
工作中的程序员2 小时前
ES 索引或索引模板
大数据·数据库·elasticsearch
严格格2 小时前
三范式,面试重点
数据库·面试·职场和发展