spring-security-oauth2-authorization-server
旧依赖的移除
长久以来,使用Spring Security整合oauth2,都是使用Spring Security Oauth2这个系列的包:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>${spring.security.oauth2.version}</version>
</dependency>
然而,这个包现在已经被Spring官方移除了,现在实现相同的功能主要使用这几个Maven依赖:
<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>
可以看出除了Spring Security核心依赖,只是多了一个资源服务器的依赖。而之前使用的认证服务器,变成了一个新的项目,依赖如下:
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
这个资源服务器的依赖只到了0.3.1的版本号,还不到1.0.0,所以也算是一个新项目。不过我尝试了下,基本上已经可以用作生产环境了,只是这个包对于token持久化的支持,只支持内存储存和SQL储存,暂时并未提供之前常用的redis持久化,不知道这是不是Spring官方针对OAuth2这么多年混乱的标准作出的回应,统一使用JWT作为token确实是不需要服务端对token进行存储了。但是这里完全可以使用常规的Opaque token进行认证,需要实现RegisteredClientRepository、OAuth2AuthorizationService和OAuth2AuthorizationConsentService这几个类。
资源服务器的配置
对于资源服务器,主要作用是提供一些用户登录后需要的资源。调用接口就可以得到这些资源了。
Maven依赖
必要的Maven依赖如下:
<?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>
<groupId>com.example</groupId>
<artifactId>Code-Resources</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Code-Resources</name>
<description>Code-Resources</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<spring-boot.version>2.6.6</spring-boot.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.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>${spring-boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.6</version>
<configuration>
<mainClass>com.example.code.resources.CodeResourcesApplication</mainClass>
</configuration>
<executions>
<execution>
<id>repackage</id>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
安全配置类
只需要对所有的接口开启认证:
package com.example.code.resources.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig {
@Bean
public SecurityFilterChain httpSecurityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity.authorizeRequests().anyRequest().authenticated()
.and().cors()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and().oauth2ResourceServer().jwt();
return httpSecurity.build();
}
}
由于前后端分离,所以将服务端的session策略设置为无状态。
配置文件
jwt模式
配置文件需要指定认证服务器的地址和认证的方式,在jwt模式下,资源服务器会请求认证服务器的/oauth2/jwks端点,拿到公钥以后对jwt进行验证。
server:
port: 9600
spring:
datasource:
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth_demo
application:
name: Code-Resources
security:
oauth2:
resourceserver:
jwt:
issuer-uri: http://127.0.0.1:9500
jackson:
default-property-inclusion: non_null
opaquetoken模式
也可以选择opaquetoken这种常规的redis验证方式。如果选用Opaque token模式,相对应的端点就是/oauth2/introspect。
资源提供
写一个controller,要求一定的权限。对于这个权限信息,在生成的access_token,即jwt的playload部分里本身是包含的,而资源服务器在校验权限时,会通过网络请求认证服务器获取认证服务器那边的公钥:
public Resource retrieveResource(URL url) throws IOException {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Arrays.asList(MediaType.APPLICATION_JSON, APPLICATION_JWK_SET_JSON));
ResponseEntity<String> response = this.getResponse(url, headers);
if (response.getStatusCodeValue() != 200) {
throw new IOException(response.toString());
} else {
return new Resource((String)response.getBody(), "UTF-8");
}
}
private ResponseEntity<String> getResponse(URL url, HttpHeaders headers) throws IOException {
try {
RequestEntity<Void> request = new RequestEntity(headers, HttpMethod.GET, url.toURI());
return this.restOperations.exchange(request, String.class);
} catch (Exception var4) {
throw new IOException(var4);
}
}
公钥可以鉴别jwt的playload部分有没有被篡改过。
package com.example.code.resources.controller;
import com.example.code.resources.entity.ClientEntity;
import com.example.code.resources.entity.TokenEntity;
import com.example.code.resources.entity.UserClientEntity;
import com.example.code.resources.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.jwt.Jwt;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
@RestController
public class UserController {
JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@RequestMapping("/getResources")
@CrossOrigin
@PreAuthorize("hasAuthority('SCOPE_message.read')")
public Map<String, Object> getResources() {
HashMap<String, Object> map = new HashMap<>();
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
String userSql = "select * from oauth2_user where username = ?";
UserEntity user = jdbcTemplate.queryForObject(userSql, new BeanPropertyRowMapper<>(UserEntity.class), username);
String queryClientSql = "select * from oauth2_authorization_consent where principal_name = ?";
UserClientEntity userClientEntity = jdbcTemplate.queryForObject(queryClientSql, new BeanPropertyRowMapper<>(UserClientEntity.class), username);
String clientSql = "select * from oauth2_registered_client where id = ?";
Optional<UserClientEntity> userClientEntityOptional = Optional.ofNullable(userClientEntity);
if (userClientEntityOptional.isPresent()) {
ClientEntity clientEntity = jdbcTemplate.queryForObject(clientSql, new BeanPropertyRowMapper<>(ClientEntity.class), userClientEntityOptional.get().getRegisteredClientId());
map.put("clientInfo", clientEntity);
}
String tokenSql = "select * from oauth2_authorization where access_token_value = ?";
Jwt jwt = (Jwt) authentication.getCredentials();
String token = jwt.getTokenValue();
TokenEntity tokenEntity = jdbcTemplate.queryForObject(tokenSql, new BeanPropertyRowMapper<>(TokenEntity.class), token);
map.put("tokenInfo", tokenEntity);
map.put("userInfo", user);
return map;
}
}
认证服务器的配置
认证服务器主要负责access_token的生成、refresh_token的生成和通过refresh_token换取access_token的逻辑。在jwt下,access_token一旦签发就无法管理,即便使用refresh_token换取了新的access_token,那旧的access_token仍然是可用的。
Maven配置
<?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">
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.6</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>code</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>code</name>
<description>code</description>
<properties>
<java.version>17</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<dependencies>
<!--spring-authorization-server依赖-->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>0.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.22</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.27</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<source>17</source>
<target>17</target>
<encoding>UTF-8</encoding>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.6.2</version>
<configuration>
<mainClass>com.example.code.authorization.CodeAuthorizationApplication</mainClass>
</configuration>
</plugin>
</plugins>
</build>
</project>
主要的依赖只需要一个认证服务器,不需要Spring Security的核心包。
跨域配置
一般架构使用前后端分离,所以设置跨域:
package com.example.code.authorization.config;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
import java.util.List;
@Component
@Order(Ordered.HIGHEST_PRECEDENCE)
public class CustomerCorsFilter extends org.springframework.web.filter.CorsFilter {
public CustomerCorsFilter() {
super(configurationSource());
}
private static UrlBasedCorsConfigurationSource configurationSource() {
CorsConfiguration corsConfig = new CorsConfiguration();
List<String> allowedHeaders = Arrays.asList("x-auth-token", "content-type", "X-Requested-With", "XMLHttpRequest","Access-Control-Allow-Origin","Authorization","authorization");
List<String> exposedHeaders = Arrays.asList("x-auth-token", "content-type", "X-Requested-With", "XMLHttpRequest","Access-Control-Allow-Origin","Authorization","authorization");
List<String> allowedMethods = Arrays.asList("POST", "GET", "DELETE", "PUT", "OPTIONS");
List<String> allowedOrigins = List.of("*");
corsConfig.setAllowedHeaders(allowedHeaders);
corsConfig.setAllowedMethods(allowedMethods);
corsConfig.setAllowedOriginPatterns(allowedOrigins);
corsConfig.setExposedHeaders(exposedHeaders);
corsConfig.setMaxAge(36000L);
corsConfig.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", corsConfig);
return source;
}
}
配置文件
不需要额外的配置。
server:
port: 9500
spring:
application:
name: Code-Authorization
datasource:
username: root
password: 12345678
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/oauth_demo
认证服务器配置类
对于配置文件,如果不计划开启oidc协议,那使用open id环绕的代码是可以删除的。为什么使用oidc,本质只是为了实现一种规范,调取/userinfo接口去获取用户的一些登录信息,同时获取id_token来解析一些用户信息。
package com.example.code.authorization.config;
import com.example.code.authorization.entity.UserEntity;
import com.example.code.authorization.service.UserService;
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.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.authorization.OidcClientRegistrationEndpointConfigurer;
import org.springframework.security.config.annotation.web.configurers.oauth2.server.resource.OAuth2ResourceServerConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.*;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.jose.jws.SignatureAlgorithm;
import org.springframework.security.oauth2.jwt.*;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
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.oauth2.server.authorization.config.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.*;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
import org.springframework.security.web.util.matcher.RequestMatcher;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
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.*;
@Configuration
public class SecurityConfig {
JdbcTemplate jdbcTemplate;
UserService userService;
@Autowired
public void setUserService(UserService userService) {
this.userService = userService;
}
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
/*
open id
*/
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
/*
open id
*/
@Bean
public JwtEncoder jwtEncoder() {
return new NimbusJwtEncoder(jwkSource());
}
@Bean
public OAuth2TokenGenerator<?> tokenGenerator() {
JwtGenerator jwtGenerator = new JwtGenerator(jwtEncoder());
jwtGenerator.setJwtCustomizer(jwtCustomizer());
OAuth2AccessTokenGenerator accessTokenGenerator = new OAuth2AccessTokenGenerator();
OAuth2RefreshTokenGenerator refreshTokenGenerator = new OAuth2RefreshTokenGenerator();
return new DelegatingOAuth2TokenGenerator(jwtGenerator, accessTokenGenerator, refreshTokenGenerator);
}
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwsHeader.Builder headers = context.getHeaders();
JwtClaimsSet.Builder claims = context.getClaims();
Map<String, Object> map = claims.build().getClaims();
if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// Customize headers/claims for access_token
// headers.header("customerHeader", "这是一个自定义header");
// claims.claim("customerClaim", "这是一个自定义Claim");
String username = (String) map.get("sub");
String sql = "select avatar, url from oauth_demo.oauth2_user where username = ?";
UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
if (userEntityOptional.isPresent()) {
claims.claim("url", userEntityOptional.get().getUrl());
claims.claim("avatar", userEntityOptional.get().getAvatar());
}
}
};
}
/**
* 端点的 Spring Security 过滤器链
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfigurer<HttpSecurity> authorizationServerConfigurer =
new OAuth2AuthorizationServerConfigurer<>();
/*
open id
*/
authorizationServerConfigurer
.oidc(oidc -> {
// 用户信息
oidc.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint.userInfoMapper(oidcUserInfoAuthenticationContext -> {
String username = oidcUserInfoAuthenticationContext.getAuthorization().getPrincipalName();
String sql = "select url from oauth_demo.oauth2_user where username = ?";
UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
Map<String, Object> claims = new HashMap<>();
if (userEntityOptional.isPresent()) {
claims.put("url", userEntity.getUrl());
}
claims.put("sub", username);
return new OidcUserInfo(claims);
}));
// 客户端注册
oidc.clientRegistrationEndpoint(Customizer.withDefaults());
}
);
http.oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt);
/*
open id
*/
RequestMatcher endpointsMatcher = authorizationServerConfigurer
.getEndpointsMatcher();
http
.requestMatcher(endpointsMatcher)
.authorizeRequests(authorizeRequests ->
authorizeRequests.anyRequest().authenticated()
)
.userDetailsService(userService)
.csrf(AbstractHttpConfigurer::disable)
.apply(authorizationServerConfigurer);
//未通过身份验证时重定向到登录页面授权端点
http.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
/**
* 用于身份验证的 Spring Security 过滤器链
* @param http
* @return
* @throws Exception
*/
@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
//表单登录处理从授权服务器过滤器链
.formLogin(Customizer.withDefaults());
return http.build();
}
/**
* 返回注册客户端资源,注意这里采用的是内存模式,后续可以改成jdbc模式。RegisteredClientRepository用于管理客户端的实例。
* @return
*/
@Bean
public RegisteredClientRepository registeredClientRepository() {
// RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString())
// .clientId("messaging-client")
// .clientSecret(passwordEncoder().encode("secret"))
// .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
// .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
// .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
// .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
// .redirectUri("http://www.baidu.com")
// .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc")
// .scope(OidcScopes.OPENID)
// .scope("message.read")
// .scope("message.write")
// .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
// .tokenSettings(TokenSettings.builder()
// // token有效期100分钟
// .accessTokenTimeToLive(Duration.ofMinutes(100L))
// // 使用默认JWT相关格式
// .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
// // 开启刷新token
// .reuseRefreshTokens(true)
// // refreshToken有效期120分钟
// .refreshTokenTimeToLive(Duration.ofMinutes(120L))
// .idTokenSignatureAlgorithm(SignatureAlgorithm.RS256).build()
// )
// .build();
JdbcRegisteredClientRepository registeredClientRepository = new JdbcRegisteredClientRepository(jdbcTemplate);
// RegisteredClient client = registeredClientRepository.findByClientId("messaging-client");
// Optional<RegisteredClient> clientOptional = Optional.ofNullable(client);
// if (clientOptional.isEmpty()) {
// registeredClientRepository.save(registeredClient);
// }
return registeredClientRepository;
}
@Bean
public OAuth2AuthorizationService authorizationService() {
return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository());
}
/**
* 授权确认信息处理服务
*/
@Bean
public OAuth2AuthorizationConsentService authorizationConsentService() {
return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository());
}
/**
* 生成jwk资源,com.nimbusds.jose.jwk.source.JWKSource用于签署访问令牌的实例。
* @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);
}
/**
* 生成密钥对,启动时生成的带有密钥的实例java.security.KeyPair用于创建JWKSource上述内容
* @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;
}
/**
* ProviderSettings配置 Spring Authorization Server的实例
* @return
*/
@Bean
public ProviderSettings providerSettings() {
return ProviderSettings.builder().build();
}
}
UserService
用户登录时使用:
package com.example.code.authorization.service;
import com.example.code.authorization.entity.UserEntity;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;
import java.util.Collections;
import java.util.Optional;
@Component
public class UserService implements UserDetailsService {
JdbcTemplate jdbcTemplate;
@Autowired
public void setJdbcTemplate(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
String sql = "select id, username, password from oauth_demo.oauth2_user where username = ?";
UserEntity user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
Optional<UserEntity> userOptional = Optional.ofNullable(user);
if (userOptional.isEmpty()) {
throw new UsernameNotFoundException("user is not exist");
}
return new User(userOptional.get().getUsername(), userOptional.get().getPassword(), true,true,true,true, Collections.emptyList());
}
}
关于OIDC
OIDC是在OAuth2协议基础上的一个认证层,通过使用access_token调用/userinfo接口获取一些用户信息。这个/userinfo接口只能给open_id的scope请求调用,其他scope的请求是无法调用这个接口的。同时,生成access_token请求的响应相对于普通的响应,会多出一个id_token,这个id_token可以用于解析身份信息:
@Bean
public OAuth2TokenCustomizer<JwtEncodingContext> jwtCustomizer() {
return context -> {
JwsHeader.Builder headers = context.getHeaders();
JwtClaimsSet.Builder claims = context.getClaims();
Map<String, Object> map = claims.build().getClaims();
if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
// Customize headers/claims for access_token
// headers.header("customerHeader", "这是一个自定义header");
// claims.claim("customerClaim", "这是一个自定义Claim");
String username = (String) map.get("sub");
String sql = "select avatar, url from oauth_demo.oauth2_user where username = ?";
UserEntity userEntity = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(UserEntity.class), username);
Optional<UserEntity> userEntityOptional = Optional.ofNullable(userEntity);
if (userEntityOptional.isPresent()) {
claims.claim("url", userEntityOptional.get().getUrl());
claims.claim("avatar", userEntityOptional.get().getAvatar());
}
}
};
}
对id_token的解析是直接在前端进行的,这个token不能用于后端接口的权限验证,作用仅仅只是储存一些信息,例如用户性别、头像等信息:
parseJwt(token) {
let strings = token.split("."); //截取token,获取载体
this.jwt = JSON.parse(window.atob(strings[1].replace(/-/g, "+").replace(/_/g, "/")))
},
本文来自博客园,作者:imissinstagram,转载请注明原文链接:https://www.cnblogs.com/LostSecretGarden/p/16741249.html