Spring Security(十八)--OAuth2:实现授权服务器(上)--环境准备以及骨架代码搭建

news2024/11/26 6:24:57

一、前言

本节大家如果一直从一开始看过来的话,就会巧妙发现我们将之前的实践代码全部连接起来,本节将会使用到之前的短信/验证码登录相关的逻辑代码,如果大家没有看的感兴趣可以回到https://editor.csdn.net/md/?articleId=127617691先将这部分逻辑代码看一看,尝试自己完成。然后本节仍然以实践为主,大家要完全吸收前几节关于OAuth2内容再来学习本节会更好点,然后当时再讲解搭建单点应用程序时,我们讲到过ClientRegistrationRepository这个类,讲到可以通过数据库去管理客户端,但是当时我们没有做相关展示,本次将会通过mysql结合mybatis完成对客户端的管理操作。
我们知道,授权服务器是在OAuth2架构中发挥作用的组件之一。授权服务器的职责是对用户进行身份验证,并向客户端提供令牌。客户端可以使用此令牌访问由资源服务器代表用户暴露的资源其中还介绍了,OAuth2框架定义了获取令牌的多个流程,我们称这些流程为授权。可以根据面临的场景选择一种不同的授权。所选择的授权的不同,授权服务器的行为也会有所不同。本章将介绍如何使用Spring Security为最常见的OAuth2授权类型配置授权服务器。

  • 授权码授权类型
  • 密码授权类型
  • 客户端凭据授权类型

并且还会讲解如何配置授权服务器以颁发刷新令牌。客户端要使用刷新令牌获得新的访问令牌。如果访问令牌过期,则客户端必须获得一个新的访问令牌。

二、编写我们自己的授权服务器实现

没有授权服务器就没有OAuth2流程。如之前所说,OAuth2的主要处理就是获取访问令牌。授权服务器是OAuth2架构的组件,它可以颁发访问令牌。所以我们首先需要知道如何实现它。
我们接着之前的项目基础上进行改造,首先在父项目的pom上加入spring-cloud-dependencies的组件依赖:

            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-dependencies</artifactId>
                <version>Hoxton.SR1</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>

当然我们引入cloud组件主要是为了用spring-cloud-starter-oauth2这个依赖,所以我们接下来在子项目的Pom加入该依赖:

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
        </dependency>

接下来可以定义一个配置类,这里将其称为AuthServerConfig.除了经典的@Configuration注解外,还需要使用@EnableAuthorizationServer对这个类进行注解。通过这种方式,就可以指示Spring Boot启用特定于OAuth2授权服务器的配置。可以通过配置AuthorizationServerConfigurerAdapter类和重写将在本章中讨论的特定方法来自定义这个配置:

package com.mbw.security.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
}

三、定义用户管理

本节将讨论用户管理。授权服务器是OAuth2框架中处理用户身份验证的组件。因此,它自然需要管理用户。幸运的是,这里的用户管理实现和之前讲解的UserDetails一家子是相同的,我们将继续使用它们来管理凭据。为了管理密码,我们要继续使用PasswordEncoder。
在这里插入图片描述
上图再次描述了Spring Security中执行身份验证过程的主要组件。你应该注意到,与之前描述的身份验证架构不同的是,图中不再有SecurityContext。之所以发生此更改,是因为身份验证的结果没有存储在SecurityContext中。取而代之的是,其中使用了来自TokenStore的令牌来管理身份验证。这个我们后面说资源服务器会细说。
那关于用户管理的配置不需要我多说,大家直接把当时短信认证登录写的用户的配置完全搬过来即可,顺便可以把之前写的短信认证,验证码登录相关的逻辑代码全部照搬过来,将前文写的ProjectConfig进行相关修改:

package com.mbw.security.config;

import com.mbw.security.filter.CaptchaCodeFilter;
import com.mbw.security.handler.CommonLoginFailureHandler;
import com.mbw.security.handler.CommonLoginSuccessHandler;
import com.mbw.security.handler.MyLogoutSuccessHandler;
import com.mbw.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
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.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.annotation.Resource;

