SpringBoot搭建OAuth2


     背景

        前几天自己从零开始的搭建了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是怎么解决四方认证的问题,这个还没有说明。不过不用着急,笔者正在向着这方面努力。等我弄清楚这些,会再推出一篇博客补充的,敬请期待。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Elasticsearch不删原有jdk8导致的系列安装和启动问题

以前在空机器直接装elasticsearch&#xff0c;没有遇到什么问题。今天在现有JDK上安装&#xff0c;遇到的问题记录一下&#xff1a; 1. JDK的环境变量配置与我原有的不一致报如下错误&#xff1a; [estestZK-DES-I root]$ /usr/elasticsearch/bin/elasticsearch could not fi…

SSL函数01-数组函数Array Functions

一、数组的初始化 SSL中&#xff0c;数组下标从1开始&#xff01; 1-1、不知道数组的长度 :DECLARE a6; a6 : {}; Aadd(a6,a); Aadd(a6,b); Aadd(a6,c); 当用a : {}创建一个数组的时候&#xff0c;不可以用a[1] 值&#xff0c;来赋值&#xff01; 1-2、知道数组的长度 方式一…

【录用案例】2天录用!提交可录,沾边即可!

本周投稿推荐 SSCI • 2区社科类&#xff0c;3.0-4.0&#xff08;录用友好&#xff09; EI • 计算机工程生物医学等&#xff08;2天录用&#xff09; CNKI • 3天内初审录用&#xff0c;随即出版&#xff08;急录友好&#xff09; SCI&EI • 4区生物医学类&#x…

数据链路层 + NAT技术

数据链路层&#xff1a;负责设备之间的数据帧的传送和识别。 一、以太网 以太网的帧格式 如何分离报头和有效数据&#xff1f; 报头是固定长度的 如何将数据交给上层协议&#xff1f; 通过类型&#xff0c;如果是0800&#xff0c;则交给IP协议&#xff0c;如果是0806&#xf…

JavaScript正则表达式

一、介绍 正则表达式是用于匹配字符串中字符组合的模式。在javascript中&#xff0c;正则表达式也是对象。通常用来查找、替换那些符合正则表达式的文本&#xff0c;许多语言都支持正则表达式。 正则表达式的作用&#xff1a; 表单验证&#xff08;匹配&#xff09;、过滤敏感…

筛斗数据提取:解锁信息宝藏的关键步骤

在数字化时代&#xff0c;数据已成为推动社会进步和企业发展的关键要素。然而&#xff0c;数据本身并不直接产生价值&#xff0c;其价值在于我们如何从中提取有用的信息。数据提取&#xff0c;作为解锁信息宝藏的关键步骤&#xff0c;对于任何希望从海量数据中获取洞察力和竞争…

十四天学会Vue——Vue核心(理论+实战)上篇(第一天)

一、Vue核心&#xff08;上篇&#xff09; 热身tops&#xff1a;选取开发模式 ①用于开发模式 我们只需要知道 我们是开发模式&#xff0c;开发模式他会跟你提示代码出现错误的地方以及出错原因&#xff0c;而生产模式比较简洁。 ②用于生产模式 1.1 new Vue()实例 了解Vue&a…

人脸识别——筛选与删除重复或近似重复数据提高人脸识别的精确度

1. 概述 人脸识别研究通常使用从网上收集的人脸图像数据集&#xff0c;但这些数据集可能包含重复的人脸图像。为了解决这个问题&#xff0c;我们需要一种方法来检测人脸图像数据集中的重复图像&#xff0c;并提高其质量。本文介绍了一种检测人脸图像数据集中重复图像的方法。该…

有趣的css - 双开门按钮

大家好&#xff0c;我是 Just&#xff0c;这里是「设计师工作日常」&#xff0c;今天分享的是一个双开门的按钮&#xff0c;交互效果比较强&#xff0c;但是实现很简单&#xff0c;快学起来吧。 最新文章通过公众号「设计师工作日常」发布。 目录 整体效果核心代码html 代码cs…

充电宝哪家好用推荐?买什么充电宝性价比高?2024年充电宝排行榜

说实话&#xff0c;我其实是个手机重度使用者&#xff0c;买过的充电宝也有无数款了&#xff0c;每次手机没电的时候插座都离得不是特别近&#xff0c;不是要下床充电就是要固定在一个位置充电感觉怪麻烦的&#xff0c;但是有了充电宝后可以在床上玩手机都不用担心手机没电&…

