1. Background
In Spring Security 5
authorization server configuration is no longer provided, but the authorization server is still used in our usual development process. However, Spring officially provides a community-driven authorization service
spring-authorization-server
led by Spring. It has reached version 0.1.2. However, this project is still an experimental project and cannot be used in a production environment. Use the project here to build A simple authorization server.
2. Pre-knowledge
1. Understand the oauth2 protocol and process. can refer to this article by
2. Concepts of JWT, JWS, JWK
JWT:
refers to JSON Web Token, which is composed of header.payload.signture. A JWT without a signature is insecure, and a JWT with a signature cannot be tampered with.JWS:
refers to the signed JWT, that is, the signed JWT.JWK:
involves signature, it involves the signature algorithm, symmetric encryption or asymmetric encryption, then the encryption key or public-private key pair is required. Here we refer to the JWT key or public-private key pair as JSON WEB KEY, which is JWK.
3. Demand
1. Complete the authorization code ( authorization-code
) process.
The safest process requires the participation of users.
2. Complete the client ( client credentials
) process.
Without the participation of users, it can generally be used for access between internal systems, or there is no need for user participation between systems.
3. The simplified mode has been deprecated in the new spring-authorization-server project.
4. Refresh the token.
5. Withdraw the token.
6. View the issued token information.
7. View JWK information.
8. Personalize the JWT token, that is, add additional information to the JWT token.
completed case: Zhang San used the
QQ login method to log in to the
CSDN
website.
After logging in, CSDN can get the token
personal information of Zhang San on the QQ resource server with the token.
role analysis Zhang Three: The user is the resource owner
CSDN:
clientQQ:
authorization server personal information: the user's resources, stored in the resource server
Four, core code writing
1. Introduce authorization server dependency
<dependency>
<groupId>org.springframework.security.experimental</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.1.2</version>
</dependency>
2. Create an authorization server user
Zhang San used the
QQ login method to log in to the
CSDN
website.
Zhang San is completed here. This Zhang San is the user of the authorization server, and here is the user of the QQ server.
@EnableWebSecurity
public class DefaultSecurityConfig {
@Bean
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.formLogin();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
// 此处创建用户,张三。
@Bean
UserDetailsService users() {
UserDetails user = User.builder()
.username("zhangsan")
.password(passwordEncoder().encode("zhangsan123"))
.roles("USER")
.build();
return new InMemoryUserDetailsManager(user);
}
}
3. Create an authorization server and client
Zhang San used the
QQ login method to log in to the
CSDN
website.
The creation of the QQ authorization server and client CSDN is completed here.
package com.huan.study.authorization.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.beans.factory.annotation.Autowired;
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.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
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.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.ProviderSettings;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
/**
* 认证服务器配置
*
* @author huan.fu 2021/7/12 - 下午2:08
*/
@Configuration
public class AuthorizationConfig {
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 个性化 JWT token
*/
class CustomOAuth2TokenCustomizer implements OAuth2TokenCustomizer<JwtEncodingContext> {
@Override
public void customize(JwtEncodingContext context) {
// 添加一个自定义头
context.getHeaders().header("client-id", context.getRegisteredClient().getClientId());
}
}
/**
* 定义 Spring Security 的拦截器链
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 设置jwt token个性化
http.setSharedObject(OAuth2TokenCustomizer.class, new CustomOAuth2TokenCustomizer());
// 授权服务器配置
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
return http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer)
.and()
.formLogin()
.and()
.build();
}
/**
* 创建客户端信息,可以保存在内存和数据库,此处保存在数据库中
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// 客户端id 需要唯一
.clientId("csdn")
// 客户端密码
.clientSecret(passwordEncoder.encode("csdn123"))
// 可以基于 basic 的方式和授权服务器进行认证
.clientAuthenticationMethod(ClientAuthenticationMethod.BASIC)
// 授权码
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新token
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 客户端模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 密码模式
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
// 简化模式,已过时,不推荐
.authorizationGrantType(AuthorizationGrantType.IMPLICIT)
// 重定向url
.redirectUri("https://www.baidu.com")
// 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
.scope("user.userInfo")
.scope("user.photos")
.clientSettings(clientSettings -> {
// 是否需要用户确认一下客户端需要获取用户的哪些权限
// 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
clientSettings.requireUserConsent(true);
})
.tokenSettings(tokenSettings -> {
// accessToken 的有效期
tokenSettings.accessTokenTimeToLive(Duration.ofHours(1));
// refreshToken 的有效期
tokenSettings.refreshTokenTimeToLive(Duration.ofDays(3));
// 是否可重用刷新令牌
tokenSettings.reuseRefreshTokens(true);
})
.build();
JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
if (null == jdbcRegisteredClientRepository.findByClientId("csdn")) {
jdbcRegisteredClientRepository.save(registeredClient);
}
return jdbcRegisteredClientRepository;
}
/**
* 保存授权信息,授权服务器给我们颁发来token,那我们肯定需要保存吧,由这个服务来保存
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
class CustomOAuth2AuthorizationRowMapper extends JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper {
public CustomOAuth2AuthorizationRowMapper(RegisteredClientRepository registeredClientRepository) {
super(registeredClientRepository);
getObjectMapper().configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false);
this.setLobHandler(new DefaultLobHandler());
}
}
CustomOAuth2AuthorizationRowMapper oAuth2AuthorizationRowMapper =
new CustomOAuth2AuthorizationRowMapper(registeredClientRepository);
authorizationService.setAuthorizationRowMapper(oAuth2AuthorizationRowMapper);
return authorizationService;
}
/**
* 如果是授权码的流程,可能客户端申请了多个权限,比如:获取用户信息,修改用户信息,此Service处理的是用户给这个客户端哪些权限,比如只给获取用户信息的权限
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 对JWT进行签名的 加解密密钥
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws NoSuchAlgorithmException {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet);
}
/**
* jwt 解码
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* 配置一些断点的路径,比如:获取token、授权端点 等
*/
@Bean
public ProviderSettings providerSettings() {
return new ProviderSettings()
// 配置获取token的端点路径
.tokenEndpoint("/oauth2/token")
// 发布者的url地址,一般是本系统访问的根路径
// 此处的 qq.com 需要修改我们系统的 host 文件
.issuer("http://qq.com:8080");
}
}
Note⚠️:
1. It is necessary to map qq.com with 127.0.0.1 in the host file of the system.
2. Because the client information and authorization information (token information, etc.) are stored in the database, the table needs to be built.
3. For more information, see the comments of the code above
Five, test
As can be seen from the code above:
Resource owner: Zhang San User name and password are: zhangsan/zhangsan123
client information: CSDN clientId and clientSecret: csdn/csdn123
Authorization server address: qq.com
clientSecret
cannot be leaked to the client and must be stored on the server.
1. Authorization code process
1. Obtain the authorization code
http://qq.com:8080/oauth2/authorize?client_id=csdn&response_type=code&redirect_uri=https://www.baidu.com&scope=user.userInfo user.userInfo
client_id=csdn: indicates who the client is
response_type=code: indicates that the authorization code is returned
scope=user.userInfo user.userInfo: Get multiple permissions separated by spaces
redirect_uri= after the user agrees or rejects
2. Obtain the token according to the authorization code
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=authorization_code&code=tDrZ-LcQDG0julJBcGY5mjtXpE04mpmXjWr9vr0-rQFP7UuNFIP6kFArcYwYo4U-iZXFiDcK4p0wihS_iUv4CBnlYRt79QDoBBXMmQBBBm9jCblEJFHZS-WalCoob6aQ&redirect_uri=https%3A%2F%2Fwww.baidu.com'
Authorization: Base64 value carrying specific clientId and clientSecret
grant_type=authorization_code means that the method used is authorization code
code=xxx: the authorization code obtained in the previous step
3. Process demonstration
2. Obtain token according to refresh token
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=refresh_token&refresh_token=Wpu3ruj8FhI-T1pFmnRKfadOrhsHiH1JLkVg2CCFFYd7bYPN-jICwNtPgZIXi3jcWqR6FOOBYWo56W44B5vm374nvM8FcMzTZaywu-pz3EcHvFdFmLJrqAixtTQZvMzx'
3. Client mode
In this mode, there is no user's participation, only the participation between the client and the authorization server.
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/token?grant_type=client_credentials'
4. Withdraw token
curl -i -X POST \
'http://qq.com:8080/oauth2/revoke?token=令牌'
5. View token information
curl -i -X POST \
-H "Authorization:Basic Y3Nkbjpjc2RuMTIz" \
'http://qq.com:8080/oauth2/introspect?token=XXX'
6. View JWK information
curl -i -X GET \
'http://qq.com:8080/oauth2/jwks'
Six, complete code
https://gitee.com/huan1993/spring-cloud-parent/tree/master/security/authorization-server
Seven, reference address
1、https://github.com/spring-projects-experimental/spring-authorization-server
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。