上章介绍了授权码模式,现在再来介绍密码模式,简单的如同砍瓜切菜。
所谓密码模式,即用户提供username,password,clientId,clientSecret,grantType=password等信息,请求/oauth/token,获得access_token,用户即可通过access_token访问资源。
还是以oauth2-demo-master项目为例,只用添加client的认证方法password。
启动QQ项目,程序自动会启动几个endpoints,如/oauth/token,/oauth/authorize等。
此时FilterChainProxy的filter顺序如下。重要的Filter有ClientCredentialsTokenEndpointFilter和BasicAuthenticationFilter,前者从request parameters中抽取client信息,后者从header Authorization Basic XXXX中抽取client信息。
用postman发送请求,所有参数写在parameters中,返回access_token。
localhost:8080/oauth/token?username=250577914&password=123456&grant_type=password&client_id=aiqiyi&client_secret=secret
ClientCredentialsTokenEndpointFilter会从parameter中抽取client_id,client_secret信息,并进行client的身份验证。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException, IOException, ServletException {
if (allowOnlyPost && !"POST".equalsIgnoreCase(request.getMethod())) {
throw new HttpRequestMethodNotSupportedException(request.getMethod(), new String[] { "POST" });
}
String clientId = request.getParameter("client_id");
String clientSecret = request.getParameter("client_secret");
// If the request is already authenticated we can assume that this
// filter is not needed
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication != null && authentication.isAuthenticated()) {
return authentication;
}
if (clientId == null) {
throw new BadCredentialsException("No client credentials presented");
}
if (clientSecret == null) {
clientSecret = "";
}
clientId = clientId.trim();
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest);
}
Postman发送请求如下,Basic Auth中填写client_id和client_secret信息。点击update requests后,postman会将client信息用base64加密,写在header——Authorization中。
这样一来,ClientCredentialsTokenEndpointFilter会由于参数中没有client_id自动跳过。BasicAuthenticationFilter会获取header中的Authorization Basic,提取出客户端信息。
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
final boolean debug = this.logger.isDebugEnabled();
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
chain.doFilter(request, response);
return;
}
try {
// Base64 反解码
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length == 2;
String username = tokens[0];
if (authenticationIsRequired(username)) {
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
username, tokens[1]);
authRequest.setDetails(
this.authenticationDetailsSource.buildDetails(request));
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
SecurityContextHolder.getContext().setAuthentication(authResult);
this.rememberMeServices.loginSuccess(request, response, authResult);
onSuccessfulAuthentication(request, response, authResult);
}
}
catch (AuthenticationException failed) {
...
}
chain.doFilter(request, response);
}
无论是哪一种方式访问/oauth/token,都事先验证了client信息,并作为authentication存储在SecurityContextHolder中。传递到TokenEndPoint的principal是client,paramters包含了user的信息和grantType。
OAuth2AuthentiactionProcessingFilter,从request中提取access_token,构建PreAuthenticatedAuthenticationToken并验证。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
ServletException {
final boolean debug = logger.isDebugEnabled();
final HttpServletRequest request = (HttpServletRequest) req;
final HttpServletResponse response = (HttpServletResponse) res;
try {
// 从request中提取access_token
Authentication authentication = tokenExtractor.extract(request);
if (authentication == null) {
if (stateless && isAuthenticated()) {
SecurityContextHolder.clearContext();
}
}
else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
}
// OAuth2AuthenticationManager 验证PreAuthenticatedAuthenticationToken
Authentication authResult = authenticationManager.authenticate(authentication);
eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
}
catch (OAuth2Exception failed) {
SecurityContextHolder.clearContext();
eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
authenticationEntryPoint.commence(request, response,
new InsufficientAuthenticationException(failed.getMessage(), failed));
return;
}
chain.doFilter(request, response);
}
tokenExtractor.extract(request)实际调用的是BearTokenExtractor里的extract方法,从Authorization header “Bearer xxxx”中抽取token,或者从request parameters抽取名为“access_token”的参数值。
@Override
public Authentication extract(HttpServletRequest request) {
String tokenValue = extractToken(request);
if (tokenValue != null) {
// 记为PreAuthenticatedAuthenticationToken
PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
return authentication;
}
return null;
}
构建后的authentication参数如下,tokenType="Bearer",principal=token值。再根据OAuth2AuthenticationManager验证该authentication的合法性。
postman发送如下请求,将access_token写在Authorization的header里,前缀是Bearer。
同样是OAuth2AuthenticationProcessingFilter拦截,从request header中提取token,记为PreAuthenticatedAuthenticationToken,用OAuth2AuthenticationManager进行验证。
当oauthserver和resourceserver不在一个应用程序时,访问resource,会自动转交到oauthserver的/oauth/check_token,获得access_token的验证结果。
配置application.yml
server:
port: 8081
security:
oauth2:
client:
client-id: aiqiyi
client-secret: secret
access-token-uri: http://localhost:8080/oauth/token
user-authorization-uri: http://localhost:8080/oauth/authorize
user-logout-uri: http://localhost:8080/oauth/logout
resource:
id: qq
token-info-uri: http://localhost:8080/oauth/check_token
prefer-token-info: true
filter-order: 3
basic:
enabled: false
配置ResouceServerConfigurerAdapter
@Configuration
@EnableResourceServer
@Order(SecurityProperties.ACCESS_OVERRIDE_ORDER)
public class ResourceServerConfig extends ResourceServerConfigurerAdapter {
@Value("${security.oauth2.resource.id}")
public String RESOURCE_ID;
@Autowired
public RemoteTokenServices remoteTokenServices;
@Override
public void configure(ResourceServerSecurityConfigurer resources) throws Exception {
// 存入resource_id,到时候与oauthserver中client对应的resourceid进行比对
resources.resourceId(RESOURCE_ID).stateless(true);
resources.tokenServices(remoteTokenServices);
}
}