@Configuration
@EnableWebSecurity
public class ProjectConfig extends WebSecurityConfigurerAdapter {
	@Autowired
	private UserDetailsServiceImpl commonUserDetailServiceImpl;
	@Autowired
	private CommonLoginSuccessHandler successHandler;
	@Autowired
	private CommonLoginFailureHandler failureHandler;
	@Autowired
	private MyLogoutSuccessHandler logoutSuccessHandler;
	@Autowired
	private CaptchaCodeFilter captchaCodeFilter;
	@Resource
	private SmsCodeSecurityConfig smsCodeSecurityConfig;

	@Override
	protected void configure(HttpSecurity http) throws Exception {
		http.cors().and()
//				.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class)
				.logout()
				.logoutSuccessHandler(logoutSuccessHandler)
				.and()
				.rememberMe()
				//默认都为remember-me
				.rememberMeParameter("remeber-me")
				//cookieName一般设置复杂一些,迷惑别人(不容易看出)
				.rememberMeCookieName("remeber-me")
				//过期时间
				.tokenValiditySeconds(24 * 60 * 60 * 2)
				.and()
				.csrf().disable()
				.formLogin()
				.loginPage("/toLogin") //用户没有权限就跳转到这个页面
				.loginProcessingUrl("/login")//登录跳转页面,表单中的action
				.usernameParameter("uname")
				.passwordParameter("upassword")//传递的属性
				.successHandler(successHandler)
				.failureHandler(failureHandler)
				.and()
				.apply(smsCodeSecurityConfig)
				.and().httpBasic().and()
				.authorizeRequests()
				.antMatchers("/home","/toLogin","/login", "/user/create", "/kaptcha", "/smsCode", "/smslogin").permitAll()
				.mvcMatchers("/test/a/*").permitAll()
				.anyRequest().authenticated()
				.and()
				.sessionManagement()
				.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
				.sessionFixation().migrateSession()
				.maximumSessions(1).
				maxSessionsPreventsLogin(false)
				.expiredSessionStrategy(new CustomExpiredSessionStrategy());
	}

	@Override
	protected void configure(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(commonUserDetailServiceImpl)
				.passwordEncoder(passwordEncoder());
	}

	@Override
	public void configure(WebSecurity web) {
		//将项目中的静态资源路径开放出来
		//为什么要抽出来?上面的规则都需要通过过滤器校验,这个不需要
		web.ignoring().antMatchers("/css/**", "/fonts/**", "/js/**", "/templates/**", "/static/**");
	}

	@Bean
	public PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

	//跨域配置
	@Bean
	CorsConfigurationSource corsConfigurationSource() {
		CorsConfiguration configuration = new CorsConfiguration();
		configuration.addAllowedOrigin("*");
		configuration.addAllowedMethod("*");
		configuration.addAllowedHeader("*");
		configuration.applyPermitDefaultValues();
		configuration.setAllowCredentials(true);
		UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
		source.registerCorsConfiguration("/**", configuration);
		return source;
	}
}

那么现在已经有了用户,接下来只需将用户管理关联到授权服务器配置即可。为此,需要在Spring上下文中将AuthenticationManager暴露一个bean,然后在AuthServerConfig类中使用它。所以我们紧接着上面的ProjectConfig配置我们的AuthenticationManager。


	@Bean
	@Override
	public AuthenticationManager authenticationManagerBean() throws Exception {
		return super.authenticationManagerBean();
	}

接下来更改AuthServerConfig类,以便向授权服务器去注册AuthenticationManager.

package com.mbw.security.config;

import com.mbw.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private UserDetailsServiceImpl userDetailsService;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
		.userDetailsService(userDetailsService);
	}
}

有了这些配置,就有了可以在身份验证服务器上进行身份验证的用户。但OAuth2架构意味着用户向客户端授予权利。客户端则以用户名义使用资源。所以接下来讲解如何配置客户端。

四、向授权服务器注册客户端

本节将介绍如何让授权服务器知晓客户端**。要调用授权服务器,在OAuth2架构中充当客户端的应用程序需要自己的凭据**。授权服务器还要管理这些凭据,并且只允许来自已知客户端的请求
在这里插入图片描述

