SpringBoot搭建OAuth2

news2024/12/24 0:25:22


     背景

        前几天自己从零开始的搭建了CAS 服务器,结果差强人意(反正是成功了)。这几天,我躁动的心又开始压抑不住了,没错,我盯上OAuth2了,大佬们都说OAuth2比CAS牛批,我就想知道它有多牛批。以我的颜值能不能驾驭OAuth2,于是就有了本次的实践。这次,我不仅要从无到有的搭建OAuth2 Server,我还要完完整整的掌握OAuth2。

     OAuth2简介

        OAuth 是一个开放标准,该标准允许用户让第三方应用访问该用户在某一网站上存储的私密资源(如头像、照片、视频等),而在这个过程中无需将用户名和密码提供给第三方应用。通过提供一个令牌(token),而不是用户名和密码来访问隐私数据来实现这样的功能。因为令牌(token)的方式可以让用户灵活的对第三方应用授权或者收回权限。

        OAuth2 是 OAuth 协议的下一版本。传统的 Web 开发登录认证一般都是基于 session 的,但是在前后端分离的架构中继续使用 session 就会有许多不便,因为移动端(Android、iOS、微信小程序等)要么不支持 cookie(微信小程序),要么使用非常不便,对于这些问题,使用 OAuth2 认证都能解决。

        OAuth2.0协议一共支持 4 种不同的授权模式:

        1、授权码模式:常见的第三方平台登录功能基本都是使用这种模式。

        2、简化模式:简化模式是不需要客户端服务器参与,直接在浏览器中向授权服务器申请令牌(token),一般如果网站是纯静态页面则可以采用这种方式。

        3、密码模式:密码模式是用户把用户名密码直接告诉客户端,客户端使用这些信息向授权服务器申请令牌(token)。这需要用户对客户端高度信任,例如客户端应用和服务提供商就是同一家公司,自己做前后端分离登录就可以采用这种模式。

        4、客户端模式:客户端模式是指客户端使用自己的名义向服务提供者申请授权,严格来说,客户端模式并不能算作 OAuth 协议要解决的问题的一种解决方案,但是,对于开发者而言,在一些前后端分离应用或者为移动端提供的认证授权服务器上使用这种模式还是非常方便的。

        这里有两张我从网上找到的图,也很有借鉴意义:

    OAuth2 实践

        如何搭建一个简单的OAuth2 Server? 这个工作我是完全是照抄Leon_Jinhai_Sun的博客OAuth2:搭建授权服务器里的内容,连代码都完全一样,本不想在这里再赘述(毕竟人家比我写的好多了)。但为了记录自己的实践过程,我还是腆着脸把我的抄袭过程重演了一遍。

        1、新建SpringBoot空项目,添加maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>2021.0.1</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        
        <!--  OAuth2.0依赖,不再内置了,所以得我们自己指定一下版本  -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-oauth2</artifactId>
            <version>2.2.5.RELEASE</version>
        </dependency>

        2、添加配置类

// security 安全相关的配置类
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    // 配置安全拦截策略
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()  //这一行下面的所有其他请求都需要经过登陆认证
                .and()
                .formLogin().permitAll();    //使用表单登录
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        auth
                .inMemoryAuthentication()   //直接创建一个静态用户
                .passwordEncoder(encoder)
                .withUser("test").password(encoder.encode("123456")).roles("USER");
    }


    @Bean   //这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    @Override
    public UserDetailsService userDetailsServiceBean() throws Exception {
        return super.userDetailsServiceBean();
    }
}

