一、问题描述:
使用Spring-security-0Auth2:2.5.1 版本,进行用户认证时,一直出现问题:
{
"error": "invalid_client",
"error_description": "Bad client credentials"
}
出现问题的条件:
用户的密码使用明文验证时,不会出现此问题。而把密码验证设置为: bcrypt后,再次获取token时,就会报此问题。
postMan的请求和结果如下:
二、代码:
认证服务安全配置类:主要是验证用户名和密码
package com.lagou.edu.oauth.config;
import com.lagou.edu.oauth.service.JdbcUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import java.util.ArrayList;
/**
* FileName: SecurityConfiger
* Author:
* Date: 2021/10/29 17:02
* Description: 该配置类为认证服务器安全配置类,主要处理用户名和密码的校验等事宜
*/
@Configuration
public class SecurityConfiger extends WebSecurityConfigurerAdapter {
@Autowired
private JdbcUserDetailsService jdbcUserDetailsService;
/**
* 注册一个认证管理器对象到容器
* @return
* @throws Exception
*/
@Bean
@Override
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
/**
* 密码编码对象(密码不进行加密处理)
* @return
*/
@Bean
public PasswordEncoder passwordEncoder() {
//return NoOpPasswordEncoder.getInstance(); //密码不加密
return new BCryptPasswordEncoder(); //密码使用bcrypt加密
}
@Autowired
private PasswordEncoder passwordEncoder;
/**
* 处理用户名和密码验证事宜
* 1)客户端传递username和password参数到认证服务器
* 2)一般来说,username和password会存储在数据库中的用户表中
* 3)根据用户表中数据,验证当前传递过来的用户信息的合法性
* @param auth
* @throws Exception
*/
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//从数据库获取用户数据
auth.userDetailsService( jdbcUserDetailsService ).passwordEncoder( passwordEncoder );
}
}
Oauth2 server的配置类
package com.lagou.edu.oauth.config;
import com.netflix.discovery.converters.Auto;
import io.netty.util.internal.NoOpTypeParameterMatcher;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.jwt.crypto.sign.MacSigner;
import org.springframework.security.oauth2.config.annotation.configurers.ClientDetailsServiceConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configuration.AuthorizationServerConfigurerAdapter;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableAuthorizationServer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerEndpointsConfigurer;
import org.springframework.security.oauth2.config.annotation.web.configurers.AuthorizationServerSecurityConfigurer;
import org.springframework.security.oauth2.provider.client.JdbcClientDetailsService;
import org.springframework.security.oauth2.provider.token.AuthorizationServerTokenServices;
import org.springframework.security.oauth2.provider.token.DefaultTokenServices;
import org.springframework.security.oauth2.provider.token.TokenStore;
import org.springframework.security.oauth2.provider.token.store.JwtAccessTokenConverter;
import org.springframework.security.oauth2.provider.token.store.JwtTokenStore;
import javax.sql.DataSource;
/**
* FileName: OauthServerConfiger
* Author: xuan zongjun
* Date: 2021/10/29 16:44
* Description: 当前类为Oauth2 server的配置类(需要继承特定的父类AuthorizationServerConfigurerAdapter)
*/
@Configuration
@EnableAuthorizationServer // 开启认证服务器功能
public class OauthServerConfiger extends AuthorizationServerConfigurerAdapter {
@Autowired
LagouAccessTokenConvertor lagouAccessTokenConvertor; //jwt的扩展信息
@Autowired
private AuthenticationManager authenticationManager;
private String sign_key = "123456"; // jwt签名密钥
@Autowired
private DataSource dataSource;
/**
* 认证服务器最终是以api接口的方式对外提供服务(校验合法性并生成令牌、校验令牌等)
* 那么,以api接口方式对外的话,就涉及到接口的访问权限,我们需要在这里进行必要的配置
*
* @param security
* @throws Exception
*/
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure( security );
//相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问接口
security
.allowFormAuthenticationForClients() //允许客户端表单认证
.tokenKeyAccess( "permitAll()" ) // 开启端口/oauth/token_key的访问权限(允许)
.checkTokenAccess( "permitAll()" ); // 开启端口/oauth/check_token的访问权限(允许)
}
/**
* 客户端详情配置,
* 比如: client_id,secret
* 当前这个服务就如同QQ平台,拉勾网作为客户端需要qq平台进⾏登录授权认证等,提前需要到QQ平台注册,QQ平台会给拉勾网
* 颁发client_id 等必要参数,表明客户端是谁
*
* @param clients
* @throws Exception
*/
@Override
public void configure(ClientDetailsServiceConfigurer clients) throws Exception {
super.configure( clients );
//2、客户信息存储在数据库
// 从内存中加载客户端详情改为从数据库中加载客户端详情,默认表:oauth_client_details
clients.withClientDetails( createJdbcClientDetailsService() );
}
@Bean
public JdbcClientDetailsService createJdbcClientDetailsService() {
JdbcClientDetailsService jdbcClientDetailsService = new JdbcClientDetailsService( dataSource );
return jdbcClientDetailsService;
}
/**
* 认证服务器是玩转token的,那么这个配置token令牌管理相关(token此时就是一个字符串,当下的token需要在服务器端存储,
* 那么存储在哪里呢?都是在这里配置)
*
* @param endpoints
* @throws Exception
*/
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
super.configure( endpoints );
endpoints
.tokenStore( tokenStore() )// 指定token的存储方法
.tokenServices( authorizationServerTokenServices() ) // token服务的一个描述,可以认为是token生成细节的描述,比如有效时间多少等
.authenticationManager( authenticationManager ) //认证管理器对象到容器
.allowedTokenEndpointRequestMethods( HttpMethod.POST, HttpMethod.GET );
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Autowired
PasswordEncoder passwordEncoder;
/*
该方法用于创建tokenStore对象(令牌存储对象)
token以什么形式存储
*/
public TokenStore tokenStore() {
//return new InMemoryTokenStore();
return new JwtTokenStore( jwtAccessTokenConverter() );
}
/**
* 返回jwt令牌转换器(帮助我们生成jwt令牌的)
* 在这里,我们可以把签名密钥传递进去给转换器对象
*
* @return
*/
public JwtAccessTokenConverter jwtAccessTokenConverter() {
JwtAccessTokenConverter jwtAccessTokenConverter = new JwtAccessTokenConverter();
jwtAccessTokenConverter.setSigningKey( sign_key );// 签名密钥
jwtAccessTokenConverter.setVerifier( new MacSigner( sign_key ) ); // 验证时使用的密钥,和签名密钥保持一致MacSigner 指定对称加密
jwtAccessTokenConverter.setAccessTokenConverter( lagouAccessTokenConvertor );
return jwtAccessTokenConverter;
}
/**
* 该方法用户获取一个token服务对象(该对象描述了token有效期等信息)
*/
public AuthorizationServerTokenServices authorizationServerTokenServices() {
// 使用默认实现
DefaultTokenServices defaultTokenServices = new DefaultTokenServices();
defaultTokenServices.setSupportRefreshToken( true ); // 是否开启令牌刷新
defaultTokenServices.setTokenStore( tokenStore() );
//针对jwt令牌的添加
defaultTokenServices.setTokenEnhancer( jwtAccessTokenConverter() );
// 设置令牌有效时间(一般设置为2个小时)
defaultTokenServices.setAccessTokenValiditySeconds( 200 ); // access_token 就是我们请求资源需要携带的令牌
// 设置刷新令牌的有效时间
defaultTokenServices.setRefreshTokenValiditySeconds( 259200 ); //3天
return defaultTokenServices;
}
}
三、解决办法
因为springsecurity在最新版本升级后,默认把之前的明文密码方式给去掉了官方文档说明;
解决方式:
- 一般我们客户端账号密码不需要加密,所以在这里实现 .passwordEncoder(NoOpPasswordEncoder.getInstance()) 告诉security客户端密码不需要加密
- 使用BCryptPasswordEncoder将数据库中client密码加密
我的修改:
@Override
public void configure(AuthorizationServerSecurityConfigurer security) throws Exception {
super.configure( security );
//相当于打开endpoints 访问接口的开关,这样的话后期我们能够访问接口
security
.allowFormAuthenticationForClients() //允许客户端表单认证
.tokenKeyAccess( "permitAll()" ) // 开启端口/oauth/token_key的访问权限(允许)
.checkTokenAccess( "permitAll()" ) // 开启端口/oauth/check_token的访问权限(允许)
.passwordEncoder( NoOpPasswordEncoder.getInstance() ); //设置 客户信息 client_secret 使用明文验证
}
参考地址:
spring boot security 项目中Encoded password does not look like BCrypt报错解决 - 简书