还记得之前开发过的客户端应用程序吗?就那个GiteeClient,其中使用Gitee作为身份验证服务器。Gitee需要知悉该客户端应用程序,所以我们做的第一件事就是在Gitee上注册这个应用程序然后,我们会接收到客户端ID和客户端密钥:客户端凭据。接着我们配置了这些凭据,该客户端应用程序使用它们通过授权服务器(Gitee)进行身份验证。同样的情况也适用于本示例。授权服务器需要知悉其客户端,因为它要接收来自客户端的请求。这里的处理过程你应该很熟悉。为授权服务器定义客户端的契约是ClientDetails.定义对象以便根据其ID检索ClientDetails的契约是ClientDetailsService
这些名称听着熟悉吗?没错,这些接口的工作方式类似于UserDetails和UserDetailsService接口,只不过一个是用户,一个是客户端。你会发现,他俩很多都是类似的。例如InMemoryClientDetailsService是ClientDetailsService接口的实现,该接口管理内存中的ClientDetails.它的工作方式类似于用于UserDetails的InMemoryUserDetailsManager类。同样,JdbcClientDetailsService也类似于JdbcUserDetailsManager。
在这里插入图片描述
可以把这些相似之处总结为很容易记住的以下几点:

  • ClientDetails之于客户端就像UserDetails之于用户一样。
  • ClientDetailsService之于客户端就像UserDetailsService之于用户一样。
  • InMemoryClientDetailsService之于客户端就像InMemoryUserDetailsManager之于用户一样
  • JdbcClientDetailsService之于客户端就像JdbcUserDetailsManager之于用户一样

理解这一层关系后,我们就可以使用之前类似于配置UserDetails的方法同样用于ClientDetails上
首先我们需要一张存储客户端信息的表,表的结构和数据如下:

