spring security 整合OAUTH
引言
<1>使用SpringSecurity整合OAuth2之前,应该对OAuth2有个基本了解,可以参考阮一峰的日志(http://www.ruanyifeng.com/blog/2019/04/oauth_design.html)
<2>至少掌握以下名词的概念:
资源拥有者: 一般用户
服务提供商: 比如QQ,微信这种支持第三方程序获取资源的服务商
第三方应用程序: 可以理解为需要借用服务提供商资源的外部程序
认证服务器: 负责认证,颁发令牌(服务商提供)
资源服务器: 确定拥有权限后即可访问资源(服务商提供)
<3>将上面的名词概念对应到自己开发的系统中:
需要开发:
1.认证服务器(认证/鉴权中心)
2.资源服务器(用户数据)
3.第三方应用程序(我们的前端程序:H5,APP,PC端C#...)
流程概要:
(密码模式)
用户使用第三方客户端 -> 认证服务器 -> 颁发token
-> 携带token至资源服务器 -> 资源服务将token请求到认证中心鉴定权限 -> 权限通过则资源服务器放开资源给资源拥有者
项目版本
<!--SpringBoot2.1.14-->
<pom>
<artifactId>spring-boot-starter-security</artifactId>
<artifactId>spring-boot-starter-web</artifactId>
<artifactId>spring-security-oauth2:2.2.5</artifactId>
</pom>
大致流程(密码模式)
-> ClientCredentialsTokenEndpointFilter | BasicAuthenticationFilter //(client过滤器)
-> /oauth/token //获取令牌的请求
-> TokenEndPoint //授权端点
-> ClientDetailsService.loadClientByClientId() //查找客户端详情
-> ClientDetails //客户端详情
-> TokenRequest //原始令牌请求
-> TokenGranter(CompositeTokenGranter) //令牌颁发者对象
-> ResourceOwnerPasswordTokenGranter //具体的颁发者
-> OAuth2Request + Authentication //客户端请求+用户认证信息
-> AuthorizationServerTokenServices //授权服务器授权服务
-> OAuth2Authentication //最终的OAuth2令牌
源码阅读
@EnableResourceServer @EnableAuthorizationServer
1.ClientCredentialsTokenEndpointFilter(废弃了)
ClientCredentialsTokenEndpointFilter实现了AbstractAuthenticationProcessingFilter,类似UsernamePasswordAuthenticationFilter 拦截路径
public ClientCredentialsTokenEndpointFilter() {
this("/oauth/token");
}
关键方法
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" });
}
//获取clientId
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;
}
// 适配成了UsernamePasswordAuthenticationToken
UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(clientId,
clientSecret);
return this.getAuthenticationManager().authenticate(authRequest);
}
好像是使用ClientCredentialsTokenEndpointFilter获取了客户端凭证 但是查看父类的
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) req;
HttpServletResponse response = (HttpServletResponse) res;
//ClientCredentialsTokenEndpointFilter的match
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
Authentication authResult;
try {
authResult = attemptAuthentication(request, response);
}
}
}
ClientCredentialsRequestMatcher是其路由匹配
protected static class ClientCredentialsRequestMatcher implements RequestMatcher {
// ...
public ClientCredentialsRequestMatcher(String path) {
this.path = path;
}
@Override
public boolean matches(HttpServletRequest request) {
String uri = request.getRequestURI();
int pathParamIndex = uri.indexOf(';');
if (pathParamIndex > 0)
// strip everything after the first semi-colon
uri = uri.substring(0, pathParamIndex);
}
String clientId = request.getParameter("client_id");
if (clientId == null) {
// !!!
// 这里给了 Basic Auth 一个机会去认代替其工作
// Give basic auth a chance to work instead (it's preferred anyway)
return false;
}
}
}
2.BasicAuthenticationFilter
BasicAuthenticationFilter是OncePerRequestFilter子类
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain chain) {
//获取Header中的Authorization信息
String header = request.getHeader("Authorization");
//只过滤Basic认证
if (header == null || !header.toLowerCase().startsWith("basic ")) {
chain.doFilter(request, response);
return;
}
try {
//解码base64编码的header
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));
// 认证Auth
Authentication authResult = this.authenticationManager
.authenticate(authRequest);
if (debug) {
this.logger.debug("Authentication success: " + authResult);
}
// 到了这一步也就是吧client的信息封装成了UsernamePasswordAuthenticationToken翻到了上下文中
SecurityContextHolder.getContext().setAuthentication(authResult);
}
return;
}
//继续调用过滤器,直到目标接口
chain.doFilter(request, response);
}
2.TokenEndpoint
终于进入这个接口了
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
//这里传入的principal就是上面过滤器设置的那个UsernamePasswordAuthenticationToken
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
//获取ClientID
String clientId = getClientId(principal);
//获取ClientDetails
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
///生成TokenRequest
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
// ...省略代码
// 根据tokenRequest和oauth2模式类型 -> 颁发token(重要)
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
// 返回 AT的JSON
return getResponse(token);
}
3.CompositeTokenGranter
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
for (TokenGranter granter : tokenGranters) {
// 查找适合的令牌颁发者
OAuth2AccessToken grant = granter.grant(grantType, tokenRequest);
if (grant!=null) {
return grant;
}
}
return null;
}
4.ResourceOwnerPasswordTokenGranter
①
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
//查找客户凭证
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
//获取AT
return getAccessToken(client, tokenRequest);
}
② 获取 OAuth2AccessToken
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
// DefaultTokenServices
// 重要
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
③ 获取OAuth2Authentication
@Override
protected OAuth2Authentication getOAuth2Authentication(ClientDetails client, TokenRequest tokenRequest) {
Map<String, String> parameters = new LinkedHashMap<String, String>(tokenRequest.getRequestParameters());
// 获取用户名
String username = parameters.get("username");
// 获取密码
String password = parameters.get("password");
// 生成AT
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
// 认证这个用户信息
userAuth = authenticationManager.authenticate(userAuth);
}
// createOAuth2Request
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
// OAuth2Request + UsernamePasswordAuthenticationToken
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
④ 获取OAuth2Request
public OAuth2Request createOAuth2Request(ClientDetails client) {
Map<String, String> requestParameters = getRequestParameters();
HashMap<String, String> modifiable = new HashMap<String, String>(requestParameters);
// Remove password if present to prevent leaks
modifiable.remove("password");
modifiable.remove("client_secret");
// Add grant type so it can be retrieved from OAuth2Request
modifiable.put("grant_type", grantType);
// 返回一个OAuth2Request
return new OAuth2Request(modifiable, client.getClientId(), client.getAuthorities(), true, this.getScope(),
client.getResourceIds(), null, null, null);
}
...
⑤ Create AccessToken
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 查看这个token是否已经存在
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 如果存在则
if (existingAccessToken != null) {
//是否过期
if (existingAccessToken.isExpired()) {
//如果过期了,查看原来的刷新token是不是空的
if (existingAccessToken.getRefreshToken() != null) {
//如果原刷新token不是空的,则获取
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
// 移除旧refreshToken
tokenStore.removeRefreshToken(refreshToken);
}
// 移除过期的token
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
// 如果没过期重新存一次 免得有更改了
tokenStore.storeAccessToken(existingAccessToken, authentication);
// 返回就行了
return existingAccessToken;
}
}
// 如果不存在token
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has
// expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
//! create AT
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}