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依赖

XML 复制代码
        <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、添加配置类

java 复制代码
// 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依赖

XML 复制代码
        <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配置

XML 复制代码
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、添加数据库表

sql 复制代码
// 注意,这些表是 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、代码处理

java 复制代码
// 用户信息实体类
@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()不生效,这时侯可用以下方式配置来解决不生效的问题:

java 复制代码
    @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,两次服务的内存各自独立,自然是无法验证通过。这里也贴一下我单元测试的代码:

java 复制代码
    @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是怎么解决四方认证的问题,这个还没有说明。不过不用着急,笔者正在向着这方面努力。等我弄清楚这些,会再推出一篇博客补充的,敬请期待。

相关推荐
Abladol-aj6 分钟前
并发和并行的基础知识
java·linux·windows
清水白石0086 分钟前
从一个“支付状态不一致“的bug,看大型分布式系统的“隐藏杀机“
java·数据库·bug
吾日三省吾码6 小时前
JVM 性能调优
java
Estar.Lee6 小时前
查手机号归属地免费API接口教程
android·网络·后端·网络协议·tcp/ip·oneapi
弗拉唐7 小时前
springBoot,mp,ssm整合案例
java·spring boot·mybatis
oi777 小时前
使用itextpdf进行pdf模版填充中文文本时部分字不显示问题
java·服务器
2401_857610037 小时前
SpringBoot社团管理:安全与维护
spring boot·后端·安全
少说多做3437 小时前
Android 不同情况下使用 runOnUiThread
android·java
知兀7 小时前
Java的方法、基本和引用数据类型
java·笔记·黑马程序员