/*
 Navicat Premium Data Transfer

 Source Server         : local_mysql
 Source Server Type    : MySQL
 Source Server Version : 50727
 Source Host           : localhost:3306
 Source Schema         : spring_security

 Target Server Type    : MySQL
 Target Server Version : 50727
 File Encoding         : 65001

 Date: 28/11/2022 12:00:29
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for oauth_client_details
-- ----------------------------
DROP TABLE IF EXISTS `oauth_client_details`;
CREATE TABLE `oauth_client_details`  (
  `id` bigint(20) NOT NULL COMMENT 'id',
  `clientId` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT '客户端ID',
  `resourceIds` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '资源ID集合,多个资源时用英文逗号分隔',
  `clientSecret` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端密匙',
  `scope` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端申请的权限范围',
  `authorizedGrantTypes` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端支持的grant_type',
  `webServerRedirectUri` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '重定向URI',
  `authorities` varchar(256) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '客户端所拥有的SpringSecurity的权限值,多个用英文逗号分隔',
  `accessTokenValidity` int(11) NULL DEFAULT NULL COMMENT '访问令牌有效时间值(单位秒)',
  `refreshTokenValidity` int(11) NULL DEFAULT NULL COMMENT '更新令牌有效时间值(单位秒)',
  `additionalInformation` varchar(500) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '预留字段',
  `autoApprove` tinyint(1) NULL DEFAULT NULL COMMENT '用户是否自动Approval操作',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `UNIQUE_CLIENT_ID`(`clientId`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = '客户端信息' ROW_FORMAT = Dynamic;

-- ----------------------------
-- Records of oauth_client_details
-- ----------------------------
INSERT INTO `oauth_client_details` VALUES (1596077605292945410, 'f7n6ockwdb9zmayr', NULL, '$2a$10$b87rn4FF4TeR7r6VB45rC.kTv5M36Qs2U62WheA4mnEB5MmA3mVAW', 'user_info', 'password,authorization_code,refresh_token', 'http://localhost:9090/yidou', 'add,delete', 86400, 432000, NULL, 0);

SET FOREIGN_KEY_CHECKS = 1;

然后同样的我们需要建立一个客户端的实体类,同样的,这个实体类和真正用于OAuth的客户端是分离开来的,类似于我们之前的User类和JwtUserDto类的关系
OAuth2Client.java

package com.mbw.pojo;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("oauth_client_details")
public class OAuth2Client implements Serializable {
	@TableId(type = IdType.ASSIGN_ID, value = "id")
	private Long id;
	@TableField("clientId")
	private String clientId;
	@TableField(value = "resourceIds")
	private String resourceIds;
	@TableField(value = "authorities")
	private String authorities;
	@TableField("clientSecret")
	private String clientSecret;
	@TableField(value = "scope")
	private String scope;
	@TableField(value = "authorizedGrantTypes")
	private String authorizedGrantTypes;
	@TableField("webServerRedirectUri")
	private String webServerRedirectUri;
	@TableField("accessTokenValidity")
	private Integer accessTokenValidity;
	@TableField("refreshTokenValidity")
	private Integer refreshTokenValidity;
	@TableField("additionalInformation")
	private String additionalInformation;
	@TableField("autoApprove")
	private Boolean autoApprove;
}

同样的dao-service-controller我们也需要建立,并且提供2个方法,通过clientId查找client(这个也是ClientDetailsService需要重写的方法的原理),然后还有注册client的方法

OAuth2ClientMapper.java

package com.mbw.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.mbw.pojo.OAuth2Client;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OAuth2ClientMapper extends BaseMapper<OAuth2Client> {
	OAuth2Client queryByClientId(String clientId);
	OAuth2Client queryClientUnique(String clientId);
}

OAuth2ClientMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.mbw.mapper.OAuth2ClientMapper">
    <select id="queryByClientId" resultType="com.mbw.pojo.OAuth2Client">
        select * from oauth_client_details
        where clientId = #{clientId}
    </select>
    <select id="queryClientUnique" resultType="com.mbw.pojo.OAuth2Client">
        select ocd.id,ocd.clientId from oauth_client_details ocd
        where ocd.clientId = #{clientId} limit 1
    </select>
</mapper>

OAuth2ClientService.java
这里主要是需要解析一下clientSecret,我在这里当时出现过Bad Crendential相关的异常。
由于OAuth2解析客户端秘钥也会通过我们配置的passwordEncoder去解密,而我们在projectConfig已经对PasswordEncoder配置为BCryptPasswordEncoder,那么如果在注册客户端不通过passwordEncoder对clientSecret进行加密,在解析的时候就会出现解析错误。所以这里我们需要注入passwordEncoder.

package com.mbw.service;

import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.RandomUtil;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.mbw.mapper.OAuth2ClientMapper;
import com.mbw.pojo.OAuth2Client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;


@Service
public class OAuth2ClientService extends ServiceImpl<OAuth2ClientMapper, OAuth2Client> {
	@Autowired
	private OAuth2ClientMapper oAuth2ClientMapper;
	@Autowired
	@Lazy
	private PasswordEncoder passwordEncoder;

	public OAuth2Client queryClientByClientId(String clientId){
		return oAuth2ClientMapper.queryByClientId(clientId);
	}

	public String getClientIdUnique(OAuth2Client oAuth2Client){
		Long id = ObjectUtil.isEmpty(oAuth2Client.getId()) ? -1: oAuth2Client.getId();
		String clientId = RandomUtil.randomString(16);
		OAuth2Client oAuthClient = oAuth2ClientMapper.queryClientUnique(clientId);
		if(ObjectUtil.isNotEmpty(oAuthClient) && !oAuthClient.getId().equals(id)){
			//说明有重复,重新生成一次
			return getClientIdUnique(oAuth2Client);
		}else {
			return clientId;
		}
	}

	public OAuth2Client createOAuth2Client(OAuth2Client oAuth2Client){
		String clientId = getClientIdUnique(oAuth2Client);
		String clientSecret = RandomUtil.randomString(16);
		String clientSecretEncoded = passwordEncoder.encode(clientSecret);
		oAuth2Client.setClientId(clientId);
		//对clientSecret进行加密
		oAuth2Client.setClientSecret(clientSecretEncoded);
		oAuth2ClientMapper.insert(oAuth2Client);
		oAuth2Client.setClientSecret(clientSecret);
		return oAuth2Client;
	}
}

但是对于加入@Lazy的原因,如果直接注入passwordEncoder,看似没什么问题,但启动后会报循环依赖的问题。原因是我们的ProjectConfig在引用两个类,SmsUserDetailsServiceImpl和UserDetailsServiceImpl分别代表短信认证和用户认证的两个Service,而这两个Service都依赖了UserService,UserService又依赖了PasswordEncoder,具体关系如下图:
在这里插入图片描述
但是我这里有一个疑问,那就是为什么我们在没写OAuth2相关业务时,其实也存在这样一个循环依赖关系,为什么在当时不会报错,而加入OAuth2Service相关逻辑,就报了相关的错误呢,如果有知道的小伙伴可以在评论群帮我解决这个疑惑,非常感谢。
接着上面代码,我们接着写OAuth2ClientController类

package com.mbw.controller;

import com.mbw.common.utils.Res;
import com.mbw.pojo.OAuth2Client;
import com.mbw.service.OAuth2ClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/oauth2/client")
public class OAuth2ClientController {
	@Autowired
	private OAuth2ClientService oAuth2ClientService;

	@PostMapping("query/clientId")
	public Res queryOauthClientByClientId(String clientId){
		OAuth2Client oAuth2Client = oAuth2ClientService.queryClientByClientId(clientId);
		return Res.success(oAuth2Client);
	}

	@PostMapping("create")
	public Res createOAuthClient(@RequestBody OAuth2Client oAuth2Client){
		OAuth2Client oAuth2ClientCreated = oAuth2ClientService.createOAuth2Client(oAuth2Client);
		return Res.success(oAuth2ClientCreated);
	}
}

然后别忘了在projectConfig中对创建客户端这个接口进行放行:

.mvcMatchers("/oauth2/client/create").permitAll()

然后对于client这个单体我们完成了,现在我们要把它和OAuth2相关的逻辑类实现
类似于我们之前实现UserDetails和UserDetailsService一样
首先我们在dto包中加入OAuth2ClientDto并实现ClientDetails
OAuth2ClientDto.java

package com.mbw.security.dto;

import cn.hutool.core.text.CharSequenceUtil;
import com.mbw.pojo.OAuth2Client;
import lombok.Data;
import lombok.ToString;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.provider.ClientDetails;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

@Data
@ToString
public class OAuth2ClientDto implements ClientDetails {

	private OAuth2Client oAuth2Client;

	@Override
	public String getClientId() {
		return oAuth2Client.getClientId();
	}

	@Override
	public Set<String> getResourceIds() {
		String resourceIds = oAuth2Client.getResourceIds();
		if(CharSequenceUtil.isNotBlank(resourceIds)){
			String[] resourceCollection = resourceIds.split(",");
			return new HashSet<>(Arrays.asList(resourceCollection));
		}
		return Collections.emptySet();
	}

	@Override
	public boolean isSecretRequired() {
		return true;
	}

	@Override
	public String getClientSecret() {
		return oAuth2Client.getClientSecret();
	}

	@Override
	public boolean isScoped() {
		return true;
	}

	@Override
	public Set<String> getScope() {
		String scopes = oAuth2Client.getScope();
		if(CharSequenceUtil.isNotBlank(scopes)){
			String[] scopeCollection = scopes.split(",");
			return new HashSet<>(Arrays.asList(scopeCollection));
		}
		return Collections.emptySet();
	}

	@Override
	public Set<String> getAuthorizedGrantTypes() {
		String authorizedGrantTypes = oAuth2Client.getAuthorizedGrantTypes();
		if(CharSequenceUtil.isNotBlank(authorizedGrantTypes)){
			String[] grantTypes = authorizedGrantTypes.split(",");
			return new HashSet<>(Arrays.asList(grantTypes));
		}
		return Collections.emptySet();
	}

	@Override
	public Set<String> getRegisteredRedirectUri() {
		String webServerRedirectUri = oAuth2Client.getWebServerRedirectUri();
		if(CharSequenceUtil.isNotBlank(webServerRedirectUri)){
			HashSet<String> redirectUris = new HashSet<>();
			redirectUris.add(webServerRedirectUri);
			return redirectUris;
		}
		return Collections.emptySet();
	}

	@Override
	public Collection<GrantedAuthority> getAuthorities() {
		String authorities = oAuth2Client.getAuthorities();
		List<GrantedAuthority> grantedAuthorityList = new ArrayList<>();
		if(CharSequenceUtil.isNotBlank(authorities)){
			String[] grantAuthorities = authorities.split(",");
			for (String grantAuthority : grantAuthorities) {
				SimpleGrantedAuthority simpleGrantedAuthority = new SimpleGrantedAuthority(grantAuthority);
				grantedAuthorityList.add(simpleGrantedAuthority);
			}
			return grantedAuthorityList;
		}
		return Collections.emptyList();
	}

	@Override
	public Integer getAccessTokenValiditySeconds() {
		return oAuth2Client.getAccessTokenValidity();
	}

	@Override
	public Integer getRefreshTokenValiditySeconds() {
		return oAuth2Client.getRefreshTokenValidity();
	}

	@Override
	public boolean isAutoApprove(String s) {
		return oAuth2Client.getAutoApprove();
	}

	@Override
	public Map<String, Object> getAdditionalInformation() {
		String additionalInformation = oAuth2Client.getAdditionalInformation();
		if(CharSequenceUtil.isNotBlank(additionalInformation)){
			HashMap<String, Object> information = new HashMap<>();
			information.put("额外信息",additionalInformation);
			return information;
		}
		return Collections.emptyMap();
	}

	public OAuth2ClientDto(OAuth2Client oAuth2Client) {
		this.oAuth2Client = oAuth2Client;
	}
}

然后实现ClientDetailsServiceImpl,让它实现ClientDetailsService类。重写loadClientByClientId(String clientId)方法:

package com.mbw.security.service;

import com.mbw.mapper.OAuth2ClientMapper;
import com.mbw.pojo.OAuth2Client;
import com.mbw.security.dto.OAuth2ClientDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.provider.ClientDetails;
import org.springframework.security.oauth2.provider.ClientDetailsService;
import org.springframework.security.oauth2.provider.ClientRegistrationException;
import org.springframework.stereotype.Service;

@Service
public class ClientDetailsServiceImpl implements ClientDetailsService {

	@Autowired
	private OAuth2ClientMapper oAuth2ClientMapper;

	@Override
	public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
		OAuth2Client oAuth2Client = oAuth2ClientMapper.queryByClientId(clientId);
		return new OAuth2ClientDto(oAuth2Client);
	}
}

对比一下我们实现UserDetails和UserDetailsService,是不是基本一致呢。
写完后,我们就需要在授权服务器配置类当中去配置客户端了,完整配置类代码如下:
AuthServerConfig.java

package com.mbw.security.config;

import com.mbw.security.service.ClientDetailsServiceImpl;
import com.mbw.security.service.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;

@Configuration
@EnableAuthorizationServer
public class AuthServerConfig extends AuthorizationServerConfigurerAdapter {
	@Autowired
	private AuthenticationManager authenticationManager;
	@Autowired
	private ClientDetailsServiceImpl clientDetailsServiceImpl;
	@Autowired
	private UserDetailsServiceImpl userDetailsServiceImpl;

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints.authenticationManager(authenticationManager)
		.userDetailsService(userDetailsServiceImpl);
	}

	@Override
	public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
		clients.withClientDetails(clientDetailsServiceImpl);
	}



	/**
	 * 解决访问/oauth/check_token 403的问题
	 *
	 * @param security
	 * @throws Exception
	 */
	@Override
	public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
		// 允许表单认证
		security
				.tokenKeyAccess("permitAll()")
				.checkTokenAccess("permitAll()")
				.allowFormAuthenticationForClients();

	}

}

