前情提要:一个“不雅观”的痛点

在上一季《Security第十六集:引入JWT》中,不知道大家有没有发现一个极其尴尬的画面——

两个微服务里,各自建了一个JwtUtils类,各自定义了一模一样的SECRET_KEY,还必须确保这俩密钥值完全相同。

这画面像什么?像极了异地恋情侣非得约定同一句暗号才能确认“是我本人”。代码冗余不说,只要密钥一换,所有服务都得跟着改,改到天荒地老。

有同学可能会问:“那怎么办?总不能让他们一直这么‘异地’下去吧?”

别急,OAuth2 就是来当这个月老的。今天我们就手把手把Security和OAuth2撮合成一对“认证授权CP”,让它们分工明确、各司其职,彻底终结那段“密钥复制粘贴”的苦日子。

第一幕:认清各自的本职工作

在动手改造之前,咱先把职责掰扯清楚,免得后面迷糊:
Spring Security 认证(Authentication) 验明正身——“你说你是张三,密码拿来我瞅瞅”
OAuth2 授权(Authorization) 发放通行证——“身份没问题,给你发个令牌,拿着它去访问别的服务吧”

简单说就是:Security负责“验人”,OAuth2负责“发牌”。

而我们今天要做的,就是把原来那个单纯的Security认证服务,改造成一个OAuth2授权服务器——让它既能验人,又能发牌,两全其美。

第二幕:动手改造,三步搞定

1.引入pom依赖

<!-- OAuth2 依赖 -->
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.3.9.RELEASE</version>
</dependency>

2.创建 OAuth2AuthorizationServerConfig,配置客户端信息以及授权类型

@Configuration
//启用授权服务器功能
@EnableAuthorizationServer
public class OAuth2AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {
    //用于验证用户的用户名和密码(密码模式需要)
    @Autowired
    private AuthenticationManager authenticationManager;
    //用于加密和验证客户端密钥
    @Autowired
    private PasswordEncoder passwordEncoder;

    //定义令牌的存储方式
    @Bean
    public TokenStore tokenStore() {
        // 使用 JWT 方式存储令牌
        // JWT 是一种自包含的令牌格式,不需要在服务器端存储
        return new JwtTokenStore(accessTokenConverter());
    }

    //JWT 令牌转换器
    @Bean
    public JwtAccessTokenConverter accessTokenConverter() {
        JwtAccessTokenConverter converter = new JwtAccessTokenConverter();


        // 注意:这里使用的是对称密钥(默认)
        // 生产环境建议使用非对称密钥(RSA)
        return converter;
    }

    //这里定义了哪些应用可以访问 OAuth2 服务
    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        // inMemory() - 将客户端信息存储在内存中
        // 生产环境应该使用 .jdbc() 存储在数据库中
        clients.inMemory()
                // 配置第一个客户端应用 , 客户端 ID(类似用户名)
                .withClient("client-app")
                // 客户端密钥(类似密码),需要加密
                .secret(passwordEncoder.encode("secret123"))
                // 允许的授权类型(四种模式)授权码模式,密码模式,客户端凭证模式,隐式模式,刷新令牌
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                // 允许的权限范围
                .scopes("read", "write")
                // 回调地址
                .redirectUris("http://localhost:8080/callback")
                // 自动批准,不需要用户手动确认授权
                .autoApprove(true)
                // 访问令牌有效期:3600秒(1小时)
                .accessTokenValiditySeconds(3600)
                // 刷新令牌有效期:7200秒(2小时)
                .refreshTokenValiditySeconds(7200)
                .and()
                // 配置下一个客户端
                .withClient("web-app")
                .secret(passwordEncoder.encode("web-secret"))
                .authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")
                .scopes("read", "write")
                .redirectUris("http://localhost:8080/web/callback")
                .autoApprove(true)
                .accessTokenValiditySeconds(3600)
                .refreshTokenValiditySeconds(7200);
    }

    //配置授权服务器端点
    @Override
    public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
        endpoints
                // 设置认证管理器,用于密码模式的用户认证
                .authenticationManager(authenticationManager)
                // 设置令牌存储方式(JWT)
                .tokenStore(tokenStore())
                // 设置令牌转换器(JWT 转换)
                .accessTokenConverter(accessTokenConverter());
    }

    //配置授权服务器的安全策略
    @Override
    public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
        security
                // /oauth/token_key 端点允许所有人访问
                // 这个端点用于获取 JWT 签名的公钥(如果使用非对称加密)
                .tokenKeyAccess("permitAll()")
                // /oauth/check_token 端点需要认证后才能访问
                // 这个端点用于验证令牌的有效性
                .checkTokenAccess("isAuthenticated()")
                // 允许客户端通过表单方式提交认证信息
                // 这样客户端可以在 POST body 中传递 client_id 和 client_secret
                .allowFormAuthenticationForClients();
    }
}

3.改造一下 MyWebSecurityConfigAdapter,放行 oauth2自带的接口,并创建一个AuthenticationManager对象

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.csrf().disable().httpBasic();
    http.authorizeRequests()
            .antMatchers("/oauth/**").permitAll()
            .anyRequest().authenticated();
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
    return super.authenticationManagerBean();
}

第三幕:实战测试,看看效果

先来测试一下密码模式

本服务端口 8129,用post请求 http://localhost:8129/oauth/token
Authorization 中 选择 Basic Auth 并 填入 用户名 client-app, 密码 secret123,注意 这个用户名密码 是

 .withClient("client-app")
                // 客户端密钥(类似密码),需要加密
                .secret(passwordEncoder.encode("secret123"))

不是users表中 的 用户名密码
在这里插入图片描述

然后在 body中添加参数,注意选择 x-www-form-urlencoded
在这里插入图片描述
grant_type是

.authorizedGrantTypes("authorization_code", "password", "client_credentials", "implicit", "refresh_token")

哪个模式访问就写哪个
这里的 username 和 password 才是 数据库中 users表的 用户名密码

scope的值是

// 允许的权限范围
.scopes("read", "write")

现在我们来 send 一下
在这里插入图片描述

返回的这一大串json,就是 oauth2的令牌。

客户端凭证模式

这个跟密码模式的区别
1,无需提供 users 的用户名密码
2,grant_type 的值 变成 client_credentials
在这里插入图片描述
这个模式通常用于服务与服务之间的内部调用,比如定时任务、微服务互相拉取数据等场景——没有真人用户,只有机器在互相对话。

下集预告

这一集我们把Security认证服务成功改造为OAuth2授权服务器,跑通了密码模式和客户端凭证模式。

下一集,我们将真正创建一个前端应用,完整跑通密码模式和授权码模式的登录流程。至于隐式模式没啥卵用,可以忘掉它

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