上一篇文章,实现了一个简单的SpringSecurityOAuth 应用,这一章,学习一下SpringSecurityOAuth的核心源码。
如上图所示
首先进入TokenEndpoint,来看一下源码
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException("There is no client authentication. Try adding an appropriate authentication filter.");
} else {
String clientId = this.getClientId(principal);
//具体调用InMemoryClientDetailsService获取第三方应用的信息封装到ClientDetails中
ClientDetails authenticatedClient = this.getClientDetailsService().loadClientByClientId(clientId);
//parameters包含请求的参数:授权类型、授权码、client-id、scope等
TokenRequest tokenRequest = this.getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("") && !clientId.equals(tokenRequest.getClientId())) {
throw new InvalidClientException("Given client ID does not match authenticated client");
} else {
if (authenticatedClient != null) {
this.oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
} else if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
} else {
if (this.isAuthCodeRequest(parameters) && !tokenRequest.getScope().isEmpty()) {
//清空请求中的scope
this.logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.emptySet());
}
if (this.isRefreshTokenRequest(parameters)) {
tokenRequest.setScope(OAuth2Utils.parseParameterList((String)parameters.get("scope")));
}
//生成OAuth2AccessToken
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
} else {
return this.getResponse(token);
}
}
}
}
}
- ClientDetails封装了第三方应用的信息,包括clientId、clientSecret 以及Scope等信息
- TokenEndPoint创建的TokenRequest封装了请求中的一些其它信息,并把ClientDetails也会放入其中
接着调用
OAuth2AccessToken token = this.getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
默认的会调用CompositeTokenGranter,跟一下代码
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
Iterator var3 = this.tokenGranters.iterator();
OAuth2AccessToken grant;
do {
if (!var3.hasNext()) {
return null;
}
//这里会根据授权类性调用不同的granter,我么这里是授权码模式,所以调用 AuthorizationCodeTokenGranter
TokenGranter granter = (TokenGranter)var3.next();
grant = granter.grant(grantType, tokenRequest);
} while(grant == null);
return grant;
}
接着看
grant = granter.grant(grantType, tokenRequest);
AuthorizationCodeTokenGranter集成了AbstractTokenGranter,所以会接着调用AbstractTokenGranter的grant方法,看一下源码
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
if (!this.grantType.equals(grantType)) {
return null;
} else {
String clientId = tokenRequest.getClientId();
//这里会调用ClientDetailsService的实现,去获取ClientDetails,类似于UserDetailsService
ClientDetails client = this.clientDetail:sService.loadClientByClientId(clientId);
this.validateGrantType(grantType, client);
this.logger.debug("Getting access token for: " + clientId);
return this.getAccessToken(client, tokenRequest);
}
}
接着调用 this.getAccessToken(client, tokenRequest);
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
//this.getOAuth2Authentication(client, tokenRequest)会构件出OAuth2Authentication对象
return this.tokenServices.createAccessToken(this.getOAuth2Authentication(client, tokenRequest));
}
默认调用DefaultTokenServices,源码如下
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
//1. 从token的存储位置,去取token
OAuth2AccessToken existingAccessToken = this.tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
// 判断token是否过期
if (!existingAccessToken.isExpired()) {
this.tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
this.tokenStore.removeRefreshToken(refreshToken);
}
this.tokenStore.removeAccessToken(existingAccessToken);
}
if (refreshToken == null) {
refreshToken = this.createRefreshToken(authentication);
} else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken)refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = this.createRefreshToken(authentication);
}
}
// 2.生成令牌
OAuth2AccessToken accessToken = this.createAccessToken(authentication, refreshToken);
//存储
this.tokenStore.storeAccessToken(accessToken, authentication);
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
this.tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
这里说明两点
1.从token的存储位置,去取token,这里token的存储方式有以及几种
- JdbcTokenStore
- RedisTokenStore
- JwtTokenStore
- InMemoryTokenStore
- JwtTokenStore
比较常用得的是 RedisTokenStore 和 JwtTokenStore
2.生成令牌
令牌生成时,可以通过实现TokenEnhancer ,从而对生成的Token进行加强。
重构用户名密码登录
通过上面对SpringSecurityOAuth核心源码解析后,我们重构一下用户名密码登录,使其支持token的访问方式。
我们在登录成功的处理器里面去生成token,代码如下
@Component("imoocAuthenticationSuccessHandler")
public class ImoocAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
private Logger logger = LoggerFactory.getLogger(getClass());
@Autowired
private ObjectMapper objectMapper;
@Autowired
private ClientDetailsService clientDetailsService;
@Autowired
private AuthorizationServerTokenServices authorizationServerTokenServices;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
logger.info("登录成功");
String header = request.getHeader("Authorization");
if (header == null || !header.startsWith("Basic ")) {
throw new UnapprovedClientAuthenticationException("请求头中无client信息");
}
//从请求头中截取tokens
String[] tokens = extractAndDecodeHeader(header, request);
assert tokens.length =
String clientId = tokens[0];
String clientSecret = tokens[1];
ClientDetails clientDetails = clientDetailsService.loadClientByClientId(clientId);
if (clientDetails == null) {
throw new UnapprovedClientAuthenticationException("clientId对应的配置信息不存在:" + clientId);
} else if (!StringUtils.equals(clientDetails.getClientSecret(), clientSecret)) {
throw new UnapprovedClientAuthenticationException("clientSecret不匹配:" + clientId);
}
TokenRequest tokenRequest = new TokenRequest(MapUtils.EMPTY_MAP, clientId, clientDetails.getScope(), "custom");//自定义模式
OAuth2Request oAuth2Request = tokenRequest.createOAuth2Request(clientDetails);
OAuth2Authentication oAuth2Authentication = new OAuth2Authentication(oAuth2Request, authentication);
OAuth2AccessToken token = authorizationServerTokenServices.createAccessToken(oAuth2Authentication);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write(objectMapper.writeValueAsString(token));
}
private String[] extractAndDecodeHeader(String header, HttpServletRequest request) throws IOException {
byte[] base64Token = header.substring(6).getBytes("UTF-8");
byte[] decoded;
try {
decoded = Base64.decode(base64Token);
} catch (IllegalArgumentException e) {
throw new BadCredentialsException("Failed to decode basic authentication token");
}
String token = new String(decoded, "UTF-8");
int delim = token.indexOf(":");
if (delim == -1) {
throw new BadCredentialsException("Invalid basic authentication token");
}
return new String[] { token.substring(0, delim), token.substring(delim + 1) };
}
}
我们用postman发起等请求,请求头里需要带上clientId 和 clientSecret,成功获取token。
带着token就可以去回去资源了
重构短信登录
重构后把验证存储在redis中,通过请求时传入的 deviceId 构建 key
重构社交登录
之前的通过SpringSoial实现的第三方登录,登录成功过中不会获取我们自己的client 端生成的令牌
授权码模式
在授权码码模式下,我们只需要把社交登录成功后的成功处理器改为app环境下的成功处理器就可以。
在SocialConfig中添加配置
@Autowired(required = false)
private SocialAuthenticationFilterPostProcessor socialAuthenticationFilterPostProcessor;
@Bean
public SpringSocialConfigurer imoocSocialSecurityConfig() {
String filterProcessesUrl = securityProperties.getSocial().getFilterProcessesUrl(); // 过滤的的url,默认为auth
ImoocSpringSocialConfigurer configurer = new ImoocSpringSocialConfigurer(filterProcessesUrl);
configurer.signupUrl(securityProperties.getBrowser().getSignUpUrl()); //注册跳转的url
//配置不同的社交登录后处理器
configurer.setSocialAuthenticationFilterPostProcessor(socialAuthenticationFilterPostProcessor);
return configurer;
}
在app环境下实现 SocialAuthenticationFilterPostProcessor
public class AppSocialAuthenticationFilterPostProcessor implements SocialAuthenticationFilterPostProcessor {
@Autowired
private AuthenticationSuccessHandler imoocAuthenticationSuccessHandler;
//设置成返回令牌的imoocAuthenticationSuccessHandler
@Override
public void process(SocialAuthenticationFilter socialAuthenticationFilter) {
socialAuthenticationFilter.setAuthenticationSuccessHandler(imoocAuthenticationSuccessHandler);
}
}
简化模式
在简化模式下,第三方应用授权之后,直接在重定向地址在携带了openId和acessToken,我们需要做的是,再拿到openId后,去换Client的令牌。简而言之就是用OpenId登录,原理和用短信验证码一样。
重构注册逻辑
之前注册时获取第三方用户的信息,是使用后spring social默认的工具类ProviderSignInUtils,用户信息是保存至session中,重构之后把用户信息存在redis中。
**思路:**再SocialConfig中设置注册跳转的url,当在app环境下,讲会跳转到/social/user,这里会调用我么自己写的工具类,把用户信息保存至redis中,并返回401和用户信息给前端,前端这是需要引导用户跳转到用户注册页面去。
令牌配置ee
思路:token令牌存储可以选择设置为redis或 jwt,默认配置为jwt。当为jwt时,添加TokenEnhancer,可以对token进行增加。
配置RedisTokenStore 和 JwtTokenStore
@Configuration
public class TokenStoreConfig {
/**
* 使用redis存储token的配置,只有在imooc.security.oauth2.tokenStore配置为redis时生效
* @author zhailiang
*
*/
@Configuration
@ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "tokenStore", havingValue = "redis")
public static class RedisConfig {
@Resource
private RedisConnectionFactory redisConnectionFactory;
/**
* @return
*/
@Bean
public TokenStore redisTokenStore() {
return new RedisTokenStore(redisConnectionFactory);
}
}
/**
* 使用jwt时的配置,默认生效
*
* @author zhailiang
*
*/
@Configuration
@ConditionalOnProperty(prefix = "imooc.security.oauth2", name = "tokenStore", havingValue = "jwt", matchIfMissing = true)
public static class JwtConfig {
@Resource
private SecurityProperties securityProperties;
/**
* @return
*/
@Bean
public TokenStore jwtTokenStore() {
return new JwtTokenStore(jwtAccessTokenConverter());
}
/**
* 添加密签,防止token被篡改
* @return
*/
@Bean
public JwtAccessTokenConverter jwtAccessTokenConverter(){
JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
converter.setSigningKey(securityProperties.getOauth2().getJwtSigningKey());
return converter;
}
/**
* @return
*/
@Bean
@ConditionalOnMissingBean(TokenEnhancer.class)
public TokenEnhancer jwtTokenEnhancer(){
return new TokenJwtEnhancer();
}
}
}
增强token
/**
* JWT 扩展
*/
public class TokenJwtEnhancer implements TokenEnhancer {
@Override
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
Map<String, Object> info = new HashMap<>();
info.put("company", "imooc");
((DefaultOAuth2AccessToken)accessToken).setAdditionalInformation(info);
return accessToken;
}
}
默认Authentication中封装的信息时不会解析TokenJwtEnhance 添加的字段,需要借助jjwt
添加依赖
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.7.0</version>
</dependency>
解析
@GetMapping("/me")
public Object getCurrentUser(Authentication user, HttpServletRequest request) throws ExpiredJwtException, UnsupportedJwtException, MalformedJwtException, SignatureException, IllegalArgumentException, UnsupportedEncodingException {
//解析TokenJwtEnhancer添加的东西
String token = StringUtils.substringAfter(request.getHeader("Authorization"), "bearer ");
Claims claims = Jwts.parser().setSigningKey(securityProperties.getOauth2().getJwtSigningKey().getBytes("UTF-8"))
.parseClaimsJws(token).getBody();
String company = (String) claims.get("company");
log.info(company);
return user;
}