完成后我们可以来到postman
试试创建一个客户端:
在这里插入图片描述
发现创建成功,但是大家最好拿笔记本记录下这个未加密的clientSecret,因为db存储的加密后的secret。到时候我们需要该密钥去请求token。
到此,我们就完成了环境和相关骨架的搭建,下一章我们将在代码基础上完成三种授权类型的测试。
大家如果是一路跟过笔者写的文章过来的,可以发现OAuth2并不是独立的,相反,它将我们之前的学习内容通过这一章又紧密连接了起来,所以大家对于之前的内容不能忘记,必须加以巩固。
如果大家需要代码的话,可以在评论区回复我,我看到时候需不需要把代码放gitee上供大家自由克隆下来学习使用。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/42703.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【pdf翻译中文】免费自用日语pdf转中文、韩语pdf转中文(也可以转英文)或者其他小语种法语、德语pdf转中文

不是很精确、有点慢&#xff0c;但是也够用&#xff0c;胜在免费free 效果图&#xff1a; 一些对比&#xff1a; 模型来自于&#xff1a; https://huggingface.co/models 文件目录 调用模型的代码&#xff1a; 运行此段代码&#xff0c;执行翻译 一些简单的设置在这里…

【LeetCode每日一题】——557.反转字符串中的单词 III

