Spring Authorization Server
上一篇文章中,我们简单快速上手了Spring Authorization Server,但是所有数据都是存储在内存中的,无法应用与生产环境。
这一次我们引入MySQL和Mybatis-Plus,将oauth-client和user数据持久化到数据库中,建立一个基本上生产可用的认证和授权服务器。
1.依赖管理
引入数据库相关的依赖后,完整的依赖如下:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.5</version>
<exclusions>
<exclusion>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-generator</artifactId>
<version>3.5.5</version>
</dependency>
<!--低版本的mybatis-spring不兼容-->
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>3.0.3</version>
</dependency>
<dependency>
<groupId>org.freemarker</groupId>
<artifactId>freemarker</artifactId>
<version>2.3.23</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
2.数据库设置
OAuth相关:
mysql
create database if not exists auth_server;
use auth_server;
DROP TABLE if exists oauth2_authorization_consent;
CREATE TABLE oauth2_authorization_consent
(
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorities varchar(1000) NOT NULL,
PRIMARY KEY (registered_client_id, principal_name)
);
/*
IMPORTANT:
If using PostgreSQL, update ALL columns defined with 'blob' to 'text',
as PostgreSQL does not support the 'blob' data type.
*/
DROP TABLE if exists oauth2_authorization;
CREATE TABLE oauth2_authorization
(
id varchar(100) NOT NULL,
registered_client_id varchar(100) NOT NULL,
principal_name varchar(200) NOT NULL,
authorization_grant_type varchar(100) NOT NULL,
authorized_scopes varchar(1000) DEFAULT NULL,
attributes blob DEFAULT NULL,
state varchar(500) DEFAULT NULL,
authorization_code_value blob DEFAULT NULL,
authorization_code_issued_at datetime DEFAULT 0,
authorization_code_expires_at datetime DEFAULT 0,
authorization_code_metadata blob DEFAULT NULL,
access_token_value blob DEFAULT NULL,
access_token_issued_at datetime DEFAULT 0,
access_token_expires_at datetime DEFAULT 0,
access_token_metadata blob DEFAULT NULL,
access_token_type varchar(100) DEFAULT NULL,
access_token_scopes varchar(1000) DEFAULT NULL,
oidc_id_token_value blob DEFAULT NULL,
oidc_id_token_issued_at datetime DEFAULT 0,
oidc_id_token_expires_at datetime DEFAULT 0,
oidc_id_token_metadata blob DEFAULT NULL,
refresh_token_value blob DEFAULT NULL,
refresh_token_issued_at datetime DEFAULT 0,
refresh_token_expires_at datetime DEFAULT 0,
refresh_token_metadata blob DEFAULT NULL,
user_code_value blob DEFAULT NULL,
user_code_issued_at datetime DEFAULT 0,
user_code_expires_at datetime DEFAULT 0,
user_code_metadata blob DEFAULT NULL,
device_code_value blob DEFAULT NULL,
device_code_issued_at datetime DEFAULT 0,
device_code_expires_at datetime DEFAULT 0,
device_code_metadata blob DEFAULT NULL,
PRIMARY KEY (id)
);
DROP TABLE if exists oauth2_registered_client;
CREATE TABLE oauth2_registered_client
(
id varchar(100) NOT NULL,
client_id varchar(100) NOT NULL,
client_id_issued_at datetime DEFAULT 0,
client_secret varchar(200) DEFAULT NULL,
client_secret_expires_at datetime DEFAULT 0,
client_name varchar(200) NOT NULL,
client_authentication_methods varchar(1000) NOT NULL,
authorization_grant_types varchar(1000) NOT NULL,
redirect_uris varchar(1000) DEFAULT NULL,
post_logout_redirect_uris varchar(1000) DEFAULT NULL,
scopes varchar(1000) NOT NULL,
client_settings varchar(2000) NOT NULL,
token_settings varchar(2000) NOT NULL,
PRIMARY KEY (id)
);
用户相关:
mysql
use auth_server;
create table auth_user
(
id int not null primary key auto_increment,
user_id varchar(50) not null default '',
username varchar(50) not null default '',
password varchar(100) not null default '',
mobile varchar(50) not null default ''
);
代码生成器:
java
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/8 11:20
*/
public class MybatisPlusGenerator {
public static void main(String[] args) {
String url = "jdbc:mysql://127.0.0.1:3306/auth_server?useSSL=false";
String user = "root";
String pass = "root";
String module = "auth-server";
FastAutoGenerator.create(url, user, pass)
.globalConfig(builder -> {
builder.author("hundanli")// 设置作者
.outputDir(System.getProperty("user.dir") + "/" + module + "/src/main/java"); // 指定输出目录
})
.dataSourceConfig(builder -> builder.typeConvertHandler((globalConfig, typeRegistry, metaInfo) -> {
int typeCode = metaInfo.getJdbcType().TYPE_CODE;
if (typeCode == Types.SMALLINT) {
// 自定义类型转换
return DbColumnType.INTEGER;
}
return typeRegistry.getColumnType(metaInfo);
}))
.packageConfig(builder -> {
builder.parent("com.hauth.auth.dao"); // 设置父包名
// .moduleName("auth-server") // 设置父包模块名
// .pathInfo(Collections.singletonMap(OutputFile.xml, System.getProperty("user.dir") + "/" + module + "/src/main/java")); // 设置mapperXml生成路径
})
.strategyConfig(builder -> {
builder.addInclude("auth_user"); // 设置需要生成的表名
})
.templateEngine(new FreemarkerTemplateEngine())
// 使用Freemarker引擎模板,默认的是Velocity引擎模板
.execute();
}
}
执行即可生成操作auth_user表的dao代码。
最后,还需要在application.properties文件加上数据库配置信息:
properties
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/auth_server?useSSL=false&serverTimezone=Asia/Shanghai&autoReconnect=true
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
auth.jdbc=true
3.授权配置
与上一篇类似,需要注入多个bean到Spring容器中,这里导入了jdbc相关的实现:
java
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/7 11:45
*/
@Configuration
@EnableWebSecurity
@ConditionalOnProperty(name = "auth.jdbc", havingValue = "true", matchIfMissing = false)
public class AuthorizationServerConfig {
/**
* 授权服务器 认证过滤器
*/
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(
HttpSecurity http) throws Exception {
// 配置默认的设置,忽略授权请求端点的csrf校验
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
// 开启OpenID Connect 1.0协议相关端点
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults());
// 当未登录时访问认证端点时重定向至login页面
http.exceptionHandling((exceptions) -> exceptions
.defaultAuthenticationEntryPointFor(
new LoginUrlAuthenticationEntryPoint("/login"),
new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
))
// 处理使用access token访问用户信息端点和客户端注册端点
.oauth2ResourceServer((resourceServer) -> resourceServer
.jwt(Customizer.withDefaults()));
return http.build();
}
// 注册用户使用,暂时忽略认证
@Bean
@Order(2)
public SecurityFilterChain customSecurityFilterChain(HttpSecurity http) throws Exception {
// whitelist
http.authorizeHttpRequests(authorize -> {
authorize.requestMatchers("/authUser/**").anonymous();
}).csrf(csrf -> {
csrf.ignoringRequestMatchers("/authUser/**");
}).formLogin(Customizer.withDefaults());
// disable cors
http.cors(AbstractHttpConfigurer::disable);
return http.build();
}
// spring security 认证过滤器,该bean必须放到最低优先级
@Bean
@Order(Ordered.LOWEST_PRECEDENCE - 1)
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();
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
// @formatter:off
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
// @formatter:on
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
// token加解密RSA密钥
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;
}
// token解密器
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
// authorization server 设置
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
// 密码加密器
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// oauth client存储服务
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate, PasswordEncoder passwordEncoder) {
RegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
initMallHelloClient(registeredClientRepository, passwordEncoder);
return registeredClientRepository;
}
// oauth授权服务
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
// 用户同意服务
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
// Will be used by the ConsentController
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
// access_token生成器
@Bean
OAuth2TokenGenerator<?> tokenGenerator(JWKSource<SecurityContext> jwkSource) {
JwtGenerator jwtGenerator = new JwtGenerator(new NimbusJwtEncoder(jwkSource));
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(
jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
// 注册测试client
private void initMallHelloClient(RegisteredClientRepository registeredClientRepository, PasswordEncoder passwordEncoder) {
String clientId = "hello";
String clientSecret = "123456";
String clientName = "Hello客户端";
String encodeSecret = passwordEncoder.encode(clientSecret);
RegisteredClient client = registeredClientRepository.findByClientId(clientId);
String id = client != null ? client.getId() : UUID.randomUUID().toString();
RegisteredClient registeredClient = RegisteredClient.withId(id)
.clientId(clientId)
.clientSecret(encodeSecret)
.clientName(clientName)
// 需要设置clientSecret过期时间,否则默认非常短回到只token接口401
.clientSecretExpiresAt(Instant.now().plus(Duration.ofDays(36500)))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 密码模式
.redirectUri("http://127.0.0.1:8080/authorized")
.postLogoutRedirectUri("http://127.0.0.1:8080/logged-out")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE) .tokenSettings(TokenSettings.builder().refreshTokenTimeToLive(Duration.ofDays(30)).accessTokenTimeToLive(Duration.ofDays(1)).build())
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
registeredClientRepository.save(registeredClient);
}
4.认证实现
这一步我们需要按照Spring Security的规范,注入一个UserDetailsService类型的Bean用于用户认证。
java
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/8 10:25
*/
@Service
@ConditionalOnProperty(value = "auth.jdbc", havingValue = "true")
public class JdbcUserAuthenticationService implements UserDetailsService {
@Autowired
private IAuthUserService authUserService;
@Autowired(required = false)
private PasswordEncoder passwordEncoder;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
LambdaQueryWrapper<AuthUser> queryWrapper = Wrappers.lambdaQuery();
queryWrapper.eq(AuthUser::getUsername, username);
queryWrapper.last("limit 1");
AuthUser authUser = authUserService.getOne(queryWrapper);
if (authUser == null) {
throw new UsernameNotFoundException("use not found: " + username);
}
return User.withUsername(username)
.password(authUser.getPassword())
.accountExpired(false)
.accountLocked(false)
.build();
}
}
这里将查询数据库获取用户,返回给Spring Security做认证。
5.用户注册
因为我们使用了密码加密存储,因此我写了一个用户注册的简单接口便于调试,也就是授权配置中忽略认证的接口:
java
/**
* @author hundanli
* @version 1.0.0
* @date 2024/3/8 15:20
*/
@RestController
@RequestMapping("authUser")
public class AuthUserController {
@Autowired
private IAuthUserService authUserService;
@Autowired(required = false)
private PasswordEncoder passwordEncoder;
@PostMapping("register")
public String register(@RequestParam("username") String username, @RequestParam("password") String password) {
String encodedPassword = passwordEncoder.encode(password);
AuthUser authUser = new AuthUser();
authUser.setUserId("" + System.currentTimeMillis()/1000);
authUser.setUsername(username);
authUser.setPassword(encodedPassword);
authUser.setMobile("");
authUserService.save(authUser);
return "ok";
}
}
6.授权测试
同样启动AuthServer和ClientServer两个服务,测试前先注册一个用户:
bash
curl -XPOST 'http://127.0.0.1:8000/authUser/register?username=hello&password=12
3456' -v
然后执行上一篇同样的流程测试授权流程:
2.此时会跳转到:http://127.0.0.1:8000/login 页面,输入hello/123456进行登录,AuthServer会返回302响应和code
3.浏览器将会自动进行302请求:http://127.0.0.1:8080/authorized?code=xxx
4.然后OAuth Client将会获取这个code调用AuthServer的/oauth2/token接口获取access_token。
5.最后再使用access_token调用AuthServer的/userinfo接口获取用户信息。
至此顺利完成数据库集成,完结撒花。