SpringBoot+SpringSecurity OAuth2 认证服务搭建实战 (一)

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,并请求接口。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

觉自性本然

您的鼓励与支持是我最大的动力~

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值