帮助文档
专业提供香港服务器、香港云服务器、香港高防服务器租用、香港云主机、台湾服务器、美国服务器、美国云服务器vps租用、韩国高防服务器租用、新加坡服务器、日本服务器租用 一站式全球网络解决方案提供商!专业运营维护IDC数据中心,提供高质量的服务器托管,服务器机房租用,服务器机柜租用,IDC机房机柜租用等服务,稳定、安全、高性能的云端计算服务,实时满足您的多样性业务需求。 香港大带宽稳定可靠,高级工程师提供基于服务器硬件、操作系统、网络、应用环境、安全的免费技术支持。
服务器资讯 / 香港服务器租用 / 香港VPS租用 / 香港云服务器 / 美国服务器租用 / 台湾服务器租用 / 日本服务器租用 / 官方公告 / 帮助文档
OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client
发布时间:2024-03-09 17:41:49   分类:帮助文档
OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client title: OAuth2.0 实践 Spring Authorization Server 搭建授权服务器 + Resource + Client date: 2023-03-27 01:41:26 tags: OAuth2.0Spring Authorization Server categories:开发实践 cover: https://cover.png feature: false 1. 授权服务器 目前 Spring 生态中的 OAuth2 授权服务器是 Spring Authorization Server,原先的 Spring Security OAuth 已经停止更新 1.1 引入依赖 这里的 spring-security-oauth2-authorization-server 用的是 0.4.0 版本,适配 JDK 1.8,Spring Boot 版本为 2.7.7 org.springframework.boot spring-boot-starter-security org.springframework.security spring-security-oauth2-authorization-server org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-jdbc mysql mysql-connector-java 1.2 配置类 可以参考官方的 Samples:spring-authorization-server/samples 1.2.1 最小配置 官网最小配置 Demo 地址:Getting Started 官网最小配置如下,通过添加该配置类,启动项目,这就能够完成 OAuth2 的授权 @Configuration public class SecurityConfig { @Bean @Order(1) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); http.getConfigurer(OAuth2AuthorizationServerConfigurer.class) .oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0 http // Redirect to the login page when not authenticated from the // authorization endpoint .exceptionHandling((exceptions) -> exceptions .authenticationEntryPoint( new LoginUrlAuthenticationEntryPoint("/login")) ) // Accept access tokens for User Info and/or Client Registration .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); } @Bean @Order(2) public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests((authorize) -> authorize .anyRequest().authenticated() ) // Form login handles the redirect to the login page from the // authorization server filter chain .formLogin(Customizer.withDefaults()); return http.build(); } @Bean public UserDetailsService userDetailsService() { UserDetails userDetails = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(userDetails); } @Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("messaging-client") .clientSecret("{noop}secret") .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri("http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc") .redirectUri("http://127.0.0.1:8080/authorized") .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) .scope("message.read") .scope("message.write") .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) .build(); return new InMemoryRegisteredClientRepository(registeredClient); } @Bean public JWKSource 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); } 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; } @Bean public JwtDecoder jwtDecoder(JWKSource jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } } 在上面的 Demo 里,将所有配置都写在了一个配置类 SecurityConfig 里,实际上 Spring Authorization Server 还提供了一种实现最小配置的默认配置形式,就是通过 OAuth2AuthorizationServerConfiguration 这个类,源码如下: @Configuration(proxyBeanMethods = false) public class OAuth2AuthorizationServerConfiguration { @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { applyDefaultSecurity(http); return http.build(); } // @formatter:off public static void applyDefaultSecurity(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfigurer authorizationServerConfigurer = new OAuth2AuthorizationServerConfigurer(); RequestMatcher endpointsMatcher = authorizationServerConfigurer .getEndpointsMatcher(); http .requestMatcher(endpointsMatcher) .authorizeRequests(authorizeRequests -> authorizeRequests.anyRequest().authenticated() ) .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .apply(authorizationServerConfigurer); } // @formatter:on public static JwtDecoder jwtDecoder(JWKSource jwkSource) { Set jwsAlgs = new HashSet<>(); jwsAlgs.addAll(JWSAlgorithm.Family.RSA); jwsAlgs.addAll(JWSAlgorithm.Family.EC); jwsAlgs.addAll(JWSAlgorithm.Family.HMAC_SHA); ConfigurableJWTProcessor jwtProcessor = new DefaultJWTProcessor<>(); JWSKeySelector jwsKeySelector = new JWSVerificationKeySelector<>(jwsAlgs, jwkSource); jwtProcessor.setJWSKeySelector(jwsKeySelector); // Override the default Nimbus claims set verifier as NimbusJwtDecoder handles it instead jwtProcessor.setJWTClaimsSetVerifier((claims, context) -> { }); return new NimbusJwtDecoder(jwtProcessor); } @Bean RegisterMissingBeanPostProcessor registerMissingBeanPostProcessor() { RegisterMissingBeanPostProcessor postProcessor = new RegisterMissingBeanPostProcessor(); postProcessor.addBeanDefinition(AuthorizationServerSettings.class, () -> AuthorizationServerSettings.builder().build()); return postProcessor; } } 这里注入一个叫做 authorizationServerSecurityFilterChain 的 bean,其实对比一下可以看出,这和最小配置的实现基本是相同的。有了这个 bean,就会支持如下协议端点: OAuth2 Authorization endpointOAuth2 Token endpointOAuth2 Token Introspection endpointOAuth2 Token Revocation endpointOAuth2 Authorization Server Metadata endpointJWK Set endpointOpenID Connect 1.0 Provider Configuration endpointOpenID Connect 1.0 UserInfo endpoint 接下来使用 OAuth2AuthorizationServerConfiguration 这个类来实现一个 Authorization Server,将 Spring Security 和 Authorization Server 的配置分开,Spring Security 使用 SecurityConfig 类,创建一个新的Authorization Server 配置类 AuthorizationServerConfig 1.2.2 ServerSecurityConfig @EnableWebSecurity @Configuration(proxyBeanMethods = false) public class ServerSecurityConfig { @Resource private DataSource dataSource; / * Spring Security 的过滤器链,用于 Spring Security 的身份认证 */ @Bean SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> authorize // 配置放行的请求 .antMatchers("/api/", "/login").permitAll() // 其他任何请求都需要认证 .anyRequest().authenticated() ) // 设置登录表单页面 .formLogin(formLoginConfigurer -> formLoginConfigurer.loginPage("/login")); return http.build(); } // @Bean // public UserDetailsService userDetailsService() { // return new JdbcUserDetailsManager(dataSource); // } @Bean UserDetailsManager userDetailsManager() { return new JdbcUserDetailsManager(dataSource); } } Spring Authorization Server 默认是支持内存和 JDBC 两种存储模式的,内存模式只适合简单的测试,所以这里使用 JDBC 存储模式。在 1.2.1 最小配置那节里注入 UserDetailsService 这个 Bean 使用的是 InMemoryUserDetailsManager,表示内存模式,这里使用 JdbcUserDetailsManager 表示 JDBC 模式 而这两个类都属于 UserDetailsManager 接口的实现类,并且后续我们需要使用到 userDetailsManager.createUser(userDetails) 方法来添加用户,因此这里需要注入 UserDetailsManager 这个 Bean,由于返回的都是 JdbcUserDetailsManager,因此可以注释掉 UserDetailsService 这个 Bean 的注入 1.2.3 AuthorizationServerConfig 该类部分配置可以参照前面提到的 OAuth2AuthorizationServerConfiguration 类来配置,同样使用 JDBC 存储模式 @Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { // 定义授权服务配置器 OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer(); configurer // 自定义授权页面 .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) // Enable OpenID Connect 1.0, 启用 OIDC 1.0 .oidc(Customizer.withDefaults()); // 获取授权服务器相关的请求端点 RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher(); http // 拦截对授权服务器相关端点的请求 .requestMatcher(endpointsMatcher) // 拦载到的请求需要认证 .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的 .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // 访问端点时表单登录 .formLogin() .and() // 应用授权服务器的配置 .apply(configurer); return http.build(); } / * 注册客户端应用, 对应 oauth2_registered_client 表 */ @Bean public RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) { return new JdbcRegisteredClientRepository(jdbcTemplate); } / * 令牌的发放记录, 对应 oauth2_authorization 表 */ @Bean public OAuth2AuthorizationService authorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository); } / * 把资源拥有者授权确认操作保存到数据库, 对应 oauth2_authorization_consent 表 */ @Bean public OAuth2AuthorizationConsentService authorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) { return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository); } / * 加载 JWT 资源, 用于生成令牌 */ @Bean public JWKSource jwkSource() { KeyPair keyPair; try { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(2048); keyPair = keyPairGenerator.generateKeyPair(); } catch (Exception ex) { throw new IllegalStateException(ex); } 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 (jwkSelector, securityContext) -> jwkSelector.select(jwkSet); } / * JWT 解码 */ @Bean public JwtDecoder jwtDecoder(JWKSource jwkSource) { return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource); } / * AuthorizationServerS 的相关配置 */ @Bean public AuthorizationServerSettings authorizationServerSettings() { return AuthorizationServerSettings.builder().build(); } } 1.3 创建数据库表 一共包括 5 个表,其中 Spring Security 相关的有 2 个表,user 和 authorities,用户表和权限表,该表的建表 SQL 在 org\springframework\security\core\userdetails\jdbc\users.ddl SQL 可能会有一些问题,根据自己使用的数据库进行更改 create table users(username varchar_ignorecase(50) not null primary key,password varchar_ignorecase(500) not null,enabled boolean not null); create table authorities (username varchar_ignorecase(50) not null,authority varchar_ignorecase(50) not null,constraint fk_authorities_users foreign key(username) references users(username)); create unique index ix_auth_username on authorities (username,authority); Spring authorization Server 有 3 个表,建表 SQL 在: org\springframework\security\oauth2\server\authorization\oauth2-authorization-consent-schema.sql org\springframework\security\oauth2\server\authorization\oauth2-authorization-schema.sql org\springframework\security\oauth2\server\authorization\client\oauth2-registered-client-schema.sql CREATE TABLE oauth2_authorization_consent ( registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorities varchar(1000) NOT NULL, PRIMARY KEY (registered_client_id, principal_name) ); /* IMPORTANT: If using PostgreSQL, update ALL columns defined with 'blob' to 'text', as PostgreSQL does not support the 'blob' data type. */ CREATE TABLE oauth2_authorization ( id varchar(100) NOT NULL, registered_client_id varchar(100) NOT NULL, principal_name varchar(200) NOT NULL, authorization_grant_type varchar(100) NOT NULL, authorized_scopes varchar(1000) DEFAULT NULL, attributes blob DEFAULT NULL, state varchar(500) DEFAULT NULL, authorization_code_value blob DEFAULT NULL, authorization_code_issued_at timestamp DEFAULT NULL, authorization_code_expires_at timestamp DEFAULT NULL, authorization_code_metadata blob DEFAULT NULL, access_token_value blob DEFAULT NULL, access_token_issued_at timestamp DEFAULT NULL, access_token_expires_at timestamp DEFAULT NULL, access_token_metadata blob DEFAULT NULL, access_token_type varchar(100) DEFAULT NULL, access_token_scopes varchar(1000) DEFAULT NULL, oidc_id_token_value blob DEFAULT NULL, oidc_id_token_issued_at timestamp DEFAULT NULL, oidc_id_token_expires_at timestamp DEFAULT NULL, oidc_id_token_metadata blob DEFAULT NULL, refresh_token_value blob DEFAULT NULL, refresh_token_issued_at timestamp DEFAULT NULL, refresh_token_expires_at timestamp DEFAULT NULL, refresh_token_metadata blob DEFAULT NULL, PRIMARY KEY (id) ); CREATE TABLE oauth2_registered_client ( id varchar(100) NOT NULL, client_id varchar(100) NOT NULL, client_id_issued_at timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL, client_secret varchar(200) DEFAULT NULL, client_secret_expires_at timestamp DEFAULT NULL, client_name varchar(200) NOT NULL, client_authentication_methods varchar(1000) NOT NULL, authorization_grant_types varchar(1000) NOT NULL, redirect_uris varchar(1000) DEFAULT NULL, scopes varchar(1000) NOT NULL, client_settings varchar(2000) NOT NULL, token_settings varchar(2000) NOT NULL, PRIMARY KEY (id) ); 创建完成后的数据库表如下: 1.4 自定义登录和授权页面 在项目 resource 目录下创建一个 templates 文件夹,然后创建 login.html 和 consent.html,登录页面的配置在 1.2.2 中配置好了,授权页面的配置在 1.2.3 中配置好了 登录页面 login.html Spring Security Example
创建 LoginConroller,用于跳转到 login.html 页面 @Controller public class LoginController { @GetMapping("/login") public String login() { return "login"; } } 授权页面 consent.html Custom consent page - Consent required