//Oauth2.0的配置类
@EnableAuthorizationServer   //开启验证服务器
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager manager;

    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();


    @Resource
    UserDetailsService service;


    /**
     * 这个方法是对客户端进行配置,一个验证服务器可以预设很多个客户端,
     * 之后这些指定的客户端就可以按照下面指定的方式进行验证
     * @param clients 客户端配置工具
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients
                .inMemory()   //这里我们直接硬编码创建,当然也可以像Security那样自定义或是使用JDBC从数据库读取
                .withClient("web")   //客户端名称,随便起就行
                .secret(encoder.encode("654321"))      //只与客户端分享的secret,随便写,但是注意要加密
                .autoApprove(false)    //自动审批,这里关闭,要的就是一会体验那种感觉
                .scopes("book", "user", "borrow")     //授权范围,这里我们使用全部all
                .redirectUris("http://127.0.0.1:19210/leixi/demo")
                .authorizedGrantTypes("client_credentials", "password", "implicit", "authorization_code", "refresh_token");
        //授权模式,一共支持5种,除了之前我们介绍的四种之外,还有一个刷新Token的模式
    }

    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .passwordEncoder(encoder)    //编码器设定为BCryptPasswordEncoder
                .allowFormAuthenticationForClients()  //允许客户端使用表单验证,一会我们POST请求中会携带表单信息
                .checkTokenAccess("permitAll()");     //允许所有的Token查询请求,没有这一行,check_token就会报401
    }

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {
        endpoints
                .userDetailsService(service)
                .authenticationManager(manager);
        //由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式
    }
}



        3、测试

        完成了以上配置,一个简单的OAuth2服务就配置好了,启动之后,可以进行如下验证。

        3.1 客户端模式验证。

        3.2 验证token(一定得用POST请求)

        3.3 用户名密码方式登陆

        或者用以下方式:

        3.4 简单登陆方式,浏览器访问以下链接,自动跳转到登陆页后,输入用户名密码

        http://127.0.0.1:19200/leixi-sso/oauth/authorize?client_id=web&response_type=token

 

        系统会自动跳转到配置文件中的.redirectUris()路径,并带上token

        3.5 授权码模式,浏览器访问以下链接,自动跳转到登陆页后,输入用户名和密码。

        http://localhost:19200/leixi-sso/oauth/authorize?client_id=web&response_type=code

        系统的返回页面就会显示一个code,拿着这个code可以用postman进行后续测试:

        3.5 刷新token 

        至此,一个简单的OAuth2 Server已经搭建成功了,而且所有测试案例都能通过。你可以怀疑我,但不要怀疑大佬Leon_Jinhai_Sun的教程,在发现这份教程之前,我找过十数篇博客,有的只能调通一部分请求,有的测试起来很麻烦,相关踩坑经历我会在后文说明。看过我的踩坑之路你就会发现。这份代码真的是太难能可贵了,短小精悍,含金量极高。

      OAuth2服务器进阶

        上面的OAuth2服务器虽然已经搭起来了,但这只是万里长征第一步。作为一个能正常使用的服务器,还需要作以下调整:

        1、客户端用户名,密码均从数据库中获取

        2、数据库里的密码都是用密文存储的,所以要加上密码的转换器。

        3、token太短了,需要引入JWT进行加密

        4、token要存储在redis里,避免auth Server宕机重启后token失效。

        闲话不多说,咱们这就来改造一下。

        1、添加maven依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

        <dependency>
            <groupId>com.jdbc.dm</groupId>
            <artifactId>DmJdbcDriver18</artifactId>
            <version>1.0</version>
        </dependency>

        2、application.yml添加数据库和redis配置

spring:
  datasource:
    driver-class-name: dm.jdbc.driver.DmDriver
    url: jdbc:dm://192.168.5.97:5238/LEIXI
    username: LEIXI
    password: leixi123
    initialSize: 5
    minIdle: 5
    maxActive: 20
    maxWait: 60000
    timeBetweenEvictionRunsMillis: 300000
    minEvictableIdleTimeMillis: 300000
    validationQuery: SELECT 1 from DUAL
    testWhileIdle: true
    testOnBorrow: true
    testOnReturn: false
    poolPreparedStatements: true
    maxPoolPreparedStatementPerConnectionSize: 20
  redis:
    database: 9
    host: 192.168.5.97
    port: 6379
    password:
mybatis-plus:
  mapper-locations: classpath:/mapper/*Mapper.xml
  type-aliases-package: com.auth.auth2.entity
  global-config:
    db-config:
      #主键类型  0:"数据库ID自增",1:"该类型为未设置主键类型", 2:"用户输入ID",3:"全局唯一ID (数字类型唯一ID)", 4:"全局唯一ID UUID";
      id-type: assign_uuid
      # 默认数据库表下划线命名
      table-underline: true
  configuration:
    # 返回类型为Map,显示null对应的字段
    call-setters-on-nulls: true
    map-underscore-to-camel-case: false #开启驼峰和下划线互转
    # 这个配置会将执行的sql打印出来,在开发或测试的时候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

        3、添加数据库表

// 注意,这些表是 auth2官网上找到的,可以在auth2的底层代码里找到查询这些表的sql
create table oauth_client_details (
    client_id VARCHAR(256) PRIMARY KEY,
	resource_ids VARCHAR(256),
	client_secret VARCHAR(256),
	scope VARCHAR(256),
	authorized_grant_types VARCHAR(256),
	web_server_redirect_uri VARCHAR(256),
	authorities VARCHAR(256),
	access_token_validity INTEGER,
	refresh_token_validity INTEGER,
	additional_information VARCHAR(4096),
	autoapprove VARCHAR(256)
);
create table oauth_client_token (
	token_id VARCHAR(256),
	token LONGVARBINARY,
	authentication_id VARCHAR(256) PRIMARY KEY,
	user_name VARCHAR(256),
	client_id VARCHAR(256)
);
create table oauth_access_token (
	token_id VARCHAR(256),
	token LONGVARBINARY,
	authentication_id VARCHAR(256) PRIMARY KEY,
	user_name VARCHAR(256),
	client_id VARCHAR(256),
	authentication LONGVARBINARY,
	refresh_token VARCHAR(256)
);
create table oauth_refresh_token (
	token_id VARCHAR(256),
	token LONGVARBINARY,
	authentication LONGVARBINARY
);
create table oauth_code (
	code VARCHAR(256), authentication LONGVARBINARY
);
create table oauth_approvals (
	userId VARCHAR(256),
	clientId VARCHAR(256),
	scope VARCHAR(256),
	status VARCHAR(10),
	expiresAt TIMESTAMP,
	lastModifiedAt TIMESTAMP
);
-- customized oauth_client_details table
create table ClientDetails (
	appId VARCHAR(256) PRIMARY KEY,
	resourceIds VARCHAR(256),
	appSecret VARCHAR(256),
	scope VARCHAR(256),
	grantTypes VARCHAR(256),
	redirectUrl VARCHAR(256),
	authorities VARCHAR(256),
	access_token_validity INTEGER,
	refresh_token_validity INTEGER,
	additionalInformation VARCHAR(4096),
	autoApproveScopes VARCHAR(256)
);

// 这个表是我自己建的用户表
drop table if exists sys_user;
CREATE TABLE sys_user(
    id VARCHAR2(32) NOT NULL,
    userCode VARCHAR2(100),
    password VARCHAR2(255),
    userName VARCHAR2(100),
    accountExpired VARCHAR2(5),
    passExpired VARCHAR2(5),
    createrId VARCHAR2(50) NOT NULL,
    createrName VARCHAR2(100) NOT NULL,
    createTime Datetime NOT NULL,
    modifierId VARCHAR2(50),
    modifierName VARCHAR2(100),
    modifyTime Datetime,
    isDelete INTEGER NOT NULL DEFAULT  (0),
    PRIMARY KEY (id)
);

COMMENT ON TABLE sys_user IS '系统用户表';
COMMENT ON COLUMN sys_user.id IS '主键ID';
COMMENT ON COLUMN sys_user.userCode IS '用户账号';
COMMENT ON COLUMN sys_user.password IS '登陆密码';
COMMENT ON COLUMN sys_user.userName IS '用户名称';
COMMENT ON COLUMN sys_user.accountExpired IS '账号过期标记';
COMMENT ON COLUMN sys_user.passExpired IS '密码过期标记';
COMMENT ON COLUMN sys_user.createrId IS '创建人ID';
COMMENT ON COLUMN sys_user.createrName IS '创建人';
COMMENT ON COLUMN sys_user.createTime IS '创建时间';
COMMENT ON COLUMN sys_user.modifierId IS '更新人ID';
COMMENT ON COLUMN sys_user.modifierName IS '更新人';
COMMENT ON COLUMN sys_user.modifyTime IS '更新时间';
COMMENT ON COLUMN sys_user.isDelete IS '是否删除';


--该密码 2b53761249254ce6b502f521e5cc0683 是clientSecret经过MD5加密后的结果
insert into oauth_client_details(client_id, client_secret, web_server_redirect_uri, authorized_grant_types, scope)
values ('clientId','2b53761249254ce6b502f521e5cc0683','http://127.0.0.1:19210/leixi/demo', 'authorization_code,client_credentials,password,implicit', 'scope1,scope2');


--该密码 e10adc3949ba59abbe56e057f20f883e 是123456经过MD5加密后的结果
insert into sys_user(id,userCode, password, userName,createrId, createrName,createTime, isDelete) values(sys_guid, 'leixi','e10adc3949ba59abbe56e057f20f883e', '雷袭','system','系统',sysdate, 0);

        4、代码处理

// 用户信息实体类
@Data
@EqualsAndHashCode(callSuper = false)
@TableName("sys_user")
public class SysUser implements Serializable {

    private static final long serialVersionUID = 1L;

    /**
     * 主键ID
     */
    private String id;

    /**
     * 用户账号
     */
    private String userCode;

    /**
     * 登陆密码
     */
    private String password;

    /**
     * 用户名称
     */
    private String userName;

    /**
     *账号过期标记
     */
    private String accountExpired;

    /**
     * 密码过期标记
     */
    private String passExpired;

    /**
     * 创建人ID
     */
    private String createrId;

    /**
     * 创建人
     */
    private String createrName;

    /**
     * 创建时间
     */
    private Date createTime;

    /**
     * 更新人ID
     */
    private String modifierId;

    /**
     * 更新人
     */
    private String modifierName;

    /**
     *更新时间
     */
    private Date modifyTime;

    /**
     *是否删除
     */
    private Integer isDelete;
}

