密码模式(Password)实战:适用场景、实现步骤、风险分析

密码模式,在OAuth2的江湖里算是个「老前辈」了。我记得刚接触Spring Security那会儿,第一个上手的就是它。说白了,就是客户端拿着用户的用户名和密码,直接去找授权服务器换一个access_token。简单粗暴,但坑也不少。

今天咱们就把它掰开揉碎了讲。我会结合我实际项目里踩过的坑,带你看看这玩意儿到底该怎么用,什么时候用,以及——什么时候千万别用。

一、适用场景:什么时候该用它?

你想想看,什么场景下客户端能直接拿到用户的明文密码?

  • 第一方应用:比如你自己公司开发的官方App、官方Web端。用户信任你,愿意把密码交给你。
  • 高度信任的遗留系统:有些老系统,改造起来成本太高,没法升级到授权码模式。这时候密码模式可以作为一个过渡方案。
  • 内部工具或管理后台:公司内部的运维平台、监控系统,用户群体固定,安全风险可控。
⚠️ 重要警告: 密码模式绝对不能用于第三方应用!你想想,如果一个小众App让你输入微信密码,你敢吗?一旦第三方客户端保存了用户密码,泄露风险极高。我见过不止一个项目因为用了密码模式给第三方,最后密码库被脱裤,那叫一个惨。

二、实现步骤:手把手带你搭起来

嗯,这里要注意。Spring Security在旧版本(比如2.x)里对密码模式支持很直接,但到了Spring Security 5.x之后,官方更推荐使用授权码模式。不过,咱们做实战的,该会还是得会。

下面我以一个标准的Spring Boot项目为例,带你走一遍。

2.1 引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.security.oauth.boot</groupId>
    <artifactId>spring-security-oauth2-autoconfigure</artifactId>
    <version>2.6.8</version>
</dependency>

我个人习惯用 spring-security-oauth2-autoconfigure,配置起来比较省事。当然,你也可以用最新的 spring-security-oauth2-authorization-server,但那个配置会复杂一些。

2.2 配置授权服务器

@Configuration
@EnableAuthorizationServer
public class AuthorizationServerConfig extends AuthorizationServerConfigurerAdapter {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Override
    public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
        clients.inMemory()
                .withClient("client-app")
                .secret("{noop}secret")
                .authorizedGrantTypes("password", "refresh_token")
                .scopes("read", "write")
                .accessTokenValiditySeconds(7200)
                .refreshTokenValiditySeconds(2592000);
    }

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

这里有个关键点:authorizedGrantTypes 里必须加上 "password"。我曾经有个同事忘了加,结果调了半天接口一直报 unsupported_grant_type,气得直拍桌子。

2.3 配置Security(让密码模式能走通)

@Configuration
@Order(1)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication()
                .withUser("user")
                .password("{noop}password")
                .roles("USER");
    }

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

注意这个 authenticationManagerBean(),必须暴露成Bean,否则授权服务器拿不到认证管理器。我刚开始学的时候,这一步卡了我整整一下午。

2.4 测试一下

启动项目,用curl发个请求:

curl -X POST \
  http://localhost:8080/oauth/token \
  -H "Authorization: Basic Y2xpZW50LWFwcDpzZWNyZXQ=" \
  -d "grant_type=password&username=user&password=password&scope=read"

如果一切顺利,你会收到类似这样的响应:

{
    "access_token": "eyJhbGciOiJSUzI1NiIs...",
    "token_type": "bearer",
    "expires_in": 7199,
    "scope": "read",
    "refresh_token": "eyJhbGciOiJSUzI1NiIs..."
}
💡 小提示: 那个 Authorization: Basic xxx 是客户端的凭证,不是用户的。很多新手容易搞混。客户端ID和密码用Base64编码,格式是 client-id:client-secret

三、风险分析:这玩意儿到底有多危险?

说实话,密码模式在OAuth2的四种模式里,安全系数是最低的。我这么说是有依据的。

风险点 具体描述 严重程度
密码明文传输 用户密码在每次请求时都要经过网络传输,即使用了HTTPS,也增加了暴露面
客户端存储密码 客户端(尤其是移动端)需要暂时保存用户密码,容易被恶意程序窃取 极高
无法实现细粒度授权 密码模式拿到的token,scope通常比较宽泛,没法做到像授权码模式那样精细控制
刷新令牌风险 如果refresh_token泄露,攻击者可以长期持有用户会话

我曾经在一个金融项目里,看到开发团队直接用密码模式对接第三方支付渠道。我当时就急了,这要是出了事,用户资金安全谁来负责?后来硬是说服他们改成了授权码模式 + PKCE。

四、避坑指南:我踩过的那些坑

做密码模式实战,有几个坑你一定要注意:

  • 不要用HTTP:密码模式必须搭配HTTPS,否则密码就是裸奔。我曾经在测试环境忘了配HTTPS,结果抓包一看,密码清清楚楚。
  • 限制失败尝试次数:密码模式天然容易受到暴力破解攻击。一定要在授权服务器层面加上登录失败次数限制。
  • 及时清理密码缓存:客户端拿到token后,应该立即丢弃用户密码。我见过有些App把密码存在SharedPreferences里,一存就是好几年。
  • 配合refresh_token使用:access_token过期后,用refresh_token去换新的,而不是让用户重新输入密码。这样能减少密码暴露的频率。
📌 核心总结: 密码模式不是不能用,但一定要用在「自己人」的场景里。如果你在做第三方登录、开放平台,请立刻放弃密码模式,改用授权码模式。安全无小事,别图一时方便。

好了,密码模式就讲到这里。下一章咱们聊聊客户端模式(Client Credentials),那个更适合服务间调用的场景。到时候我会分享一个我在微服务架构里用客户端模式做服务鉴权的实战案例,保证让你有收获。