应用程序权限

应用程序 想要访问您的帐户

上述应用程序请求以下权限
如果您批准,请查看这些并同意

您已向上述应用授予以下权限:

Your consent to provide access is required.
If you do not approve, click Cancel, in which case no information will be shared with the app.

创建 AuthorizationConsentController,用于跳转到 consent.html 页面 @Controller public class AuthorizationConsentController { private final RegisteredClientRepository registeredClientRepository; private final OAuth2AuthorizationConsentService authorizationConsentService; public AuthorizationConsentController(RegisteredClientRepository registeredClientRepository, OAuth2AuthorizationConsentService authorizationConsentService) { this.registeredClientRepository = registeredClientRepository; this.authorizationConsentService = authorizationConsentService; } @GetMapping(value = "/oauth2/consent") public String consent(Principal principal, Model model, @RequestParam(OAuth2ParameterNames.CLIENT_ID) String clientId, @RequestParam(OAuth2ParameterNames.SCOPE) String scope, @RequestParam(OAuth2ParameterNames.STATE) String state) { // 要批准的范围和以前批准的范围 Set scopesToApprove = new HashSet<>(); Set previouslyApprovedScopes = new HashSet<>(); // 查询 clientId 是否存在 RegisteredClient registeredClient = this.registeredClientRepository.findByClientId(clientId); // 查询当前的授权许可 OAuth2AuthorizationConsent currentAuthorizationConsent = this.authorizationConsentService.findById(registeredClient.getId(), principal.getName()); // 已授权范围 Set authorizedScopes; if (currentAuthorizationConsent != null) { authorizedScopes = currentAuthorizationConsent.getScopes(); } else { authorizedScopes = Collections.emptySet(); } for (String requestedScope : StringUtils.delimitedListToStringArray(scope, " ")) { if (OidcScopes.OPENID.equals(requestedScope)) { continue; } // 如果已授权范围包含了请求范围,则添加到以前批准的范围的 Set, 否则添加到要批准的范围 if (authorizedScopes.contains(requestedScope)) { previouslyApprovedScopes.add(requestedScope); } else { scopesToApprove.add(requestedScope); } } model.addAttribute("clientId", clientId); model.addAttribute("state", state); model.addAttribute("scopes", withDescription(scopesToApprove)); model.addAttribute("previouslyApprovedScopes", withDescription(previouslyApprovedScopes)); model.addAttribute("principalName", principal.getName()); return "consent"; } private static Set withDescription(Set scopes) { Set scopeWithDescriptions = new HashSet<>(); for (String scope : scopes) { scopeWithDescriptions.add(new ScopeWithDescription(scope)); } return scopeWithDescriptions; } public static class ScopeWithDescription { private static final String DEFAULT_DESCRIPTION = "未知范围 - 我们无法提供有关此权限的信息, 请在授予此权限时谨慎"; private static final Map scopeDescriptions = new HashMap<>(); static { scopeDescriptions.put( OidcScopes.PROFILE, "此应用程序将能够读取您的个人资料信息" ); scopeDescriptions.put( "message.read", "此应用程序将能够读取您的信息" ); scopeDescriptions.put( "message.write", "此应用程序将能够添加新信息, 它还可以编辑和删除现有信息" ); scopeDescriptions.put( "other.scope", "这是范围描述的另一个范围示例" ); } public final String scope; public final String description; ScopeWithDescription(String scope) { this.scope = scope; this.description = scopeDescriptions.getOrDefault(scope, DEFAULT_DESCRIPTION); } } } 1.5 ServerController 用于添加用户信息和客户端信息,这里的 passwordEncoder 使用 BCryptPasswordEncoder 进行加解密,{bcrypt} 表示加密,{noop} 表示明文 @RestController public class ServerController { @Resource private UserDetailsManager userDetailsManager; @GetMapping("/api/addUser") public String addUser() { UserDetails userDetails = User.builder().passwordEncoder(s -> "{bcrypt}" + new BCryptPasswordEncoder().encode(s)) .username("fan") .password("fan") .roles("ADMIN") .build(); userDetailsManager.createUser(userDetails); return "添加用户成功"; } @Resource private RegisteredClientRepository registeredClientRepository; @GetMapping("/api/addClient") public String addClient() { // JWT(Json Web Token)的配置项:TTL、是否复用refreshToken等等 TokenSettings tokenSettings = TokenSettings.builder() // 令牌存活时间:2小时 .accessTokenTimeToLive(Duration.ofHours(2)) // 令牌可以刷新,重新获取 .reuseRefreshTokens(true) // 刷新时间:30天(30天内当令牌过期时,可以用刷新令牌重新申请新令牌,不需要再认证) .refreshTokenTimeToLive(Duration.ofDays(30)) .build(); // 客户端相关配置 ClientSettings clientSettings = ClientSettings.builder() // 是否需要用户授权确认 .requireAuthorizationConsent(true) .build(); RegisteredClient registeredClient = RegisteredClient.withId(UUID.randomUUID().toString()) // 客户端ID和密码 .clientId("messaging-client") // .clientSecret("{bcrypt}" + new BCryptPasswordEncoder().encode("secret")) .clientSecret("{noop}secret") // 授权方法 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) // 授权模式(授权码模式) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 刷新令牌(授权码模式) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) // 回调地址:授权服务器向当前客户端响应时调用下面地址, 不在此列的地址将被拒绝, 只能使用IP或域名,不能使用 localhost .redirectUri("http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc") // OIDC 支持 .scope(OidcScopes.OPENID) .scope(OidcScopes.PROFILE) // 授权范围(当前客户端的授权范围) .scope("message.read") .scope("message.write") // JWT(Json Web Token)配置项 .tokenSettings(tokenSettings) // 客户端配置项 .clientSettings(clientSettings) .build(); registeredClientRepository.save(registeredClient); return "添加客户端信息成功"; } } 1.6 YAML 配置 配置数据库连接信息 server: port: 9000 spring: datasource: url: jdbc:mysql://localhost:3306/unified_certification?useSSL=false&useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC driver-class-name: com.mysql.cj.jdbc.Driver username: root password: root 1.7 测试 完整目录结构如下: 1.7.1 添加用户和客户端信息 启动项目,访问 http://127.0.0.1:9000/api/addUser 查询数据库 users 和 authorities 表,已有用户和权限信息 访问 http://127.0.0.1:9000/api/addClient 查询数据库 oauth2_registered_client 表,已有客户端信息 1.7.2 授权码模式获取令牌 有关 OAuth2.0 的相关知识可见:OAuth2.0 实战总结_凡 223 的博客 访问 http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=message.read&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,这里的 127.0.0.1:8000 其实为客户端地址,后面讲到客户端时,客户端的地址就为 8000 response_type:授权类型,code 为授权码模式client_id:客户端 ID,即前面注册客户端的时候定义的scope:请求的权限范围redirect_uri:回调地址,也是前面注册客户端的时候定义的 未登录,会跳转到登录页面 输入前面添加的用户信息,用户名和密码,然后会跳转到授权页面 选择是否授予权限,这里勾选后,点击提交,会跳转到回调地址,即 127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,由于这个地址还没有对应的服务,无法访问,但我们暂时需要的是地址栏的 code http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=z_3O1lEdxVsd2fn8_uKA481pO9caGd0N4x_Vbt0deuMA77sDis6fhMJkf2_9uM4KGYzLzv7ujbXZ2JAdg0ACyMapR38jnJruG2iz2XBgptKrru-IJobGVa6NTicgvCZ7 打开接口测试工具,这里我使用的是 Apifox,使用表单格式,包含三个参数 grant_type:授权类型,authorization_code 表示授权码模式code:即授权码,上面地址栏里返回给我们的 code 部分,复制到这里,code 使用一次就会失效redirect_uri:回调地址,与前面的一致。图中的地址忘记修改了,注意和前面请求 code 时写的回调地址一致,即 http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc,后面有类似问题同样修改 然后设置 Auth,Postman 里是 Authorization,选择 Basic Auth 类型,用户名密码则为注册客户端时的 client_id 和 clientSecret,客户端 ID 和密钥 保存,发送后,会给我们返回 access_token 和 refresh_token 将 access_token 复制到 JSON Web Tokens - jwt.io 网站,解析后可以看到 JWT 的信息,包括客户端 ID,权限范围,服务器地址等 1.7.3 授权码模式刷新令牌 在前面返回了 access_token 和 refresh_token,access_token 包含了授权信息,refresh_token 则是用来重新获取 access_token,同样是表单类型,包含两个参数 grant_type:refresh_token 表示刷新令牌refresh_token:即前面获取到的 refresh_token 的值 Auth 信息与前面一致 保存,发送后,会给我们返回新的 access_token 和 refresh_token,refresh_token 使用一次就会失效 1.7.4 客户端模式 同样使用表单格式,grant_type 值为 client_credentials Auth 与前面一致 保存,发送后,会给我们返回 access_token,没有 refresh_token。因为在授权码模式中的 access_token 是我们通过授权码 code 换来的,而授权码 code 是我们请求后授权得到的,为了不用每次获取 access_token 都需要重新请求授权,所以使用 refresh_token 来重新获取 access_token,refresh_token 和 access_token 都有过期时间,refresh_token 过期时间比 access_token 长 而客户端模式可以直接获取 access_token,所以也就不需要 refresh_token 了 1.7.5 OIDC 有关 OIDC 的相关知识同样可见:OAuth2.0 实战总结_凡 223 的博客 在前面 1.2.3 的配置和 1.5 的注册客户端时,已经支持了 OIDC,这里直接访问:http://127.0.0.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid&redirect_uri=http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc 这里的 scope 必须包含 openid 得到授权码 code http://127.0.0.1:8000/login/oauth2/code/messaging-client-oidc?code=NjvT1z3msYRsjvPPM4LP4EmlyBUixsKes_J6osSB3VAugXEKmyUappvtrmTWp7s_iQzoJsD8xOE3gUXawhMixL0fu2HC6UJv8CeZyCB-d2oiu4NnCO9uJcK1MXOm4poU 然后通过授权码 code 换取令牌,可以看到除了 access_token 和 refresh_token 外,还返回了一个 id_token 解析这个 id_token,信息如下,是我们的身份认证信息 再通过 refresh_token 重新获取令牌,同样也给我们返回了 id_token 通过 access_token,获取 OIDC 的用户端点 这里的 sub 就是用户的标志。在 1.2.3 的配置中,对于 OIDC 使用的是默认配置 我们也可以增加自定义信息,修改后的配置如下,其他配置不变 @Configuration(proxyBeanMethods = false) public class AuthorizationServerConfig { private static final String CUSTOM_CONSENT_PAGE_URI = "/oauth2/consent"; @Bean @Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { // 定义授权服务配置器 OAuth2AuthorizationServerConfigurer configurer = new OAuth2AuthorizationServerConfigurer(); configurer // 自定义授权页面 .authorizationEndpoint(authorizationEndpoint -> authorizationEndpoint.consentPage(CUSTOM_CONSENT_PAGE_URI)) // Enable OpenID Connect 1.0, 启用 OIDC 1.0 .oidc(oidcConfigurer -> oidcConfigurer.userInfoEndpoint(userInfoEndpointConfigurer -> userInfoEndpointConfigurer.userInfoMapper(userInfoAuthenticationContext -> { OAuth2AccessToken accessToken = userInfoAuthenticationContext.getAccessToken(); Map claims = MapUtil.map(false); claims.put("url", "http://127.0.0.1:9000"); claims.put("accessToken", accessToken); claims.put("sub", userInfoAuthenticationContext.getAuthorization().getPrincipalName()); return new OidcUserInfo(claims); }))); // 获取授权服务器相关的请求端点 RequestMatcher endpointsMatcher = configurer.getEndpointsMatcher(); http // 拦截对授权服务器相关端点的请求 .requestMatcher(endpointsMatcher) // 拦载到的请求需要认证 .authorizeHttpRequests(authorize -> authorize.anyRequest().authenticated()) // 忽略掉相关端点的 CSRF(跨站请求): 对授权端点的访问可以是跨站的 .csrf(csrf -> csrf.ignoringRequestMatchers(endpointsMatcher)) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt) // 访问端点时表单登录 .formLogin() .and() // 应用授权服务器的配置 .apply(configurer); return http.build(); } // ... 其他配置不变 } 重启项目,重新获取到 access_token,通过 access_token 访问用户端点,可以看到我们自定义的信息已经被添加了进来 2. 资源服务器 2.1 引入依赖 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-oauth2-resource-server cn.hutool hutool-all 2.2 YAML 配置 server: port: 8001 spring: security: oauth2: resourceserver: jwt: issuer-uri: http://localhost:9000 2.3 异常处理器 该部分为 Spring Security 相关知识,可见:Spring Security 总结_凡 223 的博客 2.3.1 认证失败处理器 Response 为自定义的统一结果返回类,这里的返回信息自定义即可 public class UnAccessDeniedHandler implements AccessDeniedHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { // 403, 未授权, 禁止访问 response.setStatus(HttpServletResponse.SC_FORBIDDEN); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); // 返回响应信息 ServletOutputStream outputStream = response.getOutputStream(); Response fail = Response.fail(HttpServletResponse.SC_FORBIDDEN, "UnAccessDeniedHandler-未授权, 不允许访问", "uri-" + request.getRequestURI()); outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8)); // 关闭流 outputStream.flush(); outputStream.close(); } } 2.3.2 鉴权失败处理器 public class UnAuthenticationEntryPoint implements AuthenticationEntryPoint { @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { if (authException instanceof InvalidBearerTokenException) { LogUtil.info("Token 登录失效"); } if (response.isCommitted()) { return; } // 401, 未认证 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_ACCEPTED); response.setCharacterEncoding("utf-8"); response.setContentType(MediaType.APPLICATION_JSON_VALUE); // 返回响应信息 ServletOutputStream outputStream = response.getOutputStream(); Response fail = Response.fail(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage() + "-UnAuthenticationEntryPoint-认证失败", "uri-" + request.getRequestURI()); outputStream.write(JSONUtil.toJsonStr(fail).getBytes(StandardCharsets.UTF_8)); // 关闭流 outputStream.flush(); outputStream.close(); } } 2.4 配置类 对资源请求配置了读、写、profile 权限 @EnableWebSecurity @Configuration(proxyBeanMethods = false) public class ResourceServerConfig { / * 资源管理器配置 * * @param http * @return {@link SecurityFilterChain} * @author Fan * @since 2023/2/2 9:30 */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { UnAuthenticationEntryPoint authenticationEntryPoint = new UnAuthenticationEntryPoint(); UnAccessDeniedHandler accessDeniedHandler = new UnAccessDeniedHandler(); http // security的session生成策略改为security不主动创建session, 即STALELESS // 资源服务不涉及用户登录, 仅靠token访问, 不需要seesion .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) .and() .authorizeHttpRequests(authorize -> authorize // 对 /resource1 的请求,需要 SCOPE_message.read 权限 .antMatchers("/resource1").hasAuthority("SCOPE_message.read") // 对 /resource2 的请求,需要 SCOPE_message.write 权限 .antMatchers("/resource2").hasAuthority("SCOPE_message.write") // 对 /resource3 的请求,需要 SCOPE_profile 权限 .antMatchers("/resource3").hasAuthority("SCOPE_profile") // 放行请求 .antMatchers("/api/").permitAll() // 其他任何请求都需要认证 .anyRequest().authenticated()) // 异常处理器 .exceptionHandling(exceptionConfigurer -> exceptionConfigurer // 认证失败 .authenticationEntryPoint(authenticationEntryPoint) // 鉴权失败 .accessDeniedHandler(accessDeniedHandler) ) // 资源服务 .oauth2ResourceServer(resourceServer -> resourceServer .authenticationEntryPoint(authenticationEntryPoint) .accessDeniedHandler(accessDeniedHandler) .jwt()); return http.build(); } } 2.5 Controller @RestController public class MessagesController { @GetMapping("/resource1") public Response getResource1(){ return Response.success("服务A -> 资源1 -> 读权限"); } @GetMapping("/resource2") public Response getResource2(){ return Response.success("服务A -> 资源2 -> 写权限"); } @GetMapping("/resource3") public Response resource3(){ return Response.success("服务A -> 资源3 -> profile 权限"); } @GetMapping("/api/publicResource") public Response publicResource() { return Response.success("服务A -> 公共资源"); } } 2.6 测试 完整目录结构如下: 启动项目,打开 Apifox,直接请求时,会提示我们认证失败,即上面认证失败处理器的响应结果 添加 Auth,类型选择 Bearer Token,Token 的值即为前面获取到的 access_token 的值 保存,发送后,即可获取资源 resource1 再获取资源 resource2,提示没有权限,这里返回的信息即为鉴权失败处理器的响应信息。因为在我们申请权限的时候只申请了 message.read 权限,同时也只授权了 message.read 权限,而 resource2 需要 message.write 权限,因此鉴权失败,无法访问 3. 客户端 3.1 引入依赖 org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf org.springframework.boot spring-boot-starter-oauth2-client org.springframework.boot spring-boot-starter-security org.springframework.boot spring-boot-starter-webflux cn.hutool hutool-all 3.2 YAML 配置 server: port: 8000 spring: application: name: messages-client security: oauth2: client: registration: messaging-client-oidc: provider: authorization-server client-id: messaging-client client-secret: secret authorization-grant-type: authorization_code # redirect-uri: "127.0.0.1:8000/login/oauth2/code/messaging-client-oidc" redirect-uri: "{baseUrl}/{action}/oauth2/code/{registrationId}" scope: openid,message.read,message.write client-name: messaging-client-oidc provider: # 服务提供地址 authorization-server: # issuer-uri 可以简化下面的配置 issuer-uri: http://localhost:9000 # 请求授权码地址 # authorization-uri: http://localhost:9000/oauth2/authorize # 请求令牌地址 # token-uri: http://localhost:9000/oauth2/token # 用户资源地址 # user-info-uri: http://localhost:9000/oauth2/user # 用户资源返回中的一个属性名 # user-name-attribute: name # user-info-authentication-method: GET 这里的配置要和注册客户端时的配置对应上,同一颜色对应,这里使用的是 OIDC,scope 加上了 openid 注意:使用 OIDC 是为了使用默认的用户端点,假如不使用 OIDC 需要自定义用户端点接口,否则会报如下错误 [invalid_user_info_response] An error occurred while attempting to retrieve the UserInfo Resource: 403 : “{“error”:“insufficient_scope”}” 3.3 配置类 @EnableWebSecurity @Configuration(proxyBeanMethods = false) public class ClientSecurityConfig { / * 安全配置 */ @Bean SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http.authorizeHttpRequests(authorize -> // 任何请求都需要认证 authorize.anyRequest().authenticated() ) // 登录 // .oauth2Login(oauth2Login -> oauth2Login.loginPage("/oauth2/authorization/messaging-client-oidc")) .oauth2Login(Customizer.withDefaults()) .oauth2Client(Customizer.withDefaults()); return http.build(); } } 3.4 index.html Title 登录用户:
创建 IndexController,跳转到 index.html @Controller public class IndexController { @GetMapping("/") public String root() { return "redirect:/index"; } @GetMapping("/index") public String index(Model model) { Map map = MapUtil.map(false); Authentication auth = SecurityContextHolder.getContext().getAuthentication(); map.put("name", auth.getName()); Collection authorities = auth.getAuthorities(); List authoritiesList = authorities.stream().collect(Collectors.toList()); map.put("authorities", authoritiesList); model.addAttribute("user", JSONUtil.toJsonStr(map)); return "index"; } } 3.5 ResourceController @RestController public class ResourceController { @GetMapping("/server/a/resource1") public String getServerARes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) { return getServer("http://127.0.0.1:8001/resource1", oAuth2AuthorizedClient); } @GetMapping("/server/a/resource2") public String getServerARes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) { return getServer("http://127.0.0.1:8001/resource2", oAuth2AuthorizedClient); } @GetMapping("/server/a/resource3") public String getServerBRes1(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) { return getServer("http://127.0.0.1:8001/resource3", oAuth2AuthorizedClient); } @GetMapping("/server/a/publicResource") public String getServerBRes2(@RegisteredOAuth2AuthorizedClient OAuth2AuthorizedClient oAuth2AuthorizedClient) { return getServer("http://127.0.0.1:8001/api/publicResource", oAuth2AuthorizedClient); } / * 绑定token,请求微服务 */ private String getServer(String url, OAuth2AuthorizedClient oAuth2AuthorizedClient) { LogUtil.info("getServer"); // 获取 access_token String tokenValue = oAuth2AuthorizedClient.getAccessToken().getTokenValue(); // 发起请求 Mono stringMono = WebClient.builder() .defaultHeader("Authorization", "Bearer " + tokenValue) .build() .get() .uri(url) .retrieve() .bodyToMono(String.class); return stringMono.block(); } } 3.6 测试 完整目录结构如下: 启动项目,访问 127.0.0.1:8000,未登录会直接跳转到登录页面 输入用户名密码,登录后进入授权页面 选择想要授予的权限,这里勾选 read 权限,点击提交,跳转到我们的首页 index.html 将上面 user 的 JSON 信息格式化一下如下,可以看到就是我们的认证和权限信息 点击访问 服务A -> 资源1 点击访问 服务A -> 资源2,无法访问 这是因为之前授权时只给了 read 权限,而资源 2 需要 write 权限,可以看到报了 403 异常,这里可以定义一个异常处理类,来返回对应的信息,而不是白页 我们关闭当前页面新开一个页面,再次访问 127.0.0.1:8000 可以发现直接进入了 index.html,无需再次登录 可以发现我们访问时是带了一个 JESSEIONID 的,用户登录后,会在认证服务器和客户端都保存 session 信息
香港云服务器租用推荐
服务器租用资讯
·广东云服务有限公司怎么样
·广东云服务器怎么样
·广东锐讯网络有限公司怎么样
·广东佛山的蜗牛怎么那么大
·广东单位电话主机号怎么填写
·管家婆 花生壳怎么用
·官网域名过期要怎么办
·官网邮箱一般怎么命名
·官网网站被篡改怎么办
服务器租用推荐
·美国服务器租用
·台湾服务器租用
·香港云服务器租用
·香港裸金属服务器
·香港高防服务器租用
·香港服务器租用特价