// 用户Mapper 
@Repository
public interface SysUserMapper extends BaseMapper<SysUser> {

}


// 用户信息查询服务,用于替换从内存里查询用户信息的方式
@Service
public class UserDetailServiceImpl implements UserDetailsService{

    @Autowired
    private SysUserMapper mapper;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        LambdaQueryWrapper<SysUser> wrapper = new LambdaQueryWrapper();
        wrapper.eq(SysUser::getUserCode, username);
        SysUser user = mapper.selectOne(wrapper);
        if (user == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return new org.springframework.security.core.userdetails.User(user.getUserCode(), user.getPassword(),
                new ArrayList<>());
    }
}


//Md5加密数据处理类
public class MD5PasswordEncoder implements PasswordEncoder {

    @Override
    public String encode(CharSequence rawPassword) {
        try {
            MessageDigest md5 = MessageDigest.getInstance("MD5");
            byte[] digest = md5.digest(rawPassword.toString().getBytes("UTF-8"));
            String pass = new String(Hex.encode(digest));
            return pass;
        } catch (Exception e) {
            throw new RuntimeException("Failed to encode password.", e);
        }
    }

    @Override
    public boolean matches(CharSequence rawPassword, String encodedPassword) {
        return encodedPassword.equals(encode(rawPassword));
    }
}

