Oauth2自定义Granter与Provider实现自定义身份验证
需求描述
公司的软件开发平台基于Oauth2实现身份认证,但今年某地区用户提出特殊需求——他们的系统必须使用集团公司认证平台登录,而后利用返回的token进入我公司系统。
为了以最小代价实现该需求,我们决定自定义一个认证模式,解析用户传入的token以获得员工编号,进而发放我方token以便应用端后续调用资源服务。
实现思路
Oauth提供几种基本的认证模式,如密码模式、客户端模式、授权码模式和几乎不用的简易模式。同时,还提供了认证模式的扩展机制,以便于我们在遇到特殊情况时根据自己的需求来完成身份验证。因此,我们决定实现一个自定义的集团公司凭据验证模式来校验用户身份信息。
自定义Token
在Oauth中,我们最常见的Token类型非要数
UsernamePasswordAuthenticationToken
不可了,所有基于用户名和密码进行验证的模式,最终都要返回一个
UsernamePasswordAuthenticationToken
的实例。但我们的需求中没有用户名和密码,所以我们自定义一个集团公司认证票据
GroupCompanyAuthenticationToken
。
publicclassGroupCompanyAuthenticationTokenextendsAbstractAuthenticationToken{privatefinalObject principal;/** 认证未通过时的初始化方法 */publicGroupCompanyAuthenticationToken(String token){super(null);this.principal = token;setAuthenticated(false);}/** 认证通过后的初始化方法 */publicGroupCompanyAuthenticationToken(Object principal,Collection<?extendsGrantedAuthority> authorities){super(authorities);this.principal = principal;super.setAuthenticated(true);}@OverridepublicObjectgetCredentials(){returnnull;}@OverridepublicObjectgetPrincipal(){returnthis.principal;}@OverridepublicvoidsetAuthenticated(boolean isAuthenticated){if(isAuthenticated){thrownewIllegalArgumentException("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
来实现我们自己的集团公司凭据认证模式。
publicclassGroupCompanyTokenGranterextendsAbstractTokenGranter{//我们授权模式注册到oauth中的名称privatestaticfinalStringGRANT_TYPE="group_token_authentication";privatefinalAuthenticationManager authenticationManager;publicGroupCompanyTokenGranter(AuthenticationManager authenticationManager,AuthorizationServerTokenServices tokenServices
,ClientDetailsService clientDetailsService,OAuth2RequestFactory requestFactory){super(tokenServices, clientDetailsService, requestFactory,GRANT_TYPE);this.authenticationManager = authenticationManager;}@OverrideprotectedOAuth2AuthenticationgetOAuth2Authentication(ClientDetails client,TokenRequest tokenRequest){Map<String,String> parameters =newLinkedHashMap<>(tokenRequest.getRequestParameters());//接收传入的参数String token = parameters.get("group_company_token");//利用第一个构造方法来创建一个实例Authentication userAuth =newGroupCompanyAuthenticationToken(token);//把用户传入的参数交给自定义的Token((AbstractAuthenticationToken) userAuth).setDetails(parameters);
userAuth = authenticationManager.authenticate(userAuth);if(userAuth ==null||!userAuth.isAuthenticated()){thrownewInvalidGrantException("Could not authenticate group company token: "+ token);}OAuth2Request storedOAuth2Request =getRequestFactory().createOAuth2Request(client, tokenRequest);returnnewOAuth2Authentication(storedOAuth2Request, userAuth);}}
这段代码比较简单,说白了,当用户进行身份校验时,如果传入的
grant_type
为
group_token_authentication
,那么则自动进入这段逻辑创建
GroupCompanyAuthenticationToken
对象的实例,并将request接收到的参数传入。
自定义Provider
oauth接收到了你传入的
grant_type
,并把你的请求转发到了你自己的Granter,但谁来进行真正的用户信息合法性校验呢?自然就是接下来要用到的Provider。我们新建一个
GroupCompanyAuthenticationProvider
类,继承oauth的
AuthenticationProvider
,重写其中的部分代码即可。
@SetterpublicclassGroupCompanyAuthenticationProviderimplementsAuthenticationProvider{//我们自己获取用户信息的服务privateIUserService userService;//spring security提供的UserDetailsServiceprivateUserDetailsService userDetailsService;//集团公司认证平台提供的获取用户信息接口地址privateString getUserInfoUri;privateRestTemplate restTemplate;publicGroupCompanyAuthenticationProvider(){this.restTemplate =newRestTemplate();}@OverridepublicAuthenticationauthenticate(Authentication authentication)throwsAuthenticationException{//此时principal是传进来的token,根据token查询用户编号Object principal = authentication.getPrincipal();if(principal ==null||"".equals(principal.toString())){thrownewPrincipalNotFoundException("未传入principal。");}//使用RestTemplate调用集团公司接口,利用他们下发的token来获取用户信息MultiValueMap<String,String> params =newLinkedMultiValueMap<>();
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"))){//懒得自定义异常了。抛个密码错误给前面自己体会……thrownewBadCredentialsException("Invalid Token");}//获取员工编号String userNo = responseData.get("userName");//UserDetailsService有一个默认的loadUserByUsername方法,我自己写了一个loadUserByUserNo方法,表示利用员工编号获取用户信息。返回的UserDetails是我们最终需要的那个PrincipalUserDetails userDetails =((UserDetailsServiceImpl)userDetailsService).loadUserByUserNo(userNo);//注意,这里用到了自定义Token的第二个构造方法,它将告诉oauth此时已经通过认证。//但是,如果UserDetails对象为null,后面的逻辑将抛出异常,认证还是过不去。GroupCompanyAuthenticationToken authenticationToken =newGroupCompanyAuthenticationToken(userDetails, userDetails.getAuthorities());//返回一个通过认证的自定义token对象,大功告成return authenticationToken;}/**
这个方法用于判断,用户发送过来的认证请求是否适用于当前provider来处理。
参考上面自定义Granter的代码,它创建了一个GroupCompanyAuthenticationToken的实例,因此这里会返回true。
*/@Overridepublicbooleansupports(Class<?> aClass){returnGroupCompanyAuthenticationToken.class.isAssignableFrom(aClass);}}
此时,我们自定义认证过程的大部分工作都已经完成了。接下来我们需要把自己写的这些东西告诉oauth,否则它怎么知道多了这些操作呢?
配置Provider
在SecurityConfig中,初始化provider,代码如下:
@EnableWebSecuritypublicclassBrowserSecurityConfigextendsWebSecurityConfigurerAdapter{@AutowiredprivateUserDetailsService userDetailsService;@AutowiredprivateIUserService userService;@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{//实例化provider,把需要的东西set进去GroupCompanyAuthenticationProvider provider =newGroupCompanyAuthenticationProvider();
provider.setUserDetailsService(userDetailsService);
provider.setUserService(userService);
provider.setGetUserInfoUri('http://127.0.0.1/getUserUri');
auth.authenticationProvider(provider);}}
配置自定义Granter,代码如下
@Configuration@EnableAuthorizationServer@EnableJdbcHttpSession(maxInactiveIntervalInSeconds =28800)publicclassOAuth2AuthServerConfigextendsAuthorizationServerConfigurerAdapter{@AutowiredprivateAuthenticationManager authenticationManager;@AutowiredprivateUserDetailsService userDetailsService;@Overridepublicvoidconfigure(AuthorizationServerEndpointsConfigurer endpoints)throwsException{//灵魂在这里
endpoints.tokenGranter(newCompositeTokenGranter(initGranters(endpoints)));}//你要一股脑把所有自定义的和原有的认证模式都加进去,除非你确保其他的模式永远都用不到privateList<TokenGranter>initGranters(AuthorizationServerEndpointsConfigurer endpoints){AuthorizationServerTokenServices tokenServices = endpoints.getTokenServices();ClientDetailsService clientDetailsService = endpoints.getClientDetailsService();OAuth2RequestFactory oAuth2RequestFactory = endpoints.getOAuth2RequestFactory();AuthorizationCodeServices authorizationCodeServices = endpoints.getAuthorizationCodeServices();//自定义GranterList<TokenGranter> customTokenGranters =newArrayList<>();
customTokenGranters.add(newGroupCompanyAuthenticationGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));//添加密码模式
customTokenGranters.add(newResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices, clientDetailsService, oAuth2RequestFactory));//刷新模式
customTokenGranters.add(newRefreshTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//简易模式
customTokenGranters.add(newImplicitTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//客户端模式
customTokenGranters.add(newClientCredentialsTokenGranter(tokenServices, clientDetailsService, oAuth2RequestFactory));//授权码模式
customTokenGranters.add(newAuthorizationCodeTokenGranter(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认证。
版权归原作者 slxz001 所有, 如有侵权,请联系我们删除。