Spring Authorization Server
由于Spring Security OAuth2.0 被弃用,并且宣布Security将不再提供授权服务器, 但是springcloudoauth2.0还没有标记,经过社区的反馈,提供了一个由Spring官方主导,社区驱动的授权服务
spring-authorization-server
,目前版本是 0.2.1,目前没有太多的文档只能参考提供的示例SpirngAuthorizationServer 时基于 OAuth2.1 协议的 其中弃用了OAuth2.0 的用户名密码模式,并且支持 OpenID Connect 1.0 ,目前的阶段是正在向推进对 OpenID Connect 1.0 的支持
-
OAuth2.0 的概念
-
JWT、JWS、JWK 的概念
JWT:
指的是 JSON Web Token,由 header.payload.signture 组成。不存在签名的JWT是不安全的,存在签名的JWT是不可窜改的。
JWS:
指的是签过名的JWT,即拥有签名的JWT。
JWK:
既然涉及到签名,就涉及到签名算法,对称加密还是非对称加密,那么就需要加密的 密钥或者公私钥对。此处我们将 JWT的密钥或者公私钥对统一称为 JSON WEB KEY,即 JWK。
代码地址: https://e.coding.net/duangouyu/demo/SpringAuthorizationServer.git
搭建 SpringAuthorizationServer
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.mengshuo</groupId>
<artifactId>oauth-server</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth-server</name>
<description>oauth-server</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</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-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.2.1</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
配置文件
spring:
application:
name: authorization-server
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/authorization_server?serverTimezone=GMT%2B8&characterEncoding=UTF-8
username: root
password: root
thymeleaf:
prefix: classpath:/templates/
suffix: .html
mode: HTML
encoding: UTF-8
mvc:
throw-exception-if-no-handler-found: true
web:
resources:
add-mappings: false
management:
endpoints:
web:
exposure:
include: '*'
找到sql并执行
一共三个表:
oauth2_authorization
:这个表存放客户端请求授权的 code 码信息 等
oauth2_authorization_consent
:这个表存放用户对于客户端的授权信息,授予了那些客户端那些权限,如果时不需要用户二次确认的客户端不会再次存放信息
oauth2_registered_client
:oauth2 客户端信息
添加认证服务器核心配置
package top.mengshuo.core.config.oauth2;
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.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Lazy;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.core.context.SecurityContextHolder;
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.OAuth2TokenType;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.authorization.*;
import org.springframework.security.oauth2.server.authorization.authentication.OAuth2ClientAuthenticationToken;
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.ClientSettings;
import org.springframework.security.oauth2.server.authorization.config.ProviderSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.util.ObjectUtils;
import top.mengshuo.common.constants.OAuth2Scopes;
import top.mengshuo.common.utils.RsaUtils;
import javax.annotation.Resource;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.util.Set;
/**
* AuthorizationServer 配置类
*
* @author mengshuo
* @since 2021-12-27
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration(proxyBeanMethods = false)
public class AuthorizationServerConfig {
@Resource
@Lazy
private OAuth2AuthorizationConsentService auth2AuthorizationConsentService;
/**
* 初始化oauth2 默认配置 <br/>
* 使用 @Import(OAuth2AuthorizationServerConfiguration.class) 也可以
*
* @param http security配置
* @return res
* @throws Exception ex
*/
@Bean
@Order(Ordered.HIGHEST_PRECEDENCE)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
// 设置token扩展
// 这里如果是有 openid scope 的时候 会调用两次
// 第一次 access_token OAuth2TokenType.ACCESS_TOKEN 第二次 id_token OidcParameterNames.ID_TOKEN
// 可以通过 context.getTokenType() 来获取token类型,分别做不同的定制
http.setSharedObject(OAuth2TokenCustomizer.class, (OAuth2TokenCustomizer<JwtEncodingContext>) context -> {
// 获取请求认证的信息
OAuth2ClientAuthenticationToken authenticationToken =
((OAuth2ClientAuthenticationToken) SecurityContextHolder.getContext().getAuthentication());
if (!ObjectUtils.isEmpty(authenticationToken)) {
// 获取客户端信息
RegisteredClient client = authenticationToken.getRegisteredClient();
// 如果时合法的客户端
if (!ObjectUtils.isEmpty(client)) {
// 获取客户端可申请的权限信息
Set<String> clientScopes = client.getScopes();
// 获取用户所授予的权限信息
OAuth2AuthorizationConsent authority =
this.auth2AuthorizationConsentService.findById(client.getId(), String.valueOf(authenticationToken.getPrincipal()));
if (!ObjectUtils.isEmpty(authority) && !ObjectUtils.isEmpty(clientScopes)) {
// 获取授予的权限
Set<String> scopes = authority.getScopes();
// 如果是 access_token 并且 用户授权了获取权限信息(这和scope需要提取成常量) 则将权限信息写入 access_token
if (scopes.contains(OAuth2Scopes.AUTHORITIES) && OAuth2TokenType.ACCESS_TOKEN.equals(context.getTokenType())) {
// 写入自定义信息
context.getClaims().claim("authority", context.getPrincipal().getAuthorities().toString());
}
}
}
}
});
// 设置授权服务器的默认配置 此配置用来拦截匹配 oauth2 默认的一些端点
// OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
// 设置自定义授权端点配置
authorizationServerConfigurer.authorizationEndpoint(point -> point.consentPage("/consent"));
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
// 开启form 登录
return http.formLogin(Customizer.withDefaults()).build();
}
/**
* 默认的security配置 因为上面的 oauth2 已经使用了 .anyRequest() 无法再进行匹配 所以需要新的bean
*
* @param http http
* @return res
* @throws Exception ex
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
// 拦截所有请求、启用表单登录并使用默认登录配置
return http
// 设置 userDetailsService 用于加载用户信息并进行认证
.userDetailsService(userDetailsService())
.authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
.formLogin(Customizer.withDefaults())
.build();
}
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.builder()
.username("mengshuo")
.password("$2a$10$5b8EIc5Ehoz4oCUqMlK7N.NdIcGzcVLzmk7y8Z3XJ.BvcZ7ZHUuw.")
.authorities("test:add", "test:del").build();
return new InMemoryUserDetailsManager(userDetails);
}
/**
* 对客户端信息的持久化 <br/>
* oauth2_registered_client 对应这个表
*
* @param jdbcTemplate 数据库操作
* @return 存储位置实例
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// // 添加一个客户端
// RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// .clientId("mengshuo")
// .clientSecret(passwordEncoder().encode("mengshuo"))
// .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// .redirectUri("https://baidu.com")
// .scope(OidcScopes.OPENID)
// .scope("authorities")
// // token 配置
// .tokenSettings(TokenSettings.builder().accessTokenTimeToLive(Duration.ofMinutes(120)).refreshTokenTimeToLive(Duration.ofMinutes(150)).build())
// .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// .build();
// 客户端信息 crud
JdbcRegisteredClientRepository repository = new JdbcRegisteredClientRepository(jdbcTemplate);
// // 设置自定义密码加密器
// JdbcRegisteredClientRepository.RegisteredClientParametersMapper mapper = new JdbcRegisteredClientRepository.RegisteredClientParametersMapper();
// // 这里在 0.2.1 版本弃用了 可以在注册客户端时使用密码加密器手动加密密码并保存
// // 如果这里不设置那么将是明文密码,但是验证时会调用加密器解码所以需要手动加密
// mapper.setPasswordEncoder(passwordEncoder());
// // 保存自定义编码器配置
// repository.setRegisteredClientParametersMapper(mapper);
// // 添加客户端
// repository.save(registeredClient);
return repository;
}
/**
* 对认证信息进行持久化 <br/>
* 对应 oauth2_authorization 表
*
* @param jdbcTemplate jdbcTemplate
* @param registeredClientRepository 客户端存储操作
* @return res
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 对授权信息的持久化 记录了用户的授权信息 <br/>
* 如果客户端无需用户确认授权的话, <br/>
* 设置 {@link ClientSettings.Builder#requireAuthorizationConsent(boolean)} 为 false, <br/>
* 此表不会记录信息 <br/>
* <p>
* 对应 oauth2_authorization_consent 表
*
* @param jdbcTemplate jdbcTemplate
* @param registeredClientRepository 客户端存储操作
* @return res
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 设置 jwk 密钥 这些密钥可以放在配置文件中
* 也可以放入配置中心 例如 nacos ,从 nacos 获取配置信息,然后再进行初始化,
* 同时添加监听,监听到变更后可以通过 {@link org.springframework.cloud.context.refresh.ContextRefresher#refresh} 方法进行动态刷新
*
* @return res
* @throws InvalidKeySpecException ex
*/
@Bean
public JWKSource<SecurityContext> jwkSource() throws InvalidKeySpecException {
RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
2wIDAQAB
-----END PUBLIC KEY-----
""");
RSAPrivateKey privateKey = (RSAPrivateKey) RsaUtils.getPrivateKey("""
-----BEGIN PRIVATE KEY-----
MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQDONWf9J6WZgIz3
hsenarnYRcs796se68Qmd1+PwbevJFtIcvjYOM5YfNoG500ESZ+dc9zjoVU/sVWH
vkBovzo4LC2/Nrf1JJgFGZDioC4N3qcotGdB+mw5rSCWwLWOsCV+HccfDejzqzIJ
KREOWxJr//QzX892wcDsH6fbgpw99TWNss1Yhglx8FweS5e9CQ0pPnxehM4eZ3I2
UEs5KGYx5uFk458m1nBtqEAzCaP8yF3t/y7EJ/gNiGXmaN1vv1deuJsn3Q9WqwhJ
4W4KxSfDYr9Q6Uia90DrS1RxYulwEd0wBIlPqFwaG7+en+xTv+tTcsRUCqwRLMwb
fDOzB1XbAgMBAAECggEBALahEC9ivMS92JulMBBzcpM8BSpgSJsDHl8fHHCalg80
+O+qfAAsF3zBXuv8UPa6KfZuVjT4fyMA8QpfEnZy3qI/ZmjSkow307e4k9xTF1bL
WmqvlEAYBV/zmEhL9sCf+yS+RMuZKzcb6R251FRNjnw+XU5ezcSVB0NZKfY8sBoi
CU1aIRG3oxCvDoWLnKULTFFAO8pEkw6rJiA0wn8kAbWgilT5R9OAKeQkuDGPJ4xs
BMyUjZR9ZiVEK/FmY5/I0O88HdkKquOdOF37EQCpX2yP4uU3wkII0Yj89DwXH7To
D3a6bkp7hA7KxI8aSpB+BApT2VXu6IaBxDu2baYZg4kCgYEA9kRmOr2epyGLgTLK
PNCIOK45PPisIrRzIRg1lJyebtwRK9GmJGCO0aH7NNNgVKwSvIt0xmK4WysQz8wn
KYSzSXgf02Hddei+pSpryf4YLffSP9jl8y95YV6VzDVKZdZfpEpPnlOT7piwg36h
XOeRFVWA6EUFRl96u5zpUp8f91UCgYEA1lu3LpadG30KiRoh/E+ReUXd6hd5Yzrt
rzFHA5EVJBBj182OzBsE6e+z7GE0GYWg/PnCIAseI4s2XNMyfsZl6mDRC4gXDgd2
SEQt/M7ejvj7jpQr3CCDMJgHXTE3w3XQLUkKfaTPQ5LGap3rfZihMInbGlBtlcM4
wQ8M/eTOuG8CgYEA4ZTGEAihy3Zu02oy1oIuRb1RsQgYpbGlxCro6biNZ/8tu3XP
OoM4T86QzVLSar00bIFR9md3eAt62t2nAeEMWcAvZvG+asNH3wN8uQqIG5NmhBWq
jZhvF0IM9YHbJG26LkPjqqPkXip/hfP426FCxMgNzLVsn4nWYwTYtVYNcgECgYEA
yyOxjkIHcxRllpYgXPaeuxm9+ujyDVq8AsmlLbkhGsM5izpvN/fnCTczADEB6anc
bcDW+fzvO+niw8cV1FR6IspLcS0wBAiITGXoWutEuKM16eF7Sym4iaWZXPSWjvIo
LbJJcdZs4PHIfSNZFvY80z3hWKedok6Wi0aTHkzmppsCgYADlaigi0Pk+yFsJWg7
V8FgGOKduez1lqWb993S1Y3HNAZp42IKDC++8ickfPaHCvmktVD6ZwodOcKBCo5J
enkEG2TQZo/T39Uf2k7/rIWe6PVyb9DkhpLI5MqEuFVnkBLJX8Ug4RNNMYUJz84a
dtYVz7PRa9t4w3G9oEuV6c+dqQ==
-----END PRIVATE KEY-----
""");
RSAKey rsaKey = new RSAKey.Builder(publicKey).privateKey(privateKey).keyID("123456").build();
JWKSet jwkSet = new JWKSet(rsaKey);
return (jwkSelector, context) -> jwkSelector.select(jwkSet);
}
/**
* jwt 解码器 暂时没有用到
*
* @return 解码器
* @throws InvalidKeySpecException ex
*/
@Bean
public JwtDecoder jwtDecoder() throws InvalidKeySpecException {
RSAPublicKey publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
2wIDAQAB
-----END PUBLIC KEY-----
""");
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
/**
* 服务器端点相关配置 可以设置各个接口的url <br/>
* 默认端点 {@link org.springframework.security.oauth2.server.authorization.config.ProviderSettings#builder()}
*
* @return res
*/
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().tokenEndpoint("/oauth2/token").issuer("http://127.0.0.1:8080").build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
自定义授权页面 (可选)
添加controller
因为需要用户的一些信息
package top.mengshuo.authorization.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsent;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
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.web.OAuth2AuthorizationEndpointFilter;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import top.mengshuo.authorization.entity.User;
import javax.annotation.Resource;
import java.util.Arrays;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* @author mengshuo
* @since 2021-12-31
*/
@Controller
public class AuthController {
@Resource
private OAuth2AuthorizationConsentService consentService;
@Resource
private RegisteredClientRepository registeredClientRepository;
/**
* 授权确认页面
* 授权码颁发流程 {@link OAuth2AuthorizationEndpointFilter#doFilterInternal}
*
* @param state 会话标识
* @param clientId 客户端id
* @param model model
* @param reqScopes 客户端想要获取的权限名称
* @return res
*/
@GetMapping("/consent")
public String consent(String state, @RequestParam("client_id") String clientId, Model model, @RequestParam("scope") String reqScopes) {
// 获取认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
org.springframework.security.core.userdetails.User principal =
(org.springframework.security.core.userdetails.User) authentication.getPrincipal();
// 获取用户信息 模拟从数据库获取
User userInfo = new User("mengshuo","","halt");
// 获取客户端信息
RegisteredClient client = this.registeredClientRepository.findByClientId(clientId);
if (client == null) {
throw new RuntimeException("client_id 不合法");
}
// 获取已经授予的权限
OAuth2AuthorizationConsent consent = this.consentService.findById(client.getId(), principal.getUsername());
// 设置用户信息
model.addAttribute("nickname", userInfo.getNickname());
// 设置客户端名称
model.addAttribute("clientName", client.getClientName());
// 当用户已经授权过
if (consent != null) {
// 设置已经授予的权限
Set<String> authScope = consent.getScopes();
model.addAttribute("authScope", authScope);
// 设置请求的数据
// 格式化scopes 并去除已经授予的权限
List<String> reqScope = Arrays.stream(reqScopes.split(" ")).filter(s -> !authScope.contains(s)).collect(Collectors.toList());
model.addAttribute("reqScope", reqScope);
} else {
// 格式化scope并保存 此时客户端没有拿到任何授权
model.addAttribute("reqScope", reqScopes.split(" "));
}
model.addAttribute("state", state);
model.addAttribute("clinetId", clientId);
return "consent";
}
}
添加页面
<!DOCTYPE html>
<html lang="en" class="">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css"
integrity="sha384-JcKb8q3iqJ61gNV9KGb8thSsNjpSL0n8PARn9HuZOnIxN0hoP+VmmDGMN5t9UJ0Z" crossorigin="anonymous">
<title>Consent required</title>
<link type="text/css" rel="stylesheet" charset="UTF-8"
href="https://translate.googleapis.com/translate_static/css/translateelement.css">
</head>
<body>
<div class="container">
<div class="py-5"><h1 class="text-center">确认您的授权</h1></div>
<div class="row">
<div class="col text-center">
<p>
<span class="font-weight-bold" th:text="${nickname}"></span> 您好,应用:
<span class="font-weight-bold text-primary" th:text="${clientName}"></span>
向您获取以下权限
</p>
</div>
</div>
<div class="row pb-3">
<div class="col text-center"><p>请您确认是否授权</p></div>
</div>
<div class="row">
<div class="col text-center">
<form method="post" action="./oauth2/authorize">
<input type="hidden" name="client_id" th:value="${clinetId}">
<input type="hidden" name="state" th:value="${state}">
<div th:each="scope: ${reqScope}">
<!-- openid 默认授予 -->
<div th:if="${scope eq 'openid' }" class="form-group form-check py-1">
<input type="hidden" name="openid" value="openid"/>
</div>
<div th:unless="${scope eq 'openid' }" class="form-group form-check py-1">
<input class="form-check-input" type="checkbox" name="scope" th:value="${scope}"
th:id="${scope}"/>
<label class="form-check-label" th:for="${scope}" th:text="${scope}"></label>
</div>
</div>
<div th:if="${authScope != null && authScope.size() > 0}">
<p>
以下是您已经授予
<span th:text="${clientName}" class="font-weight-bold text-primary"></span>
的权限:
</p>
<div th:each="scope: ${authScope}">
<div class="form-group form-check py-1">
<input class="form-check-input" type="checkbox" name="scope" th:value="${scope}"
th:id="${scope}" checked disabled/>
<label class="form-check-label" th:for="${scope}" th:text="${scope}"></label>
</div>
</div>
</div>
<div class="form-group pt-3">
<button class="btn btn-primary btn-lg" type="submit">确认授权</button>
</div>
<div class="form-group">
<button class="btn btn-link regular" type="reset">取消</button>
</div>
</form>
</div>
</div>
<div class="row pt-4">
<div class="col text-center"><p><small>需要您的同意才能提供访问权限<br>如果您不批准,
单击“取消”,在这种情况下,不会与应用共享任何信息。</small></p></div>
</div>
</div>
<div class="goog-te-spinner-pos">
<div class="goog-te-spinner-animation">
<svg xmlns="http://www.w3.org/2000/svg" class="goog-te-spinner" width="96px" height="96px" viewBox="0 0 66 66">
<circle class="goog-te-spinner-path" fill="none" stroke-width="6" stroke-linecap="round" cx="33" cy="33"
r="30"></circle>
</svg>
</div>
</div>
</body>
</html>
测试
测试授权码类型
- 访问 http://127.0.0.1:8080/oauth2/authorize?response_type=code&client_id=mengshuo&redirect_uri=https://baidu.com&scope=openid%20authority
- 用code换取token: http://127.0.0.1:8080/oauth2/token?grant_type=authorization_code&code=
- 刷新token: http://127.0.0.1:8080/oauth2/token?grant_type=refresh_token&refresh_token=
换取token和刷新token都需要添加请求头 Authorization : Basic Base64编码的 clientId:secret
资源服务器
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>top.mengshuo</groupId>
<artifactId>oauth-resource</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>oauth-resource</name>
<description>oauth-resource</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<repositories>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>true</enabled>
</releases>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<releases>
<enabled>true</enabled>
</releases>
</pluginRepository>
</pluginRepositories>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
核心配置
package top.mengshuo.core.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.convert.converter.Converter;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
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.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.annotation.web.configurers.CsrfConfigurer;
import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
/**
* @author mengshuo
* @since 2021-12-23
*/
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
@Configuration
public class ResourcesServerConfiguration extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
// 关闭 csrf
.csrf(CsrfConfigurer::disable)
// 关闭登录
.formLogin(FormLoginConfigurer::disable)
// 关闭session创建
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
// 需要认证的接口
.authorizeRequests(authReq -> authReq.antMatchers("/v1/**").authenticated())
.authorizeRequests(authReq->authReq.anyRequest().permitAll())
// 启用jwt令牌的支持
.oauth2ResourceServer(
resourceServer ->
resourceServer.jwt()
.jwtAuthenticationConverter(converter())
);
}
/**
* 认证信息转换器 因为
*
* @return UsernamePasswordAuthenticationToken
*/
private Converter<Jwt, ? extends AbstractAuthenticationToken> converter() {
return source -> {
// 获取 jwt token 中的权限信息
String authority = source.getClaimAsString("authority");
// 解析 jwt token 中的权限信息
String[] authorityArray = authority.substring(1).substring(0, authority.length() - 2).trim().replaceAll(" ", "").split(",");
// 创建并返回一个 username password 认证信息类
return new UsernamePasswordAuthenticationToken(source.getClaimAsString("sub"), null, AuthorityUtils.createAuthorityList(authorityArray));
};
}
/**
* 构建jwt解码器
*
* @return jwt解码器
*/
@Bean
public JwtDecoder jwtDecoder() {
RSAPublicKey publicKey = null;
try {
publicKey = (RSAPublicKey) RsaUtils.getPublicKey("""
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAzjVn/SelmYCM94bHp2q5
2EXLO/erHuvEJndfj8G3ryRbSHL42DjOWHzaBudNBEmfnXPc46FVP7FVh75AaL86
OCwtvza39SSYBRmQ4qAuDd6nKLRnQfpsOa0glsC1jrAlfh3HHw3o86syCSkRDlsS
a//0M1/PdsHA7B+n24KcPfU1jbLNWIYJcfBcHkuXvQkNKT58XoTOHmdyNlBLOShm
MebhZOOfJtZwbahAMwmj/Mhd7f8uxCf4DYhl5mjdb79XXribJ90PVqsISeFuCsUn
w2K/UOlImvdA60tUcWLpcBHdMASJT6hcGhu/np/sU7/rU3LEVAqsESzMG3wzswdV
2wIDAQAB
-----END PUBLIC KEY-----
""");
} catch (InvalidKeySpecException e) {
e.printStackTrace();
}
return NimbusJwtDecoder.withPublicKey(publicKey).build();
// return NimbusJwtDecoder.withJwkSetUri(oAuth2ResourceServerProperties.getJwt().getJwkSetUri()).build();
}
}
Controller
package top.mengshuo.controller;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* @author mengshuo
* @since 2021-12-27
*/
@RestController
public class TestController {
@PreAuthorize("hasAuthority('test:add')")
@GetMapping("/v1/add")
public String add() {
return "add";
}
@PreAuthorize("hasAuthority('test:del')")
@GetMapping("/v1/del")
public String del() {
return "del";
}
@GetMapping("/test")
public String test() {
return "test";
}
}
测试
-
通过上面拿到了 token来访问资源服务器
添加请求头 Authorization : Bearer token