文章目录一【题目类别】二【题目难度】三【题目编号】四【题目描述】五【题目示例】六【解题思路】七【题目提示】八【时间频度】九【代码实现】十【提交结果】一【题目类别】 字符串 二【题目难度】 简单 三【题目编号】 557.反转字符串中的单词 III 四【题目描述】 给…

DPU1.1S—高性能、低功耗4口高速USB2.0HUB控制器芯片

DPU1.1S是一款高性能、低功耗4口高速 USB2.0 HUB 控制器&#xff0c;上行端口兼容高速 480MHz和全速12MHz两种模式&#xff0c;4个下行端口兼容高速480MHz、全速12MHz、低速1.5MHz三种模式。 DPU1.1S采用状态机单事务处理架构&#xff0c;而非单片机架构&#xff0c;多个事务缓…

沃通CA证书支持多所高校招投标文件电子签名

招投标电子化是国家大力推行的战略性工作&#xff0c;为贯彻落实电子招投标工作&#xff0c;各政企单位、高校均启动采购与招投标系统&#xff0c;自行组织的招标、单一来源等采购活动都通过该类系统在线进行。近期&#xff0c;沃通CA证书打通与多所高校采购与招投标系统的应用…

C语言深度解析:函数的形参和实参与传值和传址(多角度分析,让你有一种相见恨晚的感觉!!!!)