// token附加信息类
public class CustomTokenEnhancer implements TokenEnhancer {

    @Override
    public OAuth2AccessToken enhance(OAuth2AccessToken oAuth2AccessToken, OAuth2Authentication oAuth2Authentication) {
        // 获取登录信息
        User user = (User) oAuth2Authentication.getUserAuthentication().getPrincipal();
        Map<String, Object> customInfoMap = new HashMap<>();
        customInfoMap.put("loginName", user.getUsername());//登录名
        customInfoMap.put("name", user.getUsername());//用户姓名
        customInfoMap.put("content", "这是一个测试的内容");
        customInfoMap.put("authorities", "hahahaha");
        ((DefaultOAuth2AccessToken) oAuth2AccessToken).setAdditionalInformation(customInfoMap);
        return oAuth2AccessToken;
    }
}


//Security的配置 
@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailServiceImpl userService;

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public MD5PasswordEncoder passwordEncoder() {
        return new MD5PasswordEncoder();
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .authorizeRequests()
                .anyRequest().authenticated()  //
                .and()
                .formLogin().permitAll();    //使用表单登录
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {

        auth.userDetailsService(userService).passwordEncoder(passwordEncoder());
    }

    @Bean   //这里需要将AuthenticationManager注册为Bean,在OAuth配置中使用
    @Override
    public AuthenticationManager authenticationManagerBean() throws Exception {
        return super.authenticationManagerBean();
    }


    @Bean
    public TokenStore tokenStore() {
        return new RedisTokenStore(redisConnectionFactory);
    }

/**
 *
     //将token存储在内存里,缺点是一重启token就失效了
     @Bean
     public TokenStore tokenStore() {
         return new InMemoryTokenStore();
     }

    将token存储到数据库里
    @Bean
    public TokenStore tokenStore() {
        return new JdbcTokenStore(dataSource);
    }

    用jwt的方式'存储' token。JwtTokenStore实现类没有持久化Token信息,但是JwtTokenStore实现了access tokens 和 authentications的相互转换,该功能通过JwtAccessTokenConverter对象实现,
    因此当需要authentications信息时,直接通过access tokens就可以获取到。因为没有存储,所以也不需要删除,同时也造成了JWT的方式不能使得Token过期,这也是JWT方式的一个致命缺点
    @Bean
    public TokenStore tokenStore() {
        return new JwtTokenStore(accessTokenConverter());
    }
 *
 */

}

