需要有一定的OAuth2的基础
需要有一定的Spring Security基础
Spring Authorization Server
官方简介:Spring Authorization Server is a framework that provides implementations of the OAuth 2.1 and OpenID Connect 1.0
个人理解为OAuth 2.1 and OpenID Connect 1.0的实现。
关于OAuth2有很多的版本,Spring Authorization Server是现在官网正在维护的版本。这里实现的是OAuth2.1,OAuth2.1里面没有密码模式。
这里用的版本是0.4.2,jdk8的,1.x的版本需要jdk17
这里可以用很简单的代码就实现OAuth2服务端
代码结构如下:
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-core</artifactId>
</dependency>
applicalication.yml
就配了一个端口
server:
port: 7000
logging:
level:
org.springframework.security: debug
启动类, 没啥特别的
@SpringBootApplication
public class Oauth2ServerApplication {
public static void main(String[] args) {
SpringApplication.run(Oauth2ServerApplication.class, args);
}
}
private.key:私钥
MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDBKxqzuZocGA9bhUsYCCfsl5cH
6omJ9I554J50tlsDl7AQLf+wX7YYWPdJa2V8o1y55wBqYiFE4JQ4H0sQog8itRS+OqwDKjwbedKo
5KTfUzvGjcFL77CWpU844n8e5A5e1oJbAPYPR/6ccHsPUgSxTAJMabaH3i5PTZ3aXiSjsMuXcg5/
HCKONUopgsb8idWSIvfVf2jM7wYFF5oRux4nlOzOCedQoEOSZQa55DElrFV/nVmi7ExIXH7GVG79
rIq0sDMqViQ6MVuXuFmyqAKd5pGA0f8+6echooKPKZklHGnHDMWmJDlCyoVnJSTti3yEhHaf0lfn
Ao/8qMU+iNX7AgMBAAECggEAZLQKCaQ6+WZ5qybETVUDK06kCBZ3eZorJNK7CPGAZVEREn5IjDR5
hBvtXzNEB0RLNQd+qfdajMPfwZpe0d8KsPdiRwHjZwr/pvtNnYsFgP+tbAe+u83La93mfStnRj1y
WHLQJo1Lug+4Zuok3YnOtHeBw0BhTlfAIMu//XWS+FprVxuBwIB6xvLTtGf5CV230y4DNmemFBuX
93qLFDSG3r/iEZ6U78CzjHjitG0kMfPSQ2TmraUJoel01t9JOONGqtsoitaWR7J5NcwLtrHqYXD5
R9d6E7i5e45Xf8HbrNVF0mgw+JQLkmhcTqWHo0Ws634v1r2HBa0HhYs/s2S3MQKBgQD7C7d43WVg
TprhoU/C6oWQOEAZSdSC8qsqg8CaiO0sv4YMGN7dxOWwuuX3OeXB8EjaL//kjnVyyueb4GF6ugMC
kDmoPg8PFOMP3VOs/0PoXQowC57cWMzzcSVepaHr6GDGl5kPZnhx2TLaPKtTRucPz6/+kWkTQCjz
3kOswTjJzQKBgQDE+v2y5gv4/s7iGfeaGMUB7IWn1G51Ay2pwoI2b60tITcbrhM0t4yxWAKUspoC
x3Qk5QNrUfevyvggBbPk0BL5Zy42HoBKGlh+hwcq8wmzdOGD7wcIvWvITceUxLrjD3/HogRrB6l4
VGsQ4Yq4e+iIc3geC6TDVNz1KMJbaR+25wKBgBOCXJa69dbfJPAl3hHyscB8bpbIgwhOHXknVf9s
ZqoUlDE6eY9YbtUmIRruV+mTZ8X09vjnDT+HfypA7LJh5Dv9w01MzVTJtb+U3pzSFY/oMxN6w7Sx
/fNpNpM9YfD4VRT50P4+Y1vNmkMVdeb52pkC9dVdrYG+ebBB9JZnSad9AoGBAMJoWQU8iGp5yWNb
b4S1l5Jbhlnqjg2MUn/uCaeCNq+IzaPS/P+VfBT3oKxzTQ8bHOTg5awA3Oyx7ItmNXLJbUCa9f/R
wJniQJ6303ovHc7wtzYILbARixPIuAZ611wLyvgTTjr39+lbn8OsZcXH/OrW06ELqtRhqCWJ0bB4
IyXXAoGBAMuMNmblY+iGfShJHWyAYESlFqyhYycfFXpec9Admf1znQ1SIo5VRxaDkMsGZImb5pPD
ZQUxNR5kJNr8rqYt21+kwNn0bteROkjQuP50P3dPcfaZfaYoKSSVEs4bnhzvtWCiHkCqf9jWHV+U
XFkhyPqfh2BZkghtU3MshbEX5IdG
public.key公钥
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwSsas7maHBgPW4VLGAgn7JeXB+qJifSO
eeCedLZbA5ewEC3/sF+2GFj3SWtlfKNcuecAamIhROCUOB9LEKIPIrUUvjqsAyo8G3nSqOSk31M7
xo3BS++wlqVPOOJ/HuQOXtaCWwD2D0f+nHB7D1IEsUwCTGm2h94uT02d2l4ko7DLl3IOfxwijjVK
KYLG/InVkiL31X9ozO8GBReaEbseJ5TszgnnUKBDkmUGueQxJaxVf51ZouxMSFx+xlRu/ayKtLAz
KlYkOjFbl7hZsqgCneaRgNH/PunnIaKCjymZJRxpxwzFpiQ5QsqFZyUk7Yt8hIR2n9JX5wKP/KjF
PojV+wIDAQAB
这个秘钥对在官方示例里面是生成并保存在内存里面的,每次都不一样,这个秘钥对是我自己生成的,保存在文件里面的,你也可以用官方的生成秘钥的那段代码。
读取秘钥
import cn.hutool.core.codec.Base64;
import cn.hutool.core.io.IoUtil;
import java.io.IOException;
import java.io.InputStream;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
/**
* TODO description
*
* @author qiudw
* @date 5/18/2023
*/
public class SecurityUtils {
public static PublicKey loadPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
try (InputStream inputStream = SecurityUtils.class.getClassLoader().getResourceAsStream("public.key")) {
assert inputStream != null;
byte[] publicKey = IoUtil.readBytes(inputStream);
KeyFactory keyfactory = KeyFactory.getInstance("RSA");
X509EncodedKeySpec encodeRule = new X509EncodedKeySpec(Base64.decode(publicKey));
return keyfactory.generatePublic(encodeRule);
}
}
public static PrivateKey loadPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
try (InputStream inputStream = SecurityUtils.class.getClassLoader().getResourceAsStream("private.key")) {
assert inputStream != null;
byte[] privateKey = IoUtil.readBytes(inputStream);
KeyFactory keyfactory = KeyFactory.getInstance("RSA");
PKCS8EncodedKeySpec encodeRule = new PKCS8EncodedKeySpec(Base64.decode(privateKey));
return keyfactory.generatePrivate(encodeRule);
}
}
}
LoginController
定义了一下登录页面,默认的登录页面需要加在国外的资源,太慢
/**
* 登录相关的控制器
*
* @author qiudw
* @date 7/11/2023
*/
@Controller
@RequestMapping
public class LoginController {
/**
* 跳转到登录页面
*
* @return 登录页面的地址
*/
@GetMapping("/login")
public ModelAndView loginPage() {
return new ModelAndView("login");
}
/**
* 首页
*
* @return 首页路径
*/
@GetMapping({ "/", "/index" })
public ModelAndView index() {
return new ModelAndView("index");
}
}
登录界面 login.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
<div>
异常信息:<span th:text="${session.SPRING_SECURITY_LAST_EXCEPTION}"></span>
</div>
<input type="text" name="username" placeholder="用户名" /> <br/>
<input type="password" name="password" placeholder="密码" /> <br/>
<button type="submit">登录</button> <br/>
</form>
</body>
</html>
安全配置
import com.demo.oauth2.utils.SecurityUtils;
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.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
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.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.jwt.JwtDecoder;
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.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.TokenSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.io.IOException;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
/**
* 安全配置
*
* @author qiudw
* @date 7/10/2023
*/
@Configuration
@EnableWebSecurity(debug = true)
public class SecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
// Enable OpenID Connect 1.0
.oidc(Customizer.withDefaults());
http.exceptionHandling()
.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login"))
.and()
.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.loginProcessingUrl("/login")
.usernameParameter("username")
.passwordParameter("password")
.and()
.csrf().disable();
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder()
.username("admin")
.password("{noop}admin")
.roles("ADMIN")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public JWKSource<SecurityContext> jwkSource() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
RSAPublicKey publicKey = (RSAPublicKey) SecurityUtils.loadPublicKey();
RSAPrivateKey privateKey = (RSAPrivateKey) SecurityUtils.loadPrivateKey();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID("key-id")
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwordEncoder) {
RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("demo-client")
.clientIdIssuedAt(Instant.now())
.clientSecret("{noop}demo-secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_JWT)
.clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.PASSWORD)
.redirectUri("https://baidu.com")
.redirectUri("http://127.0.0.1:7100/login/oauth2/code/messaging-client-oidc")
.redirectUri("http://127.0.0.1:7100/login/oauth2/code/demo-client-name")
.tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofHours(1L)).build())
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope(OidcScopes.EMAIL)
.scope(OidcScopes.ADDRESS)
.scope(OidcScopes.PHONE)
.scope("client.create")
.scope("client.read")
// 不需要跳转到授权页面
// .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.build();
return new InMemoryRegisteredClientRepository(registeredClient);
}
}
这里客户端、用户都是保存在内存里面的,为了方便演示。
# client_id: demo-client
# client_secret: demo-secret
# base64: ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXQ=
# 在线base64: https://c.runoob.com/front-end/693/
### 查看oauth配置
GET {{baseUrl}}/.well-known/oauth-authorization-server
### 查看OpenID的配置
GET {{baseUrl}}/.well-known/openid-configuration
### jwks
GET {{baseUrl}}/oauth2/jwks
### 浏览器模式 - 浏览器
http://127.0.0.1:7000/oauth2/authorize?response_type=code&client_id=demo-client&redirect_uri=https://baidu.com&scope=openid client.read client.create
### 浏览器模式(方式一) - 授权码换取token
POST {{baseUrl}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXQ=
grant_type=authorization_code&redirect_uri=https://baidu.com&code=OHd9nQj4ykR3KqGSFk14dbCRx7ifO4Vu2R_SO5CEKvqFjd3FkKJRkfpW5IuLmj0gg8PXFc3oShRtTYqAs_Tzk9SaBnwGUFqvnqmFSljuawZKwjlVcmXflGRF0PJs3Q7B
### 浏览器模式(方式二) - 授权码换取token
POST {{baseUrl}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&redirect_uri=https://baidu.com&client_id=demo-client&client_secret=demo-secret&code=XmdmrulYday-0sxw0A1_5VBrWekozvFMzLECyG6rBV7G348Py453YOguQ5VKOilD4q2ihlEL_7_2fuKuatl5HaKf9YyTjr_3yHiiRlyGmsFO3Lf-rKixY2FoZ5rQnPSS
### 获取用户
GET {{baseUrl}}/userinfo
Authorization: Bearer {{accessToken}}
### 客户端模式
POST {{baseUrl}}/oauth2/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXQ=
grant_type=client_credentials&scope=openid
### 注销令牌
POST {{baseUrl}}/oauth2/revoke
Content-Type: application/x-www-form-urlencoded
Authorization: Basic ZGVtby1jbGllbnQ6ZGVtby1zZWNyZXQ=
token_type_hint=refresh_token&token=klA-i51s2hVbankpkg9kRh9jk1EIFJnwHhdAYHbS7LoW9YBwMRvqSShOL_8h_LgunSytWh-08JveyLedHfAUD8ovrTjled6i7HYIgwKQUSP18zUTCThs-HV8AeboTtfX
idea里面可以直接发送请求