惯性测量单元M-G366PDG提供低误差系数的解决方案

人形机器人、自动驾驶的快速发展&#xff0c;促成了惯性测量单元(IMU)的爆火市场。据相关研究报告统计&#xff0c;IMU全球市场规模从2018年的99.94亿美元增加至2021年的135.95亿美元预计2027年将达到222.53亿美元&#xff0c;2021年至2027年复合增长率达8.56%。而由于智能技术…

VUE3+TS+elementplus创建table,纯前端的table

一、前言 开始学习前端&#xff0c;直接从VUE3开始&#xff0c;从简单的创建表格开始。因为自己不是专业的程序员&#xff0c;编程主要是为了辅助自己的工作&#xff0c;提高工作效率&#xff0c;VUE的基础知识并不牢固&#xff0c;主要是为了快速上手&#xff0c;能够做出一些…

免费,Python蓝桥杯等级考试真题--第13级(含答案解析和代码)

Python蓝桥杯等级考试真题–第13级 一、 选择题 答案&#xff1a;C 解析&#xff1a;正向下标由0开始&#xff0c;下标3代表第四个元素&#xff0c;故答案为C。 答案&#xff1a;A 解析&#xff1a;range&#xff08;0,4&#xff09;的取前不取后&#xff0c;元组的符号是小括…

AI大模型在测试中的深度应用与实践案例

文章目录 1. 示例项目背景2. 环境准备3. 代码实现3.1. 自动生成测试用例3.2. 自动化测试脚本3.3. 性能测试3.4. 结果分析 4. 进一步深入4.1. 集成CI/CD管道4.1.1 Jenkins示例 4.2. 详细的负载测试和性能监控4.2.1 Locust示例 4.3. 测试结果分析与报告 5. 进一步集成和优化5.1. …

Transformer模型的简单学习

前言 Transformer 来源于一篇论文&#xff1a;Attention is all you need TRM在做一件什么事情呢&#xff1f;其实一开始它是被用于机器翻译的&#xff1a; 更详细的&#xff1a; 更详细的&#xff1a; 从上图可以看出&#xff0c;一个Encoders 下面包含了 n 个 Encoder&…

triton之paged attention

一 原理 图解大模型计算加速系列之&#xff1a;vLLM核心技术PagedAttention原理 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/691038809 二 源码分析 1 测试参数设置 test_paged_attention(num_seqs32,num_heads(64, 64),head_size64,block_size16,dtypetorch.float16,…

【ARM+Codesys案例】RK3568 +Codesys 软PLC方案在电镀生产线的应用

1 电镀生产简介 电镀是一种比较重要的工艺&#xff0c;产品经过电镀工艺处理后&#xff0c;不仅产品质量获得提高&#xff0c;产品性能也会大幅度提高&#xff0c;同时延长了产品的使用时间。电镀生产线是指按一定的电镀生产工艺要求,将有关的各种电镀处理槽、电镀行车运动装置…

ubuntu移动硬盘重命名

因为在ubuntu上移动硬盘的名字是中文的&#xff0c;所以想要改成英文的。 我的方法&#xff1a; 将移动硬盘插到windows上&#xff0c;直接右键重命名。再插到ubuntu上名字就改变了。 别人的方法&#xff1a; ubuntu下如何修改U盘名字-腾讯云开发者社区-腾讯云 在自带的软件…

安卓获取内部存储信息

目录 前言获取存储容量 前言 原生系统设置里的存储容量到底是怎么计算的&#xff0c;跟踪源码&#xff0c;涉及到VolumeInfo、StorageManagerVolumeProvider、PrivateStorageInfo、StorageStatsManager......等等&#xff0c;java上层没有办法使用简单的api获取到吗&#xff1f…

力扣239. 滑动窗口最大值

Problem: 239. 滑动窗口最大值 文章目录 题目描述思路复杂度Code 题目描述 思路 1.编写实现优先队列类&#xff1a; 1.1.实现push(int n):将元素n添加到队列尾&#xff0c;同时将n前面大于n的元素删除 1.2.实现int max():将队列头元素取出&#xff08;由于实现了push所以此时队…