一、版本使用
1、Java:17或者更高的版本。
2、springboot 3.0
3、Spring Authorization Server 1.0版本。
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.0.0</version>
</dependency>
二、OAuth2涉及的参与者
- RO (resource owner): 资源所有者,对资源具有授权能力的人。也就是登录用户。
- RS (resource server): 资源服务器,它存储资源,并处理对资源的访问请求。
- Client: 第三方应用,它获得RO的授权后便可以去访问RO的资源。
- AS (authorization server): 授权服务器,它认证RO的身份,为RO提供授权审批流程,并最终颁发授权令牌(Access Token)。
注意:为了便于协议的描述,这里只是在逻辑上把AS与RS区分开来;在物理上,AS与RS的功能可以由同一个服务器来提供服务。
三、授权服务器(authorization server)
授权服务器授权的类型有三种:授权码模式、客户端模式、刷新模式。
1、授权码模式(authorization_code)
步骤:
(A)用户访问客户端,后者将前者导向认证服务器。
(B)用户选择是否给予客户端授权。
(C)假设用户给予授权,认证服务器将用户导向客户端事先指定的"重定向URI"(redirection URI),同时附上一个授权码。
(D)客户端收到授权码,附上早先的"重定向URI",向认证服务器申请令牌。这一步是在客户端的后台的服务器上完成的,对用户不可见。
(E)认证服务器核对了授权码和重定向URI,确认无误后,向客户端发送访问令牌(access token)和更新令牌(refresh token)。
参数说明:
A步骤中,客户端申请认证的URI,包含以下参数:
- response_type:表示授权类型,必选项,此处的值固定为"code"
- client_id:表示客户端的ID,必选项
- redirect_uri:表示重定向URI,可选项
- scope:表示申请的权限范围,可选项
- state:表示客户端的当前状态,可以指定任意值,认证服务器会原封不动地返回这个值。
示例:
http://localhost:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8080/authorized
通过示例请求出现一个登录界面:
输入你配置的账号及密码,若是账号密码没有问题,则会返回授权码既是路径中出现code="ssssssss"。若是出现输入账号密码都没有问题,却返回账号密码相关的问题(No AuthenticationProvider found for org.springframework.security.authentication.UsernamePasswordAuthenticationToken)
此时需要查看你密码加密问题,后面配置编码器的时候会提一下。
若是路径没有返回code,则去数据库(oauth2_authorization)看看有木有出现授权码(authorization_code_value),若有直接复制。
接着根据授权码获取token,如:
此处需要注意:需要增加客户端的配置信息(clientId、clientSecret)
最后获取:
2、客户端模式(client_credentials)
客户端模式指客户端以自己的名义,而不是以用户的名义,向服务提供商进行认证。
接着输入客户端配置信息:
最后得到:
注意:客户端模式无刷新token操作。
3、刷新模式(refresh_token)
接着添加客户端配置信息:
最后得到:
4、授权服务器配置
AuthorizationServerConfig配置:
package com.allen.demoserver.config;
import com.allen.component.redis.repository.RedisRepository;
import com.allen.demoserver.service.RedisToJdbcService;
import com.allen.demoserver.service.impl.RedisOAuth2AuthorizationConsentService;
import com.allen.demoserver.service.impl.RedisOAuth2AuthorizationService;
import com.allen.demoserver.service.impl.RedisToJdbcServiceImpl;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
)
// Accept access tokens for User Info and/or Client Registration
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
/**
* oauth2 用于第三方认证,RegisteredClientRepository 主要用于管理第三方(每个第三方就是一个客户端)
*
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("demo-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/demo-client-oidc")
.redirectUri("http://127.0.0.1:8080/authorized")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("message.read")
.scope("message.write")
.clientSettings(ClientSettings.builder()
//client端请求 jwt解密算法
.jwkSetUrl("http://localhost:9090/oauth2/jwks")
.requireAuthorizationConsent(true)
.build())
.tokenSettings(TokenSettings.builder()
//使用透明方式,默认是 OAuth2TokenFormat SELF_CONTAINED,生成 JWT 加密方式
// SELF_CONTAINED 生成 JWT 加密方式
// .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256)
// .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
.authorizationCodeTimeToLive(Duration.ofHours(1))
.accessTokenTimeToLive(Duration.ofHours(2))
.refreshTokenTimeToLive(Duration.ofHours(3))
.reuseRefreshTokens(false)
.build())
.build();
// Save registered client in db as if in-memory
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
if (null == registeredClientRepository.findByClientId("demo-client")){
registeredClientRepository.save(registeredClient);
}
return registeredClientRepository;
}
// @Bean
// public RedisToJdbcService redisToJdbcService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// return new RedisToJdbcServiceImpl(jdbcTemplate, registeredClientRepository);
// }
// @Bean
// public OAuth2AuthorizationService authorizationService(RegisteredClientRepository registeredClientRepository,RedisRepository redisRepository
// ,RedisToJdbcService redisToJdbcService) {
// return new RedisOAuth2AuthorizationService(registeredClientRepository,redisRepository,redisToJdbcService);
// }
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
// @Bean
// public OAuth2AuthorizationConsentService authorizationConsentService(RedisRepository redisRepository, RedisToJdbcService redisToJdbcService) {
// return new RedisOAuth2AuthorizationConsentService(redisRepository,redisToJdbcService);
// }
/**
* 用于给access_token签名使用。
*
* @return
*/
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (Exception ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置Authorization Server实例
* @return
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
配置说明: authorizationServerSecurityFilterChain :关于Protocol Endpoints.(协议端点)配置registeredClientRepository:关于客户端的一些配置 authorizationService:关于OAuth2Authorization 授权信息的处理 authorizationConsentService:关于OAuth2AuthorizationConsent信息的处理(入库)jwkSource()、generateRsaKey()、jwtDecoder:关于token生成规则的处理authorizationServerSettings:关于AuthorizationServerSettings【授权服务器】的配置,含路径及接口。
注意:authorizationService、authorizationConsentService 目前框架提供InMemoryOAuth2AuthorizationService(内存)、JdbcOAuth2AuthorizationService(数据库)。若需要实现Redis存储,需要自己去实现OAuth2AuthorizationService接口。关于redis的实现,可以项目看源码。
DefaultSecurityConfig配置:
package com.allen.demoserver.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
/**
* @author xuguocai
* @date 2022/12/13 21:55
**/
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class DefaultSecurityConfig {
/**
* 这个也是个Spring Security的过滤器链,用于Spring Security的身份认证。
* @param http
* @return
* @throws Exception
*/
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin(Customizer.withDefaults());
return http.build();
}
/**
* 配置用户信息,或者配置用户数据来源,主要用于用户的检索。
* @return
*/
@Bean
UserDetailsService users() {
UserDetails user = User.withDefaultPasswordEncoder()
.username("user1")
.password("123456")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
public static void main(String[] args) {
BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
String encode = bCryptPasswordEncoder.encode("123456");
System.out.println(encode);
}
}
配置说明:
defaultSecurityFilterChain:关于过滤器链的验证配置。 users():关于UserDetailsService 的实现,此处密码基于源码默认PasswordEncoder处理。
注意:基于代码学习,可以使用默认的编码器,既是如上配置方式。若是接口的实现,需要注意引入的编码器对密码的加密。若是配置的编码器出现问题,会影响授权码获取code。
package com.allen.demoserver.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.password.PasswordEncoder;
/**
* @author xuguocai
* @date 2022/11/24 17:06
**/
@Slf4j
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserClient userClient;
// 需要注入 new BCryptPasswordEncoder(),既是自己定义这个bean
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 基于用户名获取数据库中的用户信息
* @param username 这个username来自客户端
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
log.info("loadUserByUsername {}",username);
//基于feign方式获取远程数据并封装
//1.基于用户名获取用户信息
UserVO data = userClient.selectUserByUserName(username);
if(data==null) {
throw new UsernameNotFoundException("用户不存在");
}
//2.基于用于id查询用户权限
String password = passwordEncoder.encode(data.getPassword());
// log.info("数据:{}",data);
//3.对查询结果进行封装并返回
return new User(username, password,
AuthorityUtils.commaSeparatedStringToAuthorityList("ROLE_ADMIN"));
//返回给认证中心,认证中心会基于用户输入的密码以及数据库的密码做一个比对
}
}
在配置中,需要注入上面的bean:
此处需要注意编码器的注入,配置不当会影响授权码的账号密码输入,获取不到code值。
至此,授权服务器配置完毕。需要拓展的根据自己的需求拓展。
四、资源服务器(resource-server)
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
此处不再描述,可以查看oauth2-resource 项目,关注它的配置类、配置文件,其次再关注获取全局用户方式。
获取全局用户:它是根据jwt token获取,传的token经过过滤器链,既是【securityFilterChain(HttpSecurity http)】配置,最后从token中获取用户信息。
参考地址:
Spring Authorization Server 官网
Spring Authorization Server 官网示例
OAuth 2.0之授权码模式
gc-oauth2 项目源码示例