目录 一、前言 二、函数的形参和实参 &#x1f351;实参 &#x1f34e;形参 &#x1f347;形参与实参之间的区别 &#x1f350;形参与实参之间的联系&#xff08;重点&#xff01;&#xff01;&#xff01;&#xff01;&#xff09; 三、函数的传值和传址&#xff08;超…

[附源码]计算机毕业设计JAVA网上宠物商店

[附源码]计算机毕业设计JAVA网上宠物商店 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat7.0 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff09;。 项目技术&#xff1a; SSM mybatis M…

【html5期末大作业】基于HTML仿QQ音乐官网网站

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

Xcode 中设置APP的图标(Icon)和启动页面(Launch Screen)

设置图标&#xff08;Icon&#xff09; 首先&#xff0c;要准备4组图标&#xff08;如果是发布在 iOS 平台上&#xff09;&#xff0c;这4组分别是 120 x 120 px&#xff0c;152 x 152 px&#xff0c;167 x 167 px&#xff0c;1024 x 1024 px 的尺寸&#xff0c;要 PNG 格式。…

使用EL表达式时,PropertyNotFoundException异常的解决过程

一. 业务场景 最近&#xff0c;杨哥的一个班级正在做基于JSPJSTL/EL Servlet JavaBean JDBC MySQL技术方案的阶段项目。其中有一个需求是根据多条件动态搜索商品信息&#xff0c;最终分页显示出商品列表。这个功能可以在dao层采用多分支条件组合&#xff0c;显示页面使用JST…

2022王道OS 1.3 操作系统运行环境

2022王道OS 1.3 操作系统运行环境 文章目录2022王道OS 1.3 操作系统运行环境1.3_1 操作系统的运行机制知识总览知识点预备知识&#xff1a;程序是如何运行的&#xff1f;两种程序&#xff1a;内核程序 VS 应用程序两种指令&#xff1a;特权指令 VS 非特权指令两种状态&#xff…

二、【React脚手架】组件化编码(TodoList案例)

