OAuth协议
随着互联网的流行,对于提高网民的上网提验,是大厂比较关心的问题。而OAuth协议就是为此而生的。
OAuth协议经过了1.0时代,现在处于2.0时代。确切的说OAuth协议现在是2.1版本。
OAuth2.1相较于OAuth2.0在安全方面和易用方面提高了不少。主要思想是差不多的。但OAuth2和OAuth1是不兼容的。应该说OAuth1.0版本已经被废弃掉了。
早期的Spring团队在Spring Security框架中加入了OAuth2.0模块,但现在已经不在维护了。我之前的公司竟然还有人在用他们快照版。如果没有Spring Security,对于一个java开发人员,再找一个好用+牛逼+安心+oauth2的安全框架,还真有点难。
好消息就是Spring推出了Spring Authorization Server(授权服务),它与Spring Security是两个安全项目。Spring Security相当于一个安全框架思想,有完整的“认证”和“授权”框架思路。Spring Authorization Server(授权服务)要基于Spring Security之上使用。
Spring Authorization Server
可以看到Spring Authorization Server是一个比较新的项目。
Spring Authorization Server提供OAuth2.1和OpenID Connect 1.0 规范 和 其它相关的安全规范。是一个轻量级框架,开发人员可以在框架中做自己的定义化功能。
最新版本的Spring Boot 已经做了版本匹配。作者当前用的SpringBoot是3.3.0的,算是比较新的版本,在这个版本的依赖列表中,Spring Authorization Server的版本是1.3.0
好了,费话不多说。
OAuth服务
一般搭建分两个服务:认证服务,授权服务
认证服务主要用于给资源服务器用,当有客户端请求资源时,认证服务会进行认证客户端是否有资格获取资源。
授权服务主要用于给客户端授权,只有先获取权限才能请求资源,通俗点讲就是让你登录,给你发token
第一步先搭建授权服务器:
配置:Maven,SpringBoot3.3.0,Spring-Security-oauth2-authorization-server 1.3.0(这个在pom中是用starter代替掉了),最后是JDK17,当然jdk版本不是必须的,其它版本也是可以的,只要别太低。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.3.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.oauth.server.demo</groupId>
<artifactId>oauth_server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth_server</name>
<description>OAuth2 Server Demo project for Spring Boot</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<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-web</artifactId>
</dependency>
</dependencies>
整体代码结构:
史上代码量最少的OAuth2授权服务器,核心代码只需要一个类:AuthorizationServerConfig
这个类里包含了所有相关授权服务器的构建代码。其它的类是其它功能,和授权服务没直接关系。
下面大家一起看一下类的内容:
package com.oauth.server.demo.config;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
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.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.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
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.settings.AuthorizationServerSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
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;
@Configuration
@Import(OAuth2AuthorizationServerConfiguration.class)
public class AuthorizationServerConfig {
@Bean
public PasswordEncoder passwordEncoder() throws NoSuchAlgorithmException {
return new BCryptPasswordEncoder(12,SecureRandom.getInstanceStrong());
}
/**
* 提供登录页面用户名密码的认证
* @return
*/
@Bean
public UserDetailsService userDetailsService(PasswordEncoder passwdEncoder) {
UserDetails user= User.builder()
.username("user")
//.password(passwdEncoder.encode("123")) // 使用 {noop} 前缀表示密码不会被编码
.password("{noop}123") // 使用 {noop} 前缀表示密码不会被编码
.accountExpired(false)
.credentialsExpired(false)
.accountLocked(false)
.authorities("ROLE_USER") // 用户的权限
.build();
return new InMemoryUserDetailsManager(user);
}
/**
* 应用注册仓库
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository(PasswordEncoder passwdEncoder) {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("clientid")
.clientSecret(passwdEncoder.encode("client_secret"))如果是CLIENT_SECRET_POST才会用到
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
.clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://localhost:8080/login/oauth2/code/clientid")
.postLogoutRedirectUri("http://localhost:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
/**
* 生成jwk,用在jwt编码和jwt解码器上
* @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);
}
/**
* 生成RSA256非对称的秘钥对:公钥和私钥,其中公钥会出布出去。
* @return
*/
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;
}
/**
* jwt 解码器,给资源服务器用
* @param jwkSource
* @return
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/**
* jwt 编码器,给授权服务器用
* @param jwkSource
* @return
*/
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
/**
* 默认授权服务器配置
* @return
*/
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
}
这样一个简单的授权服务器就诞生了。但想要用的话,还需要有用的地方。比如说授权服务现在是可以给你授权,但你拿到授权后,请求哪些资源呢?
所以现在咱们要建一个简单的资源:UserController
package com.oauth.server.demo.web;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class UserController {
@GetMapping
public String hello() {
return "hello";
}
}
这个接口简单到令人发指!
这样一个接口,咱们得让它被资源服务器管理,当它被资源服务器管理后,客户端就必须按照资源服务器的规定请求接口。按什么规定呢? 当然就是 oauth2 的规定了,要有 jwt token喽!
第二步构建资源服务器
package com.oauth.server.demo.config;
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.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
@Order(1)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize.anyRequest().authenticated())
.oauth2ResourceServer(resourceServer -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
}
这么几行代码就完事儿了,关键代码就是oauth2ResourceServer方法,它会检查token是否有效。
这里非常确定的告诉大家,资源服务器的代码和授权服务代码是写在一个项目中的。因为是第一个demo,不想讲太多费话。写一个项目中比较方便。它们都用到同一个jwk bean对象。
同时大家可能注意到了,这个资源服务器拦截所有请求,所以想要请求hello接口,就必须获取token !
代码Git地址:Demo_01
下一篇文章讲一下怎么获取token,并请求接口。