//Oauth2的配置
@EnableAuthorizationServer
@Configuration
public class OAuth2Configuration extends AuthorizationServerConfigurerAdapter {

    @Resource
    private AuthenticationManager manager;

    private final MD5PasswordEncoder encoder = new MD5PasswordEncoder();

    @Resource
    UserDetailsService service;

    @Resource
    private DataSource dataSource;

    @Resource
    TokenStore tokenStore;


    /**
     * 这个方法是对客户端进行配置,比如秘钥,唯一id,,一个验证服务器可以预设很多个客户端,
     * 之后这些指定的客户端就可以按照下面指定的方式进行验证
     * @param clients 客户端配置工具
     */
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.jdbc(dataSource);
    }

    // 令牌端点的安全配置,比如/oauth/token对哪些开放
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) {
        security
                .passwordEncoder(encoder)    //编码器设定为BCryptPasswordEncoder
                .allowFormAuthenticationForClients()  //允许客户端使用表单验证,一会我们POST请求中会携带表单信息
                .checkTokenAccess("permitAll()");     //允许所有的Token查询请求
    }

    //令牌访问端点的配置
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) {

        endpoints
                .userDetailsService(service)
                .authenticationManager(manager)
                .tokenServices(tokenServices());
        //由于SpringSecurity新版本的一些底层改动,这里需要配置一下authenticationManager,才能正常使用password模式
    }

    // 设置token的存储,过期时间,添加附加信息等
    @Bean
    public AuthorizationServerTokenServices tokenServices() {
        DefaultTokenServices services = new DefaultTokenServices();
        services.setReuseRefreshToken(true);
        services.setTokenStore(tokenStore);
        services.setAccessTokenValiditySeconds(120);
        services.setRefreshTokenValiditySeconds(60*5);
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(new CustomTokenEnhancer(), accessTokenConverter()));
        services.setTokenEnhancer(tokenEnhancerChain);
        return services;
    }

    // 对token信息进行JWT加密
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
        converter.setSigningKey("密钥");
        return converter;
    }
}


        5、测试

        重新启动OAuth Server,以用户名+密码的模式登陆,可以看到的结果如下:

        从返回结果能看出来,token里已经添加了一些附加的数据。就算重启服务,校验token时,仍然生效。

     踩过的坑

     1、配置问题

        别看整个Oauth2中只用到了两个config文件,但我在这两个文件上踩过的坑,两个星期都说不完。这两个配置文件里的内容很相似,但各有各的作用,之前我搭服务时东拼西凑的,经常粘错地方,结果导致OAuth2启不来,或者启动后验证不通过。这里指出几个典型的地方(具体配置的位置见上文中的config代码。):

        .allowFormAuthenticationForClients() :允许客户端使用表单验证,如果没有这一行,我们在配置客户端信息时得这么传参:

        而有了这一行,我们配置参数时就简单了许多,这里是对很多初学者造成较大困扰的地方。

        .checkTokenAccess("permitAll()")  没有这一行,/oauth/check_token调用时就会报401。

        .accessTokenConverter()和.tokenEnhancer()  accessTokenConverter()用于将OAuth2访问令牌转换为认证对象,而tokenEnhancer() 则是在访问令牌生成后,对其进行自定义处理,比如添加一些用户自定义的信息。两者同时配到AuthorizationServerEndpointsConfigurer里时,会导致 accessTokenConverter()不生效,这时侯可用以下方式配置来解决不生效的问题:

    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        TokenEnhancerChain tokenEnhancerChain = new TokenEnhancerChain();
        tokenEnhancerChain.setTokenEnhancers(Arrays.asList(accessTokenConverter, accessTokenConverter()));
        endpoints.authenticationManager(authenticationManager)
                .userDetailsService(userDetailsService)
                .tokenStore(tokenStore)
                .tokenEnhancer(tokenEnhancerChain);
    }

        出现以上的问题,全因为我对于Spring Security的配置不甚了解,空中楼阁还是要不得的。奉劝后面学习的新人们,在尝试掌握Oauth2时,最好先熟悉下Spring Security。

      2、粗心大意

        1、我在config文件里配置的PasswordEncoder的实现类是MD5PasswordEncoder,登陆时,oauth2 服务会先将用户输入的密码进行MD5加密,再与数据库中的密码对比。所以如果我在静态配置文件中还这么配,肯定是通不过的。

        2、OAuth2的所有的请求名都是/oauth/…,然而我第一次测试时,顺手写成了/auth/…,能访问通才怪!

        3、之前测试授权码登陆的方式时,我先启动server, 输入用户名密码后,得到code,再通过单元测试来尝试code方式的登陆,结果自然是失败了。因为在server中设置的将token和code存储在内存中,单元测试时,相当于重新启动一次server,两次服务的内存各自独立,自然是无法验证通过。这里也贴一下我单元测试的代码:

    @org.junit.Test
    public void token_password() {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "password");
        params.add("username", "admin");
        params.add("password", "1234567");
        params.add("scope", "read");
        String response = restTemplate.withBasicAuth("client", "secret").
                postForObject("/oauth/token", params, String.class);
        System.out.println(response);
    }

    @org.junit.Test
    public void token_client() {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "client_credentials");
        String response = restTemplate.withBasicAuth("client", "secret").
                postForObject("/oauth/token", params, String.class);
        System.out.println(response);
    }

    // 注意,这个方法在以内存方式存储token时,不会成功
    @org.junit.Test
    public void token_code() {
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "authorization_code");
        params.add("code", "5qpZFh");
        String response = restTemplate.withBasicAuth("clientId", "clientSecret").postForObject("/oauth/token", params, String.class);
        System.out.println(response);
    }

        但是,这些粗心大意引起的问题,也恰好是我进一步查看OAuth2源码的契机,话不多说,请往下看。

     3、代码debug

        因为粗心大意,我走了些弯路,为了知道自己为什么会错,我也好好研究了一下源码,这对我调通OAuth2也有一定的帮助,下面给大家介绍一些比较重要的类。

        TokenEndpoint  /oauth/token的入口

        AuthorizationEndpoint  /oauth/authorize 入口

        ClientDetailsUserDetailsService.loadUserByUsername 通过客户端id,密码登入,会进这里

        UsernamePasswordAuthenticationFilter.attemptAuthentication 通过授权码的方式,输入用户名密码时,会进这里对用户名密码进行校验

        InMemoryClientDetailsService.loadClientByClientId 通过clientId从内存中获取client信息,所有的clientId对应的相关信息在项目启动时都会查询这个clientDetailsStore

        DefaultLoginPageConfigurer Spring-Security的默认登陆页面生成位置

        FilterChainProxy.doFilter  Spring Security 过滤开始的位置

        BasicAuthenticationEntryPoint.commence 这个位置报401的错

        ExceptionTranslationFilter.handleSpringSecurityException 就是这个Exception报Full authentication is required to access this resource,比较常见的一个错误。

        部分方法是通过异步线程调用的,可能一次两次进不去,多调几次就进去了。

     致谢

        为了整理这篇博客,我可真是搜了好多资料,看过很多大佬的文章,以下是我觉得非常有用的,在这里推荐给大家,感谢大佬指路。

       妹子始终没搞懂OAuth2.0,今天一次说明白!这一篇是从原理讲到实践 的,非常有参考意议。

     『 OAuth2.0』 猴子都能懂的图解   深入浅出的讲解了oauth2的概念,值得一看。

       OAuth2:搭建授权服务器   非常实用,我的实践部分就是抄的这位大佬的。

        最后要说一句,本文虽然搭建了OAuth2 的认证服务器,但是怎么实现它与资源服务器的交互,OAuth2是怎么解决四方认证的问题,这个还没有说明。不过不用着急,笔者正在向着这方面努力。等我弄清楚这些,会再推出一篇博客补充的,敬请期待。

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

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