文章目录1、组件化编码流程&#xff08;通用&#xff09;2、样式覆盖问题3、DEMO3.1、需要实现的效果3.2、前期须知3.3、项目结构3.4、CODE3.4.1、App.js3.4.2、App.css3.4.3、Header3.4.4、List3.4.5、Item3.4.6、Footer1、组件化编码流程&#xff08;通用&#xff09; 拆分组…

Python 面向对象简介

什么是面向对象&#xff1f; 面向对象是一种编程思想&#xff0c;在开发过程中关注的不是解决问题的步骤和过程&#xff0c;而是参与解决问题的对象和它们具备的行为。 01 面向过程和面向对象的区别 1、面向过程 面向过程在开发过程中关注的是解决问题的步骤和过程&#xf…

《计算机体系结构量化研究方法第六版》1.6 成本趋势

1.6.1 时间、产量和大众化的影响 即便没有技术进步&#xff0c;计算机组件的制造成本也会随着时间的推移而降低。这背后有着学习曲线的因素。 【学习曲线&#xff08;from百度&#xff09;】 学习曲线的定义为"在一定时间内获得的技能或知识的速率"&#xff0c;又称…

2022王道OS 1.4 操作系统的体系结构 chap1回顾

2022王道OS 1.4 操作系统的体系结构 & chap1回顾 操作系统的体系结构 知识总览 操作系统的内核 时钟管理&#xff1a;利用时钟中断实现计时功能 原语&#xff1a;一种特殊的程序&#xff0c;具有原子性&#xff08;一气呵成&#xff0c;不可被中断&#xff09; 内核是操…

人工智能开启甲骨文整理研究新范式

编者按&#xff1a;在甲骨学研究中&#xff0c;甲骨“校重”整理是一项费事费力但又极其重要的基础性研究工作。微软亚洲研究院与首都师范大学甲骨文研究中心莫伯峰教授团队合作开发的甲骨文校重助手Diviner&#xff0c;第一次将自监督AI模型引入到甲骨文“校重”工作中&#x…

【Mysql】数据库的基本操作和表的基本操作

本章内容是,用sql语言实现对数据库的基本操作和表的基本操作 文章目录前言1. 数据库的基本操作1.1 创建数据库1.2 查看数据库1.3 选中数据库1.4 删除数据库2. 数据库基本数据类型3. 表的基本操作3.1 创建表3.2 显示数据库中的表3.3 查看表的构造3.4 删表4. 表的增删改查4.1 增加…

使用synchronized 加锁你加对了么?

本文讲解使用synchronized只是对synchronized的使用,底层原理将在后续文章 目录 从实际中理解共享带来的问题 Java代码实现共享带来的问题进行分析 临界区(Critical Section) 与 竞态条件(Race Condition) 临界区 竞态条件 synchronized解决方案 怎么理解synchronized中…

【web前端期末大作业】基于HTML+CSS+JavaScript实现代理商销售管理系统后台(8页)

&#x1f389;精彩专栏推荐 &#x1f4ad;文末获取联系 ✍️ 作者简介: 一个热爱把逻辑思维转变为代码的技术博主 &#x1f482; 作者主页: 【主页——&#x1f680;获取更多优质源码】 &#x1f393; web前端期末大作业&#xff1a; 【&#x1f4da;毕设项目精品实战案例 (10…

mathtype在word内的简单使用

一、简单使用 1、快捷键 快捷键说明ctrlaltQ行内公式altshiftQ右编号&#xff08;行间公式&#xff09;\ ;{空格、大括号都需要转义ALTF4关闭mathtype窗口 2、小技巧 \left与\right 一定要配对使用&#xff0c;且对于对称的符号&#xff08;如(), {}等&#xff09;来说&…

【Linux】---进程控制(创建、终止、等待、替换)

文章目录进程创建fork()进程退出进程退出场景进程退出方法退出码exit、_exit进程等待进程等待的方法waitwaitpid阻塞和非阻塞进程替换替换的原理替换所用到的函数execlexeclpexecle简易的shell进程创建 fork() fork函数在之前的文章中也已经提到过了。其主要作用是从已存在的…