框架:Spring security oauth2+MmongoDB
问题描述:请求自定义登陆接口和使用默认接口都都能正常返回token信息,但携带refresh_token换取token时,则报错。自定义登陆接口相关配置和错误信息如下。
ResourceConfig配置如下:
@EnableResourceServer
@Configuration
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Autowired
private SysUserService sysUserService;
@Autowired
private CustomAuthenticationSuccessHandler customAuthenticationSuccessHandler;
@Autowired
private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;
@Autowired
private CustomAuthenticationEntryPoint authenticationEntryPoint;
@Autowired
private CustomAccessDeineHandler accessDeniedHandler;
@Autowired
private CustomLogoutSuccessHandler logoutSuccessHandler;
@Autowired
private TheCustomAuthenticationProvider theCustomAuthenticationProvider;
@Value("${custom.app.formLogin.uri}")
private String formLoginUri;
@Autowired
private PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider;
@Override
public void configure(HttpSecurity http) throws Exception {
http
.formLogin()
.loginProcessingUrl(formLoginUri)//自定义登陆接口,输入相关信息,返回token
.successHandler(customAuthenticationSuccessHandler) //成功处理器 返回Token
.failureHandler(customAuthenticationFailureHandler)//失败处理器
.and()
.csrf().disable() //模拟请求
.cors()
.and()
.authorizeRequests()
.antMatchers("/sysUser/register","logout","/sysUser/restPwd").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling() // 配置被拦截时的处理
.authenticationEntryPoint(authenticationEntryPoint)// token效或者没有携带token时的处理
.accessDeniedHandler(accessDeniedHandler)
.and()
.logout()
.logoutSuccessHandler(logoutSuccessHandler)
.deleteCookies("JSESSIONID")
.and()
.sessionManagement()
.maximumSessions(1)//单个用户所允许同时在线的最大会话数量
.maxSessionsPreventsLogin(true)//会话达到最大数量时,创建新的会话而不是踢掉旧会话
;
}
@Bean("passwordEncoder") //密码加密
public static BCryptPasswordEncoder passwordEncoder(){
final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
return bCryptPasswordEncoder;
}
@Bean
public HttpSessionEventPublisher httpSessionEventPublisher(){
return new HttpSessionEventPublisher();
}
}
WebSecurityConfigurer配置如下
/**
* @descript security基础配置,若ResourceServerConfig中亦配置,则此文件http配置被覆盖
* */
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private Log logger = LogFactory.getLog(this.getClass());
@Override /*认证管理使用默认*/
@Bean("authenticationManagerBean")
public AuthenticationManager authenticationManagerBean() throws Exception {
logger.info("===============配置WebSecurityConfig的Bean:authenticationManagerBean=====================");
return super.authenticationManagerBean();
}
}
自定义认证程序如下:
/**@author 离染
* @description 框架未默认在authenticationManager中对authenticationProvider进行初始化,导致token无法刷新,需手动添加。
* @main function 校验用户名和密码,表单登录自定义认证器*/
@Component
@ApiOperation(value="根据用户名判断密码是否正确")
public class TheCustomAuthenticationProvider implements AuthenticationProvider , Serializable {
private static final Log logger = LogFactory.getLog(TheCustomAuthenticationProvider.class);
@Autowired
private SysUserService sysUserService;
@Autowired
private BCryptPasswordEncoder bCryptPasswordEncoder;
@Value("${custom.app.client.isCiphertext}")
private boolean isCiphertxt;
@Override
@ApiOperation("作用:验证用户是否存在,判断输入密码是否正确")
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (!supports(authentication.getClass())) {
return null;
}
String textUsername = authentication.getName();
String textPassword = authentication.getCredentials().toString();
logger.info("解密前的报文:username:"+textUsername+", password:"+textPassword);
//如果是秘文,则先解密
if(isCiphertxt){
String[] usernamePwd = decryptionTheCiphertext(textUsername,textPassword);
textPassword = usernamePwd[0];
textUsername = usernamePwd[1];
}
logger.info("解密后的报文:username:"+textUsername+", password:"+textPassword);
UserDetails userDetails = validateUsernamePassword(textUsername,textPassword);
UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken(userDetails,userDetails.getPassword(),userDetails.getAuthorities());
return usernamePasswordAuthenticationToken;
}
@Override
public final boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
}
/**解密DES*/
private String[] decryptionTheCiphertext(String ciphertxtUsername,String ciphertextPassword){
logger.info("开始解密用户名和密码......");
Map<String,String> usernaAndKey = DESEnDeUtils.getUsernameDESEncode(ciphertxtUsername);
String textUsername = usernaAndKey.get("username");
String desKey = usernaAndKey.get("desKey");
String textPassword = DESEnDeUtils.getPasswordDESEncode(ciphertextPassword,desKey);
logger.info("解密用户名和密码成功,用户名:" + textUsername + ",密码:" + null + ",秘钥:" + desKey);
return new String[]{textUsername,textPassword};
}
/**校验用户名和密码*/
private UserDetails validateUsernamePassword(String textUsername,String textPassword){
//判断用户是否存在
UserDetails userDetails = sysUserService.loadUserByUsername(textUsername);
if (userDetails == null) { throw new UsernameNotFoundException("用户不存在."); }
boolean isEquals = bCryptPasswordEncoder.matches(textPassword,userDetails.getPassword());
//判断密码是否正确
if(!isEquals) { throw new BadCredentialsException("密码错误");}
return userDetails;
}
}
错误信息如下:
o.s.s.o.provider.endpoint.TokenEndpoint : Handling error: ProviderNotFoundException, No AuthenticationProvider found for org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken
错误背景:获取token时(/oauth/token或自定义接口),走的是自定义认证程序(TheCustomAuthenticationProvider ),一切正常,但刷新token时报上述错误。访问链接如下:
1,自定义接口登陆,获取token: POST http://127.0.0.1:8090/api/sso/form/login?username=root&password=root&grant_type=password Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0 //Basic client id : client secret2,默认接口获取token
### 默认接口获取token POST http://127.0.0.1:8090/api/sso/oauth/token?username=root&password=root&grant_type=password Authorization: Basic Y2xpZW50SWQ6Y2xpZW50U2VjcmV0 //Basic client id : client secret返回报文为:
3,刷新token访问接口
POST http://localhost:8090/api/sso/oauth/token?client_id=clientId&client_secret=clientSecret&grant_type=refresh_token&refresh_token=9fc71604-dfbf-4817-9e88-e100adcbeda2
即报错!
经多方查阅资料,查看源码,经过数小时爆肝,记不得多少次的尝试之下,终于找到原因。请求token和刷新token调用不同的认证处理器。查看DefaultTokenService源码,当刷新时默认使用PreAuthenticatedAuthenticationToken类,如图所示。
至此,已经很明显,spring默认没有初始化PreAuthenticatedAuthenticationToken类,故需创建此类的Bean,PreAuthenticatedAuthenticationToken又依靠PreAuthenticatedAuthenticationProvider而存在,故需建一个PreAuthenticatedAuthenticationProvider类型的bean,更改WebSecurityConfig后,代码如下。
@EnableWebSecurity
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
private Log logger = LogFactory.getLog(this.getClass());
@Autowired
private PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider;
@Autowired
private TheCustomAuthenticationProvider theCustomAuthenticationProvider;
@Override /*认证管理使用默认,认证管理器默认也未初始化,故需创建Bean,否则亦会报错*/
@Bean("authenticationManagerBean")
public AuthenticationManager authenticationManagerBean() throws Exception {
logger.info("===============配置WebSecurityConfig的Bean:authenticationManagerBean=====================");
return super.authenticationManagerBean();
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth
.authenticationProvider(preAuthenticatedAuthenticationProvider)
.authenticationProvider(theCustomAuthenticationProvider)
;
}
@Bean//刷新token时自动调用,不能用TheCustomAuthenticationProvider替代
public PreAuthenticatedAuthenticationProvider preAuthenticatedAuthenticationProvider(){
PreAuthenticatedAuthenticationProvider PAAP = new PreAuthenticatedAuthenticationProvider();
PAAP.setPreAuthenticatedUserDetailsService(new UserDetailsByNameServiceWrapper<>(new SysUserService()));
return PAAP;
}
}
至此,问题得以解决。但又出现新的错误(mongoTemplate is NULL),为方便查阅,每篇文章记录一个错误,故留在下一章述说。
若此文对您有帮助,我不胜荣幸,能赞一个,不胜感激。