相关文章

FFmpeg编解码的那些事(1)

看了网上很多ffmpeg的编解码的文章和代码&#xff0c;发现有很多文章和代码都过时了&#xff0c;主要还是ffmpeg有很多接口都已经发生变化了。 这里简单说一下&#xff0c;什么是编码和解码。 1.视频编码 对于视频来说&#xff0c;可以理解为多张&#xff08;rgb或者yuv&…

【SCAU操作系统】实验四实现FCFS、SSTF、电梯LOOK和C-SCAN四种磁盘调度算法python源代码及实验报告参考

需求分析 设计一个程序将模拟实现FCFS&#xff08;先来先服务&#xff09;、SSTF&#xff08;最短寻道时间优先&#xff09;、电梯LOOK和C-SCAN&#xff08;循环扫描&#xff09;四种磁盘调度算法&#xff0c;并通过图形可视化界面动态展示每种算法的调度过程。 程序所能达到…

消费增值的真面目!绿色积分的合理运用!

各位朋友&#xff0c;大家好&#xff01;我是吴军&#xff0c;来自一家备受瞩目的软件开发企业&#xff0c;担任产品经理一职。今天&#xff0c;我非常荣幸能有机会与大家分享一种在市场上备受瞩目的新型商业模式——消费增值模式。 随着环保和可持续发展理念日益深入人心&…

