oauth2自定义granter与provider实现自定义身份认证
Oauth2自定义Granter与Provider实现自定义身份验证
需求描述
公司的软件开发平台基于Oauth2实现身份认证,但今年某地区用户提出特殊需求——他们的系统必须使用集团公司认证平台登录,而后利用返回的token进入我公司系统。
为了以最小代价实现该需求,我们决定自定义一个认证模式,解析用户传入的token以获得员工编号,进而发放我方token以便应用端后续调用资源服务。
实现思路
Oauth提供几种基本的认证模式,如密码模式、客户端模式、授权码模式和几乎不用的简易模式。同时,还提供了认证模式的扩展机制,以便于我们在遇到特殊情况时根据自己的需求来完成身份验证。因此,我们决定实现一个自定义的集团公司凭据验证模式来校验用户身份信息。
自定义Token
在Oauth中,我们最常见的Token类型非要数UsernamePasswordAuthenticationToken
不可了,所有基于用户名和密码进行验证的模式,最终都要返回一个UsernamePasswordAuthenticationToken
的实例。但我们的需求中没有用户名和密码,所以我们自定义一个集团公司认证票据GroupCompanyAuthenticationToken
。
public class GroupCompanyAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
/** 认证未通过时的初始化方法 */
public GroupCompanyAuthenticationToken(String token){
super(null);
this.principal = token;
setAuthenticated(false);
}
/** 认证通过后的初始化方法 */
public GroupCompanyAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities){
super(authorities);
this.principal = principal;
super.setAuthenticated(true);
}
@Override
public Object getCredentials() {
return null;
}
@Override
public Object getPrincipal() {
return this.principal;
}
@Override
public void setAuthenticated(boolean isAuthenticated) {
if (isAuthenticated) {
throw new IllegalArgumentException(
"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
}
super.setAuthenticated(false);
}
}
可能有一些小伙伴不知道这个东西是干嘛使的。我也没法给你一个非常标准正规的说法,只能按我的理解来简单解释一下:
首先,我们自定义了一个类,集成自AbstractAuthenticationToken
,因此它拥有AbstractAuthenticationToken
的特性,可以被oauth识别和调用。
当认证开始进行的时候,利用第一个构造方法来创建一个实例,此时传入一个token值作为principal,注意此时构造方法中setAuthenticated(false)
表示当前未通过认证。
当我们确认传入的token是有效的,再调用第二个构造方法来创建一个新实例,此时setAuthenticated(true)
表示已通过认证。
这两次创建实例的操作分别位于Granter和Provider,在后面会看到。
oauth通过读取该实例的属性来判断是否通过认证,是否可以颁发令牌。
自定义Granter
什么是Granter?说白了,它就是授权模式。在oauth中, ResourceOwnerPasswordTokenGranter
定义出了我们常见的密码模式,AuthorizationCodeTokenGranter
定义出了授权码模式。在此我们定义一个GroupCompanyTokenGranter
来实现我们自己的集团公司凭据认证模式。
public class GroupCompanyTokenGranter extends AbstractTokenGranter {
//我们授权模式注册到oauth中的名称
private static final String GRANT_TYPE = "group_token_authentication";
private final AuthenticationManager authenticationManager;
public GroupCompanyTokenGranter(AuthenticationManager authenticationManager, AuthorizationServerTokenServices tokenServices
, ClientDetailsService clientDetailsService, OAuth2RequestFactory requestFactory) {
super(tokenServices, clientDetailsService, requestFactory, GRANT_TYPE);
this.authenticationManager = authenticationManager;
}
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<>(tokenRequest.getRequestParameters());
//接收传入的参数
String token = parameters.get("group_company_token");
//利用第一个构造方法来创建一个实例
Authentication userAuth = new GroupCompanyAuthenticationToken(token);
//把用户传入的参数交给自定义的Token
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
userAuth = authenticationManager.authenticate(userAuth);
if (userAuth == null || !userAuth.isAuthenticated()) {
throw new InvalidGrantException("Could not authenticate group company token: " + token);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
}
这段代码比较简单,说白了,当用户进行身份校验时,如果传入的grant_type
为group_token_authentication
,那么则自动进入这段逻辑创建GroupCompanyAuthenticationToken
对象的实例,并将request接收到的参数传入。
自定义Provider
oauth接收到了你传入的grant_type
,并把你的请求转发到了你自己的Granter,但谁来进行真正的用户信息合法性校验呢?自然就是接下来要用到的Provider。我们新建一个GroupCompanyAuthenticationProvider
类,继承oauth的AuthenticationProvider
,重写其中的部分代码即可。
@Setter
public class GroupCompanyAuthenticationProvider implements AuthenticationProvider {
//我们自己获取用户信息的服务
private IUserService userService;
//spring security提供的UserDetailsService
private UserDetailsService userDetailsService;
//集团公司认证平台提供的获取用户信息接口地址
private String getUserInfoUri;
private RestTemplate restTemplate;
public GroupCompanyAuthenticationProvider(){
this.restTemplate = new RestTemplate();
}
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
//此时principal是传进来的token,根据token查询用户编号
Object principal = authentication.getPrincipal();
if (principal == null || "".equals(principal.toString())){
throw new PrincipalNotFoundException("未传入principal。");
}
//使用RestTemplate调用集团公司接口,利用他们下发的token来获取用户信息
MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
params.add("token", principal.toString());
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(getUserInfoUri);
URI uri = builder.queryParams(params).build().encode().toUri();
ResponseEntity<Map> responseEntity = restTemplate.getForEntity(uri, Map.class);
Map<String, String> responseData = responseEntity.getBody();
if (responseData.get("code") != null && "500".equals(responseData.get("code"))){
//懒得自定义异常了。抛个密码错误给前面自己体会……
throw new BadCredentialsException("Invalid Token");
}
//获取员工编号
String userNo = responseData.get("userName");
//UserDetailsService有一个默认的loadUserByUsername方法,我自己写了一个loadUserByUserNo方法,表示利用员工编号获取用户信息。返回的UserDetails是我们最终需要的那个Principal
UserDetails userDetails = ((UserDetailsServiceImpl)userDetailsService).loadUserByUserNo(userNo);
//注意,这里用到了自定义Token的第二个构造方法,它将告诉oauth此时已经通过认证。
//但是,如果UserDetails对象为null,后面的逻辑将抛出异常,认证还是过不去。
GroupCompanyAuthenticationToken authenticationToken = new GroupCompanyAuthenticationToken(userDetails, userDetails.getAuthorities());
//返回一个通过认证的自定义token对象,大功告成
return authenticationToken;
}
/**
这个方法用于判断,用户发送过来的认证请求是否适用于当前provider来处理。
参考上面自定义Granter的代码,它创建了一个GroupCompanyAuthenticationToken的实例,因此这里会返回true。
*/
@Override
public boolean supports(Class<?> aClass) {
return GroupCompanyAuthenticationToken.class.isAssignableFrom(aClass);
}
}
此时,我们自定义认证过程的大部分工作都已经完成了。接下来我们需要把自己写的这些东西告诉oauth,否则它怎么知道多了这些操作呢?
配置Provider
在SecurityConfig中,初始化provider,代码如下:
@EnableWebSecurity
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private UserDetailsService userDetailsService;
@Autowired
private IUserService userService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
//实例化provider,把需要的东西set进去
GroupCompanyAuthenticationProvider provider = new GroupCompanyAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setUserService(userService);
provider.setGetUserInfoUri('http://127.0.0.1/getUserUri');
auth.authenticationProvider(provider);
}
}
配置自定义Granter,代码如下
@Configuration
@EnableAuthorizationServer
@EnableJdbcHttpSession(maxInactiveIntervalInSeconds = 28800)
public class OAuth2AuthServerConfig extends AuthorizationServerConfigurerAdapter {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
//灵魂在这里
endpoints.tokenGranter(new CompositeTokenGranter(initGranters(endpoints)));
}
//你要一股脑把所有自定义的和原有的认证模式都加进去,除非你确保其他的模式永远都用不到
private List<TokenGranter> initGranters(AuthorizationServerEndpointsConfigurer endpoints) {
AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();
ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();
OAuth2RequestFactory oAuth2RequestFactory = endpoints.getOAuth2RequestFactory();
AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();
//自定义Granter
List<TokenGranter> customTokenGranters = new ArrayList<>();
customTokenGranters.add(new GroupCompanyAuthenticationGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));
//添加密码模式
customTokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));
//刷新模式
customTokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
//简易模式
customTokenGranters.add(new ImplicitTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
//客户端模式
customTokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));
//授权码模式
customTokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetailsService, oAuth2RequestFactory));
return customTokenGranters;
}
}
修改Client_details表
最后,你还需要为你的应用客户端增加自定义认证模式的支持,否则还是用不了。
在数据库中找到OAUTH_CLIENT_DETAILS
表,在AUTHORIZED_GRANT_TYPES
中增加我们自定义的授权模式:
authorization_code,password,refresh_token
修改为
authorization_code,password,refresh_token,group_token_authentication
测试
postman我就不截图了。
调用 http://localhost/oauth/token,传参
grant_type: group_token_authentication
group_company_token: 获取到的jwt token
但这里有个前提,还是要利用clientId和clientSecret生成Basic token放到header中,否则过不了Basic认证。