引言: 以前系列文章《认证鉴权与API权限控制在微服务架构中的设计与实现》,前面文章已经将认证鉴权与API权限控制的流程和主要细节讲解完。因为有些同窗想了解下受权码模式,本文特意补充讲解。html
受权码类型(authorization code)经过重定向的方式让资源全部者直接与受权服务器进行交互来进行受权,避免了资源全部者信息泄漏给客户端,是功能最完整、流程最严密的受权类型,可是须要客户端必须能与资源全部者的代理(一般是Web浏览器)进行交互,和可从受权服务器中接受请求(重定向给予受权码),受权流程以下:java
+----------+
| Resource |
| Owner |
| |
+----------+
^
|
(B)
+----|-----+ Client Identifier +---------------+
| -+----(A)-- & Redirection URI ---->| |
| User- | | Authorization |
| Agent -+----(B)-- User authenticates --->| Server |
| | | |
| -+----(C)-- Authorization Code ---<| |
+-|----|---+ +---------------+
| | ^ v
(A) (C) | |
| | | |
^ v | |
+---------+ | |
| |>---(D)-- Authorization Code ---------' |
| Client | & Redirection URI |
| | |
| |<---(E)----- Access Token -------------------'
+---------+ (w/ Optional Refresh Token)
复制代码
因为受权码模式须要登陆用户给请求access_token的客户端受权,因此auth-server须要添加Spring-Security的相关配置用于引导用户进行登陆。spring
在原来的基础上,进行Spring-Securiy相关配置,容许用户进行表单登陆:数据库
@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
CustomLogoutHandler customLogoutHandler;
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
.and()
.requestMatchers().antMatchers("/**")
.and().authorizeRequests()
.antMatchers("/**").permitAll()
.anyRequest().authenticated()
.and().formLogin()
.permitAll()
.and().logout()
.logoutUrl("/logout")
.clearAuthentication(true)
.logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
.addLogoutHandler(customLogoutHandler);
}
}
复制代码
同时须要把ResourceServerConfig
中的资源服务器中的对于登出端口的处理迁移到WebSecurityConfig
中,注释掉ResourceServerConfig
的HttpSecurity
配置:json
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
// @Override
// public void configure(HttpSecurity http) throws Exception {
// http.csrf().disable()
// .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
// .and()
// .requestMatchers().antMatchers("/**")
// .and().authorizeRequests()
// .antMatchers("/**").permitAll()
// .anyRequest().authenticated()
// .and().logout()
// .logoutUrl("/logout")
// .clearAuthentication(true)
// .logoutSuccessHandler(new HttpStatusReturningLogoutSuccessHandler())
// .addLogoutHandler(customLogoutHandler());
//
// //http.antMatcher("/api/**").addFilterAt(customSecurityFilter(), FilterSecurityInterceptor.class);
//
// }
/* @Bean public CustomSecurityFilter customSecurityFilter() { return new CustomSecurityFilter(); } */
.....
}
复制代码
因为用户表单登陆的认证过程可能有所不一样,为此再添加一个CustomSecurityAuthenticationProvider
,基本上与CustomAuthenticationProvider
一致,只是忽略对client客户端的认证和处理。api
@Component
public class CustomSecurityAuthenticationProvider implements AuthenticationProvider {
@Autowired
private UserClient userClient;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
String username = authentication.getName();
String password;
Map map;
password = (String) authentication.getCredentials();
//若是你是调用user服务,这边不用注掉
//map = userClient.checkUsernameAndPassword(getUserServicePostObject(username, password, type));
map = checkUsernameAndPassword(getUserServicePostObject(username, password));
String userId = (String) map.get("userId");
if (StringUtils.isBlank(userId)) {
String errorCode = (String) map.get("code");
throw new BadCredentialsException(errorCode);
}
CustomUserDetails customUserDetails = buildCustomUserDetails(username, password, userId);
return new CustomAuthenticationToken(customUserDetails);
}
private CustomUserDetails buildCustomUserDetails(String username, String password, String userId) {
CustomUserDetails customUserDetails = new CustomUserDetails.CustomUserDetailsBuilder()
.withUserId(userId)
.withPassword(password)
.withUsername(username)
.withClientId("for Security")
.build();
return customUserDetails;
}
private Map<String, String> getUserServicePostObject(String username, String password) {
Map<String, String> requestParam = new HashMap<String, String>();
requestParam.put("userName", username);
requestParam.put("password", password);
return requestParam;
}
//模拟调用user服务的方法
private Map checkUsernameAndPassword(Map map) {
//checkUsernameAndPassword
Map ret = new HashMap();
ret.put("userId", UUID.randomUUID().toString());
return ret;
}
@Override
public boolean supports(Class<?> aClass) {
return true;
}
}
复制代码
在AuthenticationManagerConfig
添加CustomSecurityAuthenticationProvider
配置:浏览器
@Configuration
public class AuthenticationManagerConfig extends GlobalAuthenticationConfigurerAdapter {
@Autowired
CustomAuthenticationProvider customAuthenticationProvider;
@Autowired
CustomSecurityAuthenticationProvider securityAuthenticationProvider;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.authenticationProvider(customAuthenticationProvider)
.authenticationProvider(securityAuthenticationProvider);
}
}
复制代码
保证数据库中的请求客户端存在受权码的请求受权和具有回调地址,回调地址是用来接受受权码的。服务器
启动服务,浏览器访问地址http://localhost:9091/oauth/authorize?response_type=code&client_id=frontend& scope=all&redirect_uri=http://localhost:8080
。微信
重定向到登陆界面,引导用户登陆:session
登陆成功,受权客户端获取受权码。
受权以后,从回调地址中获取到受权码:
http://localhost:8080/?code=7OglOJ
复制代码
携带受权码获取对应的token:
AuthorizationServerTokenServices
是受权服务器中进行token操做的接口,提供了如下的三个接口:
public interface AuthorizationServerTokenServices {
// 生成与OAuth2认证绑定的access_token
OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
// 根据refresh_token刷新access_token
OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest) throws AuthenticationException;
// 获取OAuth2认证的access_token,若是access_token存在的话
OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}
复制代码
请注意,生成的token都是与受权的用户进行绑定的。
AuthorizationServerTokenServices
接口的默认实现是DefaultTokenServices
,注意token经过TokenStore
进行保存管理。
生成token:
//DefaultTokenServices
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 从TokenStore获取access_token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
// 若是access_token已经存在可是过时了
// 删除对应的access_token和refresh_token
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// 若是access_token已经存在而且没有过时
// 从新保存一下防止authentication改变,而且返回该access_token
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// 只有当refresh_token为null时,才从新建立一个新的refresh_token
// 这样可使持有过时access_token的客户端能够根据之前拿到refresh_token拿到从新建立的access_token
// 由于建立的access_token须要绑定refresh_token
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
// 若是refresh_token也有期限而且过时,从新建立
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 绑定受权用户和refresh_token建立新的access_token
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
// 将access_token与受权用户对应保存
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
// 将refresh_token与受权用户对应保存
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
复制代码
须要注意到,在建立token的过程当中,会根据该受权用户去查询是否存在未过时的access_token,有就直接返回,没有的话才会从新建立新的access_token,同时也应该注意到是先建立refresh_token,再去建立access_token,这是为了防止持有过时的access_token可以经过refresh_token从新得到access_token,由于先后建立access_token绑定了同一个refresh_token。
DefaultTokenServices
中刷新token的refreshAccessToken()
以及获取token的getAccessToken()
方法就留给读者们本身去查看,在此不介绍。
本文主要讲了受权码模式,在受权码模式须要用户登陆以后进行受权才获取获取受权码,再携带受权码去向TokenEndpoint
请求访问令牌,固然也能够在请求中设置response_token=token
经过隐式类型直接获取到access_token。这里须要注意一个问题,在到达AuthorizationEndpoint
端点时,并无对客户端进行验证,可是必需要通过用户认证的请求才能被接受。
系列文章:认证鉴权与API权限控制在微服务架构中的设计与实现