网络、HTTP、HTTPS、Session、Cookie、UDP、TCP

OSI 七层模型 应用层、表示层、会话层、传输层、网络层、数据链路层、物理层 TCP/IP 五层模型 应用层&#xff1a;为用户的应用进程提供网络通信服务&#xff08;协议&#xff1a;域名系统DNS协议&#xff0c;HTTP协议&#xff0c;SMTP协议&#xff09;传输层&#xff1a;负…

Gopher的Rust第一课:第一个Rust程序

经过上一章[1]的学习&#xff0c;我想现在你已经成功安装好一个Rust开发环境了&#xff0c;是时候撸起袖子开始写Rust代码了&#xff01; 程序员这个历史并不算悠久的行当&#xff0c;却有着一个历史悠久的传统&#xff0c;那就是每种编程语言都将一个名为“hello, world”的示…

【渗透测试】|基于dvwa的CSRF初级,中级,高级

一、渗透测试 二、渗透测试过程中遇到的问题和解决 在初级csrf中&#xff0c;想要通过伪造一个404页面&#xff0c;达到修改密码的效果 伪造404页面的html代码如下&#xff1a; <html> <head> </head> <body> <img src"http://192.xx.xx.xx/…

python PyQt5 数字时钟程序

效果图&#xff1a; 概述 本文档将指导您如何使用Python的PyQt5库创建一个简单的时钟程序。该程序将显示当前时间&#xff0c;并具有以下特性&#xff1a; 始终在最前台显示。窗口可拖动。鼠标右键点击窗口可弹出退出菜单。时间标签具有红色渐变效果。窗口初始化时出现在屏幕…

解析智慧物流园区系统的多方位优势

智慧物流园区系统是基于物联网、大数据、人工智能等先进技术的应用系统&#xff0c;旨在实现物流园区的高效、智能化管理。随着物流行业的快速发展&#xff0c;传统物流园区已经无法满足日益增长的需求。智慧物流园区系统的出现填补了现有物流园区管理的空白&#xff0c;带来了…

Windows11系统安装QEMU虚拟化软件

Windows11系统安装QEMU虚拟化软件 QEMU软件是一个通用的开源机器模拟器和虚拟机。本文档适用于在Windows 11系统平台上安装QEMU软件。 1. 安装准备 1.1 安装平台 Windows 11 1.2. 软件信息 软件名称软件版本安装路径QEMUQEMU-8.2.93D:\qemu 1.3软件下载 QEMU官网官网下…

AI办公自动化:kimi批量新建文件夹

工作任务&#xff1a;批量新建多个文件夹&#xff0c;每个文件夹中的年份不一样 在kimi中输入提示词&#xff1a; 你是一个Python编程专家&#xff0c;要完成一个编写关于录制电脑上的键盘和鼠标操作的Python脚本的任务&#xff0c;具体步骤如下&#xff1a; 打开文件夹&…

