前言
注意:我本地没有生成公钥和私钥,所以每次启动项目jwkSource都会重新生成,导致之前认证的token都会失效,具体如何生成私钥和公钥以及怎么配置到授权服务器中,网上有很多方法自行实现即可
之前有个项目用的0.0.3的,正好最近想研究研究,所以就去了官网看文档研究了一下,1.1.1基于的事security6.x的版本, security6与5.7之前的版本有很大的差别,废话不多说,直接上代码(代码中也有一些注释)
最基础的配置官网都有,这里不去体现,主要体现功能:
- 自定义认证和授权
- 自定义端点拦截器
- 持久化到数据库
版本
依赖项 | 版本 |
---|---|
springboot | 3.1.2 |
spring-authorization-server | 1.1.1 |
jdk | 17 |
dynamic-datasource-spring-boot3-starter | 4.1.2 |
mybatis-plus | 3.5.3 |
sql文件
代码实操
目录结构
pom文件
需要额外引入spring security cas包原因是启动时(logging等级:org.springframework.security: trace)会报错:java.lang.ClassNotFoundException:org.springframework.security.cas.jackson2.CasJackson2Module错误。
<?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>3.1.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>demo</name>
<description>demo</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencyManagement>
<!-- <dependencies>-->
<!-- <!– SpringBoot的依赖配置–>-->
<!-- <dependency>-->
<!-- <groupId>org.springframework.boot</groupId>-->
<!-- <artifactId>spring-boot-dependencies</artifactId>-->
<!-- <version>2.5.14</version>-->
<!-- <type>pom</type>-->
<!-- <scope>import</scope>-->
<!-- </dependency>-->
<!-- </dependencies>-->
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</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-oauth2-authorization-server</artifactId>
</dependency>
<!-- 添加spring security cas支持
这里需添加spring-security-cas依赖,
否则启动时报java.lang.ClassNotFoundException: org.springframework.security.cas.jackson2.CasJackson2Module错误。
-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.fastjson2</groupId>
<artifactId>fastjson2</artifactId>
<version>2.0.34</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3</version>
</dependency>
<!-- 阿里数据库连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.2.16</version>
</dependency>
<!-- SpringBoot Web容器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.31</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>dynamic-datasource-spring-boot3-starter</artifactId>
<version>4.1.2</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
授权服务器配置(包名:authenticationServer )
CustomAuthorizationServerConfiguration
import com.example.demo.config.security.provider.WeChatMiniAppAuthenticationProvider;
import com.example.demo.config.security.provider.converter.WeChatMiniAppAuthenticationConverter;
import com.example.demo.utils.OAuth2ConfigurerUtils;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2AuthorizationCodeAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2ClientCredentialsAuthenticationConverter;
import org.springframework.security.oauth2.server.authorization.web.authentication.OAuth2RefreshTokenAuthenticationConverter;
import org.springframework.security.web.util.matcher.RequestMatcher;
public class CustomAuthorizationServerConfiguration {
public static void applyDefaultSecurity(HttpSecurity http, JWKSource<SecurityContext> jwkSource) throws Exception {
OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer();
OAuth2AuthorizationService authorizationService = OAuth2ConfigurerUtils.getBean(http,OAuth2AuthorizationService.class);
// 认证过滤器链
authorizationServerConfigurer.tokenEndpoint(oAuth2TokenEndpointConfigurer -> {
oAuth2TokenEndpointConfigurer.accessTokenRequestConverters( customJwtAuthenticationToken -> {
customJwtAuthenticationToken.add(new OAuth2AuthorizationCodeAuthenticationConverter());
customJwtAuthenticationToken.add(new OAuth2RefreshTokenAuthenticationConverter());
customJwtAuthenticationToken.add(new OAuth2ClientCredentialsAuthenticationConverter());
customJwtAuthenticationToken.add(new WeChatMiniAppAuthenticationConverter());
})
// 返回accessToken的后置处理器 https://docs.spring.io/spring-authorization-server/docs/current/reference/html/protocol-endpoints.html#oauth2-token-endpoint
// .accessTokenResponseHandler()
// 异常返回处理器
// .errorResponseHandler()
.authenticationProviders((customProviders) -> {
// 自定义认证提供者
customProviders.add(new WeChatMiniAppAuthenticationProvider(jwkSource,authorizationService));
});
});
// 端点匹配器
RequestMatcher endpointsMatcher = authorizationServerConfigurer.getEndpointsMatcher();
http.securityMatcher(endpointsMatcher)
// .authorizeHttpRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated())
// csrf
.csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher))
.apply(authorizationServerConfigurer);
}
}
认证提供者(包名:provider)
预处理(包名: Converter)
WeChatMiniAppAuthenticationConverter
可以说是预处理类转换token信息
import com.example.demo.config.security.provider.token.WeChatMiniAppAuthenticationToken;
import com.example.demo.config.security.provider.type.AuthorizationGrantTypes;
import jakarta.servlet.http.HttpServletRequest;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.endpoint.OAuth2ParameterNames;
import org.springframework.security.web.authentication.AuthenticationConverter;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import java.util.LinkedHashMap;
import java.util.Map;
public final class WeChatMiniAppAuthenticationConverter implements AuthenticationConverter {
private final Logger logger = LoggerFactory
.getLogger(WeChatMiniAppAuthenticationConverter.class);
/**
* 参数对象
*
* @param request {@link HttpServletRequest}
* @return {@link Authentication}
*/
@Override
public Authentication convert(HttpServletRequest request) {
String grantType = request.getParameter(OAuth2ParameterNames.GRANT_TYPE);
if (!AuthorizationGrantTypes.WECHAT_MINIAPP.getValue().equals(grantType)) {
return null;
}
// 获取参数
MultiValueMap<String, String> parameters = getParameters(request);
logger.info("微信小程序授权入参:{}", parameters);
// 微信CODE
// 其他参数
Map<String, Object> additionalParameters = getOtherParameters(parameters);
// clientPrincipal
Authentication clientPrincipal = SecurityContextHolder.getContext().getAuthentication();
logger.info("小程序授权全部参数:{}", additionalParameters);
return new WeChatMiniAppAuthenticationToken(clientPrincipal, additionalParameters, "code",
"appid", "encryptedData", "ivStr");
}
/**
* 获取其他参数
*
* @param parameters {@link MultiValueMap}
* @return {@link Map}
*/
private Map<String, Object> getOtherParameters(MultiValueMap<String, String> parameters) {
Map<String, Object> additionalParameters = new LinkedHashMap<>(16);
parameters.forEach((key, value) -> {
if (!key.equals(OAuth2ParameterNames.GRANT_TYPE)
&& !key.equals(OAuth2ParameterNames.SCOPE)) {
additionalParameters.put(key, value.get(0));
}
});
return additionalParameters;
}
static MultiValueMap<String, String> getParameters(HttpServletRequest request) {
Map<String, String[]> parameterMap = request.getParameterMap();
MultiValueMap<String, String> parameters = new LinkedMultiValueMap<>(parameterMap.size());
parameterMap.forEach((key, values) -> {
for (String value : values) {
parameters.add(key, value);
}
});
return parameters;
}
}
自定义token (包名: token)
WeChatMiniAppAuthenticationToken
@Setter
@Getter
public class WeChatMiniAppAuthenticationToken extends OAuth2AuthorizationGrantAuthenticationToken {
/**
* appId 应用ID
*/
private final String appId;
/**
* code 小程序CODE
*/
private final String code;
private final String encryptedData;
private final String ivStr;
/**
* Sub-class constructor.
* @param clientPrincipal the authenticated client principal
* @param additionalParameters the additional parameters
* @param code {@link String } 小程序code
* @param appId {@link String } 平台appid
* @param encryptedData {@link String } encryptedData
* @param ivStr {@link String } ivStr
*/
public WeChatMiniAppAuthenticationToken(Authentication clientPrincipal,
Map<String, Object> additionalParameters, String code,
String appId, String encryptedData, String ivStr) {
super(AuthorizationGrantTypes.WECHAT_MINIAPP, clientPrincipal, additionalParameters);
this.code = code;
this.appId = appId;
this.encryptedData = encryptedData;
this.ivStr = ivStr;
}
}
定义认证type (包名:type)
AuthorizationGrantTypes
人获取access_token时,会用到grantType
public class AuthorizationGrantTypes {
public static final AuthorizationGrantType WECHAT_MINIAPP = new AuthorizationGrantType(
"wechat_miniapp");
}
资源服务器 (包名:resourceServer)
CustomAuthenticationTokenConverter
自定义jwt 预处理器
public class CustomAuthenticationToken implements Converter<Jwt, AbstractAuthenticationToken> {
private final Converter<Jwt, Collection<GrantedAuthority>> jwtGrantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter();
private static final String PRINCIPAL_CLAIM_NAME = "data";
private final Logger logger = LoggerFactory.getLogger(CustomAuthenticationToken.class);
public CustomAuthenticationToken() {
}
public AbstractAuthenticationToken convert(@NonNull Jwt jwt) {
this.logger.info("convert ->jwt:{}", JSONObject.toJSONString(jwt));
Collection<GrantedAuthority> authorities = this.extractAuthorities(jwt);
// 获取个性化的token信息
String principalClaimValue = jwt.getClaimAsString(PRINCIPAL_CLAIM_NAME);
if (principalClaimValue == null) {
return new JwtAuthenticationToken(jwt, authorities);
} else {
UserDto user = this.extractUserInfo(jwt);
user.setToken(jwt.getTokenValue());
CustomJwtAuthenticationToken jwtAuthenticationToken = new CustomJwtAuthenticationToken(jwt, user, authorities);
SecurityContextHolder.getContext().setAuthentication(jwtAuthenticationToken);
return jwtAuthenticationToken;
}
}
protected Collection<GrantedAuthority> extractAuthorities(Jwt jwt) {
return this.jwtGrantedAuthoritiesConverter.convert(jwt);
}
protected UserDto extractUserInfo(Jwt jwt) {
String principalClaimValue = jwt.getClaimAsString("data");
return JSONObject.parseObject(principalClaimValue, UserDto.class);
}
}
CustomJwtAuthenticationToken
自定义的jwt
@Transient
public class CustomJwtAuthenticationToken extends AbstractOAuth2TokenAuthenticationToken<Jwt> {
private static final long serialVersionUID = 560L;
private final String name;
public CustomJwtAuthenticationToken(Jwt jwt, Object principal, Collection<? extends GrantedAuthority> authorities) {
super(jwt, principal, "", authorities);
this.setAuthenticated(true);
this.name = jwt.getSubject();
}
public Map<String, Object> getTokenAttributes() {
return ((Jwt)this.getToken()).getClaims();
}
public String getName() {
return this.name;
}
}
CustomOauth2AuthenticationEntryPoint
自定义协议端点
public class CustomOauth2AuthenticationEntryPoint implements AuthenticationEntryPoint {
private static final Logger logger = LoggerFactory.getLogger(CustomOauth2AuthenticationEntryPoint.class);
private String realmName;
public CustomOauth2AuthenticationEntryPoint() {
}
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException {
logger.error(e.getLocalizedMessage(), e);
HttpStatus status = HttpStatus.UNAUTHORIZED;
Map<String, String> parameters = new LinkedHashMap<>();
if (Objects.nonNull(this.realmName)) {
parameters.put("realm", this.realmName);
}
if (e instanceof OAuth2AuthenticationException oAuth2AuthenticationException) {
OAuth2Error error = oAuth2AuthenticationException.getError();
parameters.put("error", error.getErrorCode());
if (StringUtils.hasText(error.getDescription())) {
String errorMessage = error.getDescription();
parameters.put("error_description", errorMessage);
}
if (StringUtils.hasText(error.getUri())) {
parameters.put("error_uri", error.getUri());
}
if (error instanceof BearerTokenError bearerTokenError) {
if (StringUtils.hasText(bearerTokenError.getScope())) {
parameters.put("scope", bearerTokenError.getScope());
}
status = ((BearerTokenError)error).getHttpStatus();
}
}
ResponseEntity<String> unauthenticated = new ResponseEntity<String>("Unauthenticated", HttpStatusCode.valueOf(status.value()));
String message = JSON.toJSONString(unauthenticated);
String wwwAuthenticate = WwwAuthenticateHeaderBuilder.computeWwwAuthenticateHeaderValue(parameters);
response.addHeader("WWW-Authenticate", wwwAuthenticate);
response.setStatus(status.value());
response.setContentType("application/json");
response.getWriter().write(message);
}
}
WwwAuthenticateHeaderBuilder
public final class WwwAuthenticateHeaderBuilder {
public WwwAuthenticateHeaderBuilder() {
}
public static String computeWwwAuthenticateHeaderValue(Map<String, String> parameters) {
StringJoiner wwwAuthenticate = new StringJoiner(", ", "Bearer ", "");
if (!parameters.isEmpty()) {
parameters.forEach((k, v) -> {
wwwAuthenticate.add(k + "=\"" + v + "\"");
});
}
return wwwAuthenticate.toString();
}
}
Oauth2ResourceServerConfigurer
资源服务器配置
public final class Oauth2ResourceServerConfigurer {
public Oauth2ResourceServerConfigurer() {
}
public static void applyDefaultSecurity(HttpSecurity http) throws Exception {
http.oauth2ResourceServer((oauth2ResourceServerConfigurer) ->
oauth2ResourceServerConfigurer
// 无权限处理器
// .accessDeniedHandler()
// 自定义协议端点
.authenticationEntryPoint(new CustomOauth2AuthenticationEntryPoint())
.jwt((jwtConfigurer) -> jwtConfigurer.jwtAuthenticationConverter(new CustomAuthenticationToken())));
}
}
SecurityConfig
实现授权及资源服务器
package com.example.demo.config.security;
import com.example.demo.config.security.authenticationServer.CustomAuthorizationServerConfiguration;
import com.example.demo.config.security.resourceServer.Oauth2ResourceServerConfigurer;
import com.example.demo.service.CustomUserDetailsService;
import com.fasterxml.jackson.databind.ObjectMapper;
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 lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
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.jackson2.CoreJackson2Module;
import org.springframework.security.jackson2.SecurityJackson2Modules;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.*;
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.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.jackson2.OAuth2AuthorizationServerJackson2Module;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
/***
*
* @author qb
* @since 2023/7/21 15:14
* @version 1.0
*/
@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig {
// 协议端点过滤器链
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception {
CustomAuthorizationServerConfiguration.applyDefaultSecurity(http,jwkSource());
// // 开启oidc connect 1.0
// http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
// // 重定向到未通过身份验证的登录页面 授权终结点
// http.exceptionHandling((exceptions) -> exceptions.defaultAuthenticationEntryPointFor(
// new LoginUrlAuthenticationEntryPoint("/login"),
// new MediaTypeRequestMatcher(MediaType.TEXT_HTML)
// ))
// // 授权服务 接受的用户信息和/或客户端注册的访问令牌
// .oauth2ResourceServer((resourceServer) -> resourceServer.jwt(Customizer.withDefaults()));
return http.build();
}
// 协议认证筛选器
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
Oauth2ResourceServerConfigurer.applyDefaultSecurity(http);
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth ->
auth.requestMatchers("/login","/callback","/oauth2/client/**")
.permitAll().anyRequest()
.authenticated());
return http.build();
}
// 自定义认证service
@Bean
public UserDetailsService userDetailsService() {
return new CustomUserDetailsService();
}
// 密码加密
@Bean
public PasswordEncoder passwordEncoder(){
// return PasswordEncoderFactories.createDelegatingPasswordEncoder();
return new BCryptPasswordEncoder();
}
/**
* 管理客户端
* @return /
*/
@Bean
public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
// RegisteredClient oidcClient = defaultClient();
// 配置模式
// JdbcRegisteredClientRepository jdbcRegisteredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// if (null == jdbcRegisteredClientRepository.findByClientId("client")) {
// jdbcRegisteredClientRepository.save(oidcClient);
// }
return new JdbcRegisteredClientRepository(jdbcTemplate);
}
private static RegisteredClient defaultClient() {
// 方便测试,先注册一个测试客户端
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("client")
// 123456
.clientSecret("$2a$10$gJUJo9Ad3wDIhBGVH.8/i.Ox82tSCR4.UkbiDWEDUVQnIzcTMPjKK")
// 可以基于 basic 的方式和授权服务器进行认证
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// 授权码
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// 刷新token
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// 客户端模式
.authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// 密码模式
.authorizationGrantType(AuthorizationGrantType.JWT_BEARER)
// 重定向url
.redirectUri("http://127.0.0.1:9000/callback")
.postLogoutRedirectUri("http://127.0.0.1:9000/")
// 客户端申请的作用域,也可以理解这个客户端申请访问用户的哪些信息,比如:获取用户信息,获取用户照片等
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.clientSettings(
ClientSettings.builder()
// 是否需要用户确认一下客户端需要获取用户的哪些权限
// 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
.requireAuthorizationConsent(true)
.build()
)
.tokenSettings(
TokenSettings.builder()
// accessToken 的有效期
.accessTokenTimeToLive(Duration.ofHours(1))
// refreshToken 的有效期
.refreshTokenTimeToLive(Duration.ofDays(3))
// 是否可重用刷新令牌
.reuseRefreshTokens(true)
.build()
)
.build();
return oidcClient;
}
/**
* 自定义授权service
* @return /
*/
@Bean
public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
// JdbcOAuth2AuthorizationService authorizationService = new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
// JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper rowMapper = new JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper(registeredClientRepository);
// ClassLoader classLoader = JdbcOAuth2AuthorizationService.OAuth2AuthorizationRowMapper.class.getClassLoader();
// ObjectMapper objectMapper = new ObjectMapper();
// objectMapper.registerModules(new CoreJackson2Module());
// objectMapper.registerModules(SecurityJackson2Modules.getModules(classLoader));
// objectMapper.registerModule(new OAuth2AuthorizationServerJackson2Module());
// rowMapper.setObjectMapper(objectMapper);
// authorizationService.setAuthorizationRowMapper(rowMapper);
// return authorizationService;
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
}
/**
* 自定义确认授权 service 配置
*
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate,
RegisteredClientRepository registeredClientRepository) {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
}
/**
* 签名实例
* 此处注意,目前重启项目就会导致之前已存在的token失效,需要改为固定私钥和公钥,生成到项目目录下
* @return /
*/
@Bean
public JWKSource<SecurityContext> jwkSource(){
var keyPair = generateRsaKeyPair();
// 公钥
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
var jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
// 解析JWKSource访问令牌,构建 JwtDecoder 实例
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource){
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
// 启动时生成的 with 密钥的实例,用于创建上述内容。java.security.KeyPairJWKSource
private static KeyPair generateRsaKeyPair(){
KeyPair keyPair ;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}catch (Exception e) {
throw new IllegalStateException(e);
}
return keyPair;
}
/**
* 自定义jwt信息,全局(自定义jwt实现例外)
* @return /
*/
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
context.getJwsHeader().header("client-id", context.getRegisteredClient().getClientId());
context.getClaims().claim("test","哈哈哈").build();
log.info("jwtCustomizer -> getJwsHeader:{}",context.getJwsHeader());
log.info("jwtCustomizer -> claim:{}", context.getClaims());
// Customize claims
};
}
/**
* 自定义token属性 预留
* @return /
*/
@Bean
public OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer() {
return context -> {
OAuth2TokenClaimsSet.Builder claims = context.getClaims();
claims.claim("test200","哈哈哈哈哈").build();
log.info("accessTokenCustomizer -> claims:{}",claims);
// Customize claims
};
}
// Jwt编码上下文 拓展token
// @Bean
// public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
// return context -> {
// JwsHeader.Builder headers = context.getJwsHeader();
// JwtClaimsSet.Builder claims = context.getClaims();
// headers.header("client-id", context.getRegisteredClient().getClientId());
// log.info("jwtCustomizer headers:{}",headers);
// if (context.getTokenType().equals(OAuth2TokenType.ACCESS_TOKEN)) {
// // Customize headers/claims for access_token
//
// } else if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// // Customize headers/claims for id_token
// }
// };
// }
// 自定义 授权服务器的实例,设置用于配置spring授权服务器
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
// 总之server服务,例如:个性化认证及授权相关的路径
return AuthorizationServerSettings.builder().build();
}
// 会话管理
// @Bean
// public SessionRegistry sessionRegistry() {
// return new SessionRegistryImpl();
// }
// @Bean
// public HttpSessionEventPublisher httpSessionEventPublisher() {
// return new HttpSessionEventPublisher();
// }
}
MybatisPlusConfig
@Configuration
public class MybatisPlusConfig {
/**
* 新的分页插件,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存出现问题(该属性会在旧插件移除后一同移除)
*/
@Bean
public MybatisPlusInterceptor mybatisPlusInterceptor() {
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
return interceptor;
}
}
controller包
ClientController
注册客户端
@RequestMapping("/oauth2/client")
@RestController
@RequiredArgsConstructor
public class ClientController {
private final RegisteredClientRepository registeredClientRepository;
private final PasswordEncoder passwordEncoder;
/**
* 注册客户端
*/
@PostMapping
public ResponseEntity<Boolean> registeredClientRepository(@RequestBody RegisteredOauth2Client registeredClient) {
// @formatter:off
RegisteredClient entity = RegisteredClient.withId(UUID.randomUUID().toString())
// ID
.clientId(registeredClient.getClientId())
// 秘钥
.clientSecret(passwordEncoder.encode(registeredClient.getClientSecret()))
// POST 请求方法
.clientAuthenticationMethods(clientAuthenticationMethods ->
clientAuthenticationMethods.addAll(registeredClient.getClientAuthenticationMethods())
)
// CLIENT_CREDENTIALS
.authorizationGrantTypes(authorizationGrantTypes ->
authorizationGrantTypes.addAll(registeredClient.getAuthorizationGrantTypes()))
// 范围
.scopes(strings -> {
// 超级
strings.addAll(registeredClient.getScopes());
})
.tokenSettings(registeredClient.getTokenSettings())
// 客户端配置
.clientSettings(registeredClient.getClientSettings())
.build();
// Save registered client in db
registeredClientRepository.save(entity);
return ResponseEntity.ok(true);
}
}
InfoController
测试 当前认证上下文信息
@RestController
@RequestMapping("/info")
public class InfoController {
@GetMapping("/token")
public Object token(){
return SecurityContextHolder.getContext().getAuthentication().getPrincipal();
}
}
domain
BaseResponseDto
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder
public class BaseResponseDto {
private String code;
private String message;
}
LoginRequest
@Data
public class LoginRequest {
private String username;
private String password;
}
UserDto
@Data
@Accessors(chain = true)
public class UserDto {
private String id;
private String hosId;
private String token;
}
RegisteredOauth2Client 重要
这个类可以着重看一下
@Data
@NoArgsConstructor
public class RegisteredOauth2Client implements Serializable {
/**
* 客户端ID
*/
private String clientId;
/**
* 客户端秘钥
*/
private String clientSecret;
/**
* 客户端名称
*/
private String clientName;
/**
* 权限范围
*/
private Set<String> scopes;
private Instant clientIdIssuedAt;
private Instant clientSecretExpiresAt;
private Set<ClientAuthenticationMethod> clientAuthenticationMethods;
private Set<AuthorizationGrantType> authorizationGrantTypes;
private Set<String> redirectUris;
private ClientSettings clientSettings = ClientSettings.builder()
// 是否需要用户确认一下客户端需要获取用户的哪些权限
// 比如:客户端需要获取用户的 用户信息、用户照片 但是此处用户可以控制只给客户端授权获取 用户信息。
.requireAuthorizationConsent(true)
.build();
private TokenSettings tokenSettings = TokenSettings.builder()
// accessToken 的有效期
.accessTokenTimeToLive(Duration.ofHours(1))
// refreshToken 的有效期
.refreshTokenTimeToLive(Duration.ofDays(3))
// 是否可重用刷新令牌
.reuseRefreshTokens(true).build();
}
UserEntity
@Data
@TableName("user")
public class UserEntity {
@TableId("id_")
private Long id;
@TableField("username")
private String username;
@TableField("password")
private String password;
}
mapper
UserMapper
public interface UserMapper extends BaseMapper<UserEntity> {
}
service
IUserService
public interface IUserService extends IService<UserEntity> {
}
UserServiceImpl
@Service
@RequiredArgsConstructor
public class UserServiceImpl extends ServiceImpl<UserMapper, UserEntity> implements IUserService {
}
utils
HelperUtils
public class HelperUtils {
public static final ObjectWriter JSON_WRITER = new ObjectMapper().writer().withDefaultPrettyPrinter();
}
JwtUtils
public final class JwtUtils {
private JwtUtils() {
}
public static JwsHeader.Builder headers() {
return JwsHeader.with(SignatureAlgorithm.RS256);
}
public static JwtClaimsSet.Builder accessTokenClaims(RegisteredClient registeredClient,
String issuer, String subject,
Set<String> authorizedScopes) {
Instant issuedAt = Instant.now();
Instant expiresAt = issuedAt
.plus(registeredClient.getTokenSettings().getAccessTokenTimeToLive());
/**
* iss (issuer):签发人/发行人
* sub (subject):主题
* aud (audience):用户
* exp (expiration time):过期时间
* nbf (Not Before):生效时间,在此之前是无效的
* iat (Issued At):签发时间
* jti (JWT ID):用于标识该 JWT
*/
// @formatter:off
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(subject)
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.notBefore(issuedAt);
if (!CollectionUtils.isEmpty(authorizedScopes)) {
claimsBuilder.claim(OAuth2ParameterNames.SCOPE, authorizedScopes);
}
// @formatter:on
return claimsBuilder;
}
public static JwtClaimsSet.Builder idTokenClaims(RegisteredClient registeredClient,
String issuer, String subject, String nonce) {
Instant issuedAt = Instant.now();
// TODO Allow configuration for ID Token time-to-live
Instant expiresAt = issuedAt.plus(30, ChronoUnit.MINUTES);
// @formatter:off
JwtClaimsSet.Builder claimsBuilder = JwtClaimsSet.builder();
if (StringUtils.hasText(issuer)) {
claimsBuilder.issuer(issuer);
}
claimsBuilder
.subject(subject)
.audience(Collections.singletonList(registeredClient.getClientId()))
.issuedAt(issuedAt)
.expiresAt(expiresAt)
.claim(IdTokenClaimNames.AZP, registeredClient.getClientId());
if (StringUtils.hasText(nonce)) {
claimsBuilder.claim(IdTokenClaimNames.NONCE, nonce);
}
// TODO Add 'auth_time' claim
// @formatter:on
return claimsBuilder;
}
}
OAuth2ConfigurerUtils
/**
* 复制security底层的工具类
*/
public class OAuth2ConfigurerUtils {
private OAuth2ConfigurerUtils() {
}
public static RegisteredClientRepository getRegisteredClientRepository(HttpSecurity httpSecurity) {
RegisteredClientRepository registeredClientRepository = (RegisteredClientRepository)httpSecurity.getSharedObject(RegisteredClientRepository.class);
if (registeredClientRepository == null) {
registeredClientRepository = (RegisteredClientRepository)getBean(httpSecurity, RegisteredClientRepository.class);
httpSecurity.setSharedObject(RegisteredClientRepository.class, registeredClientRepository);
}
return registeredClientRepository;
}
public static OAuth2AuthorizationService getAuthorizationService(HttpSecurity httpSecurity) {
OAuth2AuthorizationService authorizationService = (OAuth2AuthorizationService)httpSecurity.getSharedObject(OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = (OAuth2AuthorizationService)getOptionalBean(httpSecurity, OAuth2AuthorizationService.class);
if (authorizationService == null) {
authorizationService = new InMemoryOAuth2AuthorizationService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationService.class, authorizationService);
}
return (OAuth2AuthorizationService)authorizationService;
}
public static OAuth2AuthorizationConsentService getAuthorizationConsentService(HttpSecurity httpSecurity) {
OAuth2AuthorizationConsentService authorizationConsentService = (OAuth2AuthorizationConsentService)httpSecurity.getSharedObject(OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = (OAuth2AuthorizationConsentService)getOptionalBean(httpSecurity, OAuth2AuthorizationConsentService.class);
if (authorizationConsentService == null) {
authorizationConsentService = new InMemoryOAuth2AuthorizationConsentService();
}
httpSecurity.setSharedObject(OAuth2AuthorizationConsentService.class, authorizationConsentService);
}
return (OAuth2AuthorizationConsentService)authorizationConsentService;
}
public static OAuth2TokenGenerator<? extends OAuth2Token> getTokenGenerator(HttpSecurity httpSecurity) {
OAuth2TokenGenerator<? extends OAuth2Token> tokenGenerator = (OAuth2TokenGenerator)httpSecurity.getSharedObject(OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
tokenGenerator = (OAuth2TokenGenerator)getOptionalBean(httpSecurity, OAuth2TokenGenerator.class);
if (tokenGenerator == null) {
JwtGenerator jwtGenerator = getJwtGenerator(httpSecurity);
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2TokenCustomizer<OAuth2TokenClaimsContext> accessTokenCustomizer = getAccessTokenCustomizer(httpSecurity);
if (accessTokenCustomizer != null) {
accessTokenGenerator.setAccessTokenCustomizer(accessTokenCustomizer);
}
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
if (jwtGenerator != null) {
tokenGenerator = new DelegatingOAuth2TokenGenerator(new OAuth2TokenGenerator[]{jwtGenerator, accessTokenGenerator, refreshTokenGenerator});
} else {
tokenGenerator = new DelegatingOAuth2TokenGenerator(new OAuth2TokenGenerator[]{accessTokenGenerator, refreshTokenGenerator});
}
}
httpSecurity.setSharedObject(OAuth2TokenGenerator.class, tokenGenerator);
}
return (OAuth2TokenGenerator)tokenGenerator;
}
private static JwtGenerator getJwtGenerator(HttpSecurity httpSecurity) {
JwtGenerator jwtGenerator = (JwtGenerator)httpSecurity.getSharedObject(JwtGenerator.class);
if (jwtGenerator == null) {
JwtEncoder jwtEncoder = getJwtEncoder(httpSecurity);
if (jwtEncoder != null) {
jwtGenerator = new JwtGenerator(jwtEncoder);
OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer = getJwtCustomizer(httpSecurity);
if (jwtCustomizer != null) {
jwtGenerator.setJwtCustomizer(jwtCustomizer);
}
httpSecurity.setSharedObject(JwtGenerator.class, jwtGenerator);
}
}
return jwtGenerator;
}
private static JwtEncoder getJwtEncoder(HttpSecurity httpSecurity) {
JwtEncoder jwtEncoder = (JwtEncoder)httpSecurity.getSharedObject(JwtEncoder.class);
if (jwtEncoder == null) {
jwtEncoder = (JwtEncoder)getOptionalBean(httpSecurity, JwtEncoder.class);
if (jwtEncoder == null) {
JWKSource<SecurityContext> jwkSource = getJwkSource(httpSecurity);
if (jwkSource != null) {
jwtEncoder = new NimbusJwtEncoder(jwkSource);
}
}
if (jwtEncoder != null) {
httpSecurity.setSharedObject(JwtEncoder.class, jwtEncoder);
}
}
return (JwtEncoder)jwtEncoder;
}
public static JWKSource<SecurityContext> getJwkSource(HttpSecurity httpSecurity) {
JWKSource<SecurityContext> jwkSource = (JWKSource)httpSecurity.getSharedObject(JWKSource.class);
if (jwkSource == null) {
ResolvableType type = ResolvableType.forClassWithGenerics(JWKSource.class, new Class[]{SecurityContext.class});
jwkSource = (JWKSource)getOptionalBean(httpSecurity, type);
if (jwkSource != null) {
httpSecurity.setSharedObject(JWKSource.class, jwkSource);
}
}
return jwkSource;
}
private static OAuth2TokenCustomizer<JwtEncodingContext> getJwtCustomizer(HttpSecurity httpSecurity) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, new Class[]{JwtEncodingContext.class});
return (OAuth2TokenCustomizer)getOptionalBean(httpSecurity, type);
}
private static OAuth2TokenCustomizer<OAuth2TokenClaimsContext> getAccessTokenCustomizer(HttpSecurity httpSecurity) {
ResolvableType type = ResolvableType.forClassWithGenerics(OAuth2TokenCustomizer.class, new Class[]{OAuth2TokenClaimsContext.class});
return (OAuth2TokenCustomizer)getOptionalBean(httpSecurity, type);
}
public static AuthorizationServerSettings getAuthorizationServerSettings(HttpSecurity httpSecurity) {
AuthorizationServerSettings authorizationServerSettings = (AuthorizationServerSettings)httpSecurity.getSharedObject(AuthorizationServerSettings.class);
if (authorizationServerSettings == null) {
authorizationServerSettings = (AuthorizationServerSettings)getBean(httpSecurity, AuthorizationServerSettings.class);
httpSecurity.setSharedObject(AuthorizationServerSettings.class, authorizationServerSettings);
}
return authorizationServerSettings;
}
public static <T> T getBean(HttpSecurity httpSecurity, Class<T> type) {
return ((ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class)).getBean(type);
}
public static <T> T getBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = (ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length == 1) {
return (T) context.getBean(names[0]);
} else if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
} else {
throw new NoSuchBeanDefinitionException(type);
}
}
public static <T> T getOptionalBean(HttpSecurity httpSecurity, Class<T> type) {
Map<String, T> beansMap = BeanFactoryUtils.beansOfTypeIncludingAncestors((ListableBeanFactory)httpSecurity.getSharedObject(ApplicationContext.class), type);
if (beansMap.size() > 1) {
int var10003 = beansMap.size();
String var10004 = type.getName();
throw new NoUniqueBeanDefinitionException(type, var10003, "Expected single matching bean of type '" + var10004 + "' but found " + beansMap.size() + ": " + StringUtils.collectionToCommaDelimitedString(beansMap.keySet()));
} else {
return !beansMap.isEmpty() ? beansMap.values().iterator().next() : null;
}
}
public static <T> T getOptionalBean(HttpSecurity httpSecurity, ResolvableType type) {
ApplicationContext context = (ApplicationContext)httpSecurity.getSharedObject(ApplicationContext.class);
String[] names = context.getBeanNamesForType(type);
if (names.length > 1) {
throw new NoUniqueBeanDefinitionException(type, names);
} else {
return names.length == 1 ? (T) context.getBean(names[0]) : null;
}
}
}
效果截图
注册客户端
{"clientId":"123456",
"clientSecret":"8b30c1482ff973cfc92e51e1ec636966",
"clientName":"test",
"scopes":[
"super"
],
"clientAuthenticationMethods":["client_secret_post"],
"authorizationGrantTypes": ["refresh_token", "client_credentials", "sms_app,wechat_miniapp"]
}
客户端授权
grant_type:wechat_miniapp
scope:super
client_id:123456
client_secret:8b30c1482ff973cfc92e51e1ec636966
appId:123456789