【鸟叔的Linux私房菜】2-主机规划与磁盘分区

文章目录 2.1 Linux与硬件的搭配各硬件设备在Linux的文件名使用虚拟机学习 2.2 磁盘分区磁盘连接方式和设备文件名的关系MBR(MS-DOS)与GPT磁盘分区表MBR(MS-DOS)GPT磁盘分区表 启动流程的BIOS与UEFI启动检测程序BIOS搭配MBR/GPT的启动流程UEFI BIOS搭配 GPT启动的流程 Linux安装…

解决SSH客户端远程连接CentOS7虚拟机时加载过慢问题

1、编辑 /etc/ssh/sshd_config 文件&#xff0c;将 useDNS 中的 yes 改为 no &#xff0c;关闭UseDNS加速&#xff1a; vi /etc/ssh/sshd_config2、重启ssh服务: systemctl restart sshd

构建php环境、安装、依赖、nginx配置、ab压力测试命令、添加php-fpm为系统服务

目录 php简介 官网php安装包 选择下载稳定版本 &#xff08;建议使用此版本&#xff0c;文章以此版本为例&#xff09; 安装php解析环境 准备工作 安装依赖 zlib-devel 和 libxml2-devel包。 安装扩展工具库 安装 libmcrypt 安装 mhash 安装mcrypt 安装php 选项含…

【Linux】22. 线程控制

Linux线程控制 POSIX线程库 与线程有关的函数构成了一个完整的系列&#xff0c;绝大多数函数的名字都是以“pthread_”打头的 要使用这些函数库&#xff0c;要通过引入头文<pthread.h> 链接这些线程函数库时要使用编译器命令的“-lpthread”选项 线程创建 pthread_cr…

成都爱尔眼科蔡裕主任解说什么是近视性黄斑病变

近视性黄斑病变&#xff0c;属于黄斑病变的其中一种。 黄斑是眼内一个部位&#xff0c;它位于眼底的后极部&#xff0c;视网膜的中心部&#xff0c;管理着光、形、色。黄斑变性是指由于年龄、遗传、不良环境、慢性光损伤等各种因素的影响&#xff0c;使眼部视网膜处的黄斑发生…

kafka-主题创建(主题操作的命令)

文章目录 1、topic主题操作的命令1.1、创建一个3分区1副本的主题1.1.1、获取 kafka-topics.sh 的帮助信息1.1.2、副本因子设置不能超过集群中broker的数量1.1.3、创建一个3分区1副本的主题1.1.4、查看所有主题1.1.5、查看主题详细描述 1、topic主题操作的命令 kafka发送消息会存…

彩光赋能中国智造 极简光3.X助力“数智”转型

蒸汽时代、电气时代、信息时代三大工业革命后 互联网和智能制造主导的工业4.0时代来临 大数据、云计算、人工智能等新兴技术 对企业园区的网络架构、负载能力等 提出了新要求,也使得光纤较于传统铜缆 在距离、性能、延时上的优势日益凸显 基于此 围绕未来园区网建设的企…

【NumPy】深入了解NumPy的multiply函数:高效矩阵和数组乘法指南

&#x1f9d1; 博主简介&#xff1a;阿里巴巴嵌入式技术专家&#xff0c;深耕嵌入式人工智能领域&#xff0c;具备多年的嵌入式硬件产品研发管理经验。 &#x1f4d2; 博客介绍&#xff1a;分享嵌入式开发领域的相关知识、经验、思考和感悟&#xff0c;欢迎关注。提供嵌入式方向…

positivessl泛域名证书500元13个月

随着创建网站的门槛变低&#xff0c;不论是个人用户还是企事业单位用户创建的域名网站也越来越多&#xff0c;怎么维护网络环境的安全成为了各个用户需要解决的问题。为了保护网站的数据安全&#xff0c;防止恶意攻击和数据泄露&#xff0c;大多数用户选择为域名网站安装数字证…

基于51单片机的直流电机调速设计

一.硬件方案 本系统采用STC89C51控制输出数据&#xff0c;由单片机IO口产生PWM信号&#xff0c;送到直流电机&#xff0c;直流电机通过测速电路将实时转速送回单片机&#xff0c;进行转速显示&#xff0c;从而实现对电机速度和转向的控制&#xff0c;达到直流电机调速的目的。…