前言
Oauth2登录流程源码分析
前言
上篇主要介绍了Oauth2和spring security 的基本概念。本篇接着讲登录过程。谈到登录必然要考虑的问题:
-
用户信息怎么存,存什么,存在哪,
-
如何保证用户信息的安全,
-
如何解决单点登录问题
-
token盗用问题
-
如何防止短信验证码频繁刷新或被劫取
-
如何支持多种登录方式或多端登录
-
如何处理token过期,刷新问题
-
token在异步场景中如何传递
-
定时任务场景token如何使用
-
如何解决登录服务单点故障问题
-
第三方服务如何接入
1.相关问题解答
1.1 jwt 和token的区别
token: 服务器验证完用户名和密码后会生成一个签名后的token,一般是随机uuid.
jwt: JWT是json web token缩写。它将用户信息加密到token里,服务器不保存任何用户信息。服务器通过使用保存的密钥验证token的正确性,只要正确即通过验证。 安全性比token要高。
1.2 单点登录问题
1.3 token盗用问题。
在jwt的生成过程中我们编码进了用户的真实ip,即使token在使用过程中被劫取,但当网关在鉴权中发现token中的ip和请求的ip不同,也会自己拒绝掉。
1.4 如何防止短信验证码频繁刷新或被劫取
1.5单点故障问题
在整个微服务系统中,网关是所有流量的入口,所有的请求都要经过网关鉴权后再路由到不同的微服务中。在最开始,我们的登录系统也是耦合在平台业务支撑系统中。慢慢的登录功能趋于稳定,但是平台业务支撑部分功能迭代周期长,每次更新必然影响到用户的登录。再加上业务系统频繁访问平台的流量也会同时降低登录操作的效率。为了提高服务的可用性,解决登录单点故障问题,于是将登录系统单独拆分成独立的部分,这样一方面随着用户量的增加方便随时扩容,另一方面与业务分离开开来,大大提高了系统的可用性。
1.6如何支持多端登录
2.登录功能的扩展和实现
2.1 资源服务器配置
@Configuration
@EnableResourceServer
protected class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http
.csrf()
.disable()
// 开启跨域
.cors().and()
.exceptionHandling()
.authenticationEntryPoint(customerAuthenticationEntryPoint())
.accessDeniedHandler(customerOAuth2AccessDeniedHandler())
.and()
.authorizeRequests()
.antMatchers(
"/oauth/**",
...
)
.permitAll()
.anyRequest()
.authenticated();
}
2.2 认证服务器配置
@Configuration
@EnableAuthorizationServer
protected class AuthorizationServerConfiguration extends AuthorizationServerConfigurerAdapter {
@Override
public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
endpoints
//❶token存储
.tokenStore(tokenStore())
//❷异常翻译器
.exceptionTranslator(webResponseExceptionTranslator)
//❸token转化器
.accessTokenConverter(accessTokenConverter())
//❹自省处理器
.userDetailsService(customerUserDetailService)
//❺认证管理器
.authenticationManager(authenticationManager);
}
接下来依次分析。
2.2.1 token存储
@Bean
public TokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
2.2.2 异常翻译器
定制自己的异常转化器。
2.2.3 AccessToken转化器
@Bean
public JwtAccessTokenConverter accessTokenConverter() {
...
return accessTokenConverter;
}
2.2.4 自省处理器
public class CustomUserDetailsService implements UserDetailsService {
public UserDetails loadUserByUsername(String s){
xxx
}
}
2.2.5 认证管理器
AuthenticationManager,使用框架自身的。
2.2.6 客户端认证
public class JdbcClientDetailsServiceEnhancer extends JdbcClientDetailsService {
@Override
public ClientDetails loadClientByClientId(String clientId) throws ClientRegistrationException {
...
}
}
2.2.7 token状态机维护
public class CustomerTokenGranterAdapter implements TokenGranter {
private TokenGranter tokenGranter;
private TokenStatusProperties tokenStatusProperties;
public CustomerTokenGranterAdapter(TokenGranter tokenGranter,
TokenStatusProperties tokenStatusProperties){
this.tokenGranter = tokenGranter;
this.tokenStatusProperties=tokenStatusProperties;
}
使用装饰器模式,将oauth2自带的tokenStore进行包装。
@Configuration
class TokenEndPointConfiguration implements ApplicationContextAware, InitializingBean {
@Override
public void afterPropertiesSet() throws Exception {
AuthorizationServerEndpointsConfiguration configuration =
applicationContext.getBean(AuthorizationServerEndpointsConfiguration.class);
TokenGranter tokenGranter = new CustomerTokenGranterAdapter(
configuration.getEndpointsConfigurer().getTokenGranter(),tokenStatusProperties);
TokenEndpoint tokenEndpoint = applicationContext.getBean(TokenEndpoint.class);
tokenEndpoint.setTokenGranter(tokenGranter);
}
3.Oauth2登录流程源码分析
public class TokenEndpoint extends AbstractEndpoint {
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
//从请求中获取clientId
String clientId = getClientId(principal);
//❶根据clientId获取clientSecret
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
//❷组装认证请求。
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
//❸获取tokenGrant开始授权操作
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
3.1 客户端认证
TokenRequest tokenRequest = new TokenRequest(requestParameters, clientId, scopes, grantType);
getTokenGranter()就是我们前面自定义的CustomerTokenGranterAdapter,然后里面注入的是Spring security自带的tokenGranter.
private List<TokenGranter> getDefaultTokenGranters() {
ClientDetailsService clientDetails = clientDetailsService();
AuthorizationServerTokenServices tokenServices = tokenServices();
AuthorizationCodeServices authorizationCodeServices = authorizationCodeServices();
OAuth2RequestFactory requestFactory = requestFactory();
List<TokenGranter> tokenGranters = new ArrayList<TokenGranter>();
//❶授权码模式
tokenGranters.add(new AuthorizationCodeTokenGranter(tokenServices, authorizationCodeServices, clientDetails,
requestFactory));
//❷token刷新
tokenGranters.add(new RefreshTokenGranter(tokenServices, clientDetails, requestFactory));
//❸隐式模式
ImplicitTokenGranter implicit = new ImplicitTokenGranter(tokenServices, clientDetails, requestFactory);
tokenGranters.add(implicit);
//❹客户端模式
tokenGranters.add(new ClientCredentialsTokenGranter(tokenServices, clientDetails, requestFactory));
//❺密码模式
if (authenticationManager != null) {
tokenGranters.add(new ResourceOwnerPasswordTokenGranter(authenticationManager, tokenServices,
clientDetails, requestFactory));
}
return tokenGranters;
}
遍历每个tokenGranter,调用grant方法。grant方法会调用到抽象类AbstractTokenGranter的实现。
public abstract class AbstractTokenGranter implements TokenGranter {
public OAuth2AccessToken grant(String grantType, TokenRequest tokenRequest) {
//❶ 判断grantType是否匹配
if (!this.grantType.equals(grantType)) {
return null;
}
String clientId = tokenRequest.getClientId();
//❷验证客户端
validateGrantType(grantType, client);
ClientDetails client = clientDetailsService.loadClientByClientId(clientId);
//❸获取token
return getAccessToken(client, tokenRequest);
}
❶ 每个grantType对应一个tokenGranter。
eg.
❷validateGrantType 验证客户端的支持的类型。
这一块关于tokenGranter的设计就融入了组合模式,代理模式,和抽象化的思想。我们在碰到类似处理场景可以借鉴,可以优化if else代码。
3.2 认证获取accessToken
public abstract class AbstractTokenGranter implements TokenGranter {
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
3.2.1 组装认证对象
基于不同的许可类型,认证对象也会不同,同样getOAuth2Authentication也会会子类重写。
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");
// Protect from downstream leaks of password
parameters.remove("password");
Authentication userAuth = new UsernamePasswordAuthenticationToken(username, password);
((AbstractAuthenticationToken) userAuth).setDetails(parameters);
try {
userAuth = authenticationManager.authenticate(userAuth);
}
OAuth2Request storedOAuth2Request = getRequestFactory().createOAuth2Request(client, tokenRequest);
return new OAuth2Authentication(storedOAuth2Request, userAuth);
}
从请求参数中或者用户名和密码后,开始认证,调用的则是核心类:认证管理器AuthenticationManager。(可以参考上一篇关于核心类的介绍)
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
for (AuthenticationProvider provider : getProviders()) {
if (!provider.supports(toTest)) {
continue;
}
try {
result = provider.authenticate(authentication);
...
}
}
}
public boolean supports(Class<?> authentication) {
return (UsernamePasswordAuthenticationToken.class
.isAssignableFrom(authentication));
}
如RememberMe的token类型是RememberMeAuthenticationToken
这里我们注入的是自定义provider: CustomerAuthenticationProvider.
我们继续看AuthenticationProvider的authenticate方法。这里调用的还是抽象类AbstractUserDetailsAuthenticationProvider的方法。
public Authentication authenticate(Authentication authentication)
throws AuthenticationException {
// Determine username
String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED": authentication.getName();
try {
//❶ 自省处理,根据用户名获取用户认证信息
user = retrieveUser(username,(UsernamePasswordAuthenticationToken) authentication);
}
}
try {
//❷前置检查,比如用户是否被锁等
preAuthenticationChecks.check(user);
//❸额外检查
additionalAuthenticationChecks(user,(UsernamePasswordAuthenticationToken) authentication);
}
postAuthenticationChecks.check(user);
//❹创建认证对象
return createSuccessAuthentication(principalToReturn, authentication, user);
}
❶retrieveUser 获取用户信息,这是springSecurity最核心的方法之一了。用户可以实现自己的自省方式,去定义用户信息的来源。如db,redis等。
❷preAuthenticationChecks.check 前置检查,比如检查用户是否被锁,是否启用等。
❸additionalAuthenticationChecks 额外检查,比如不同的登录方式其他信息的校验,短信验证码,签名证书,指纹信息等。
❹创建Authentication 认证对象
前面一篇也介绍过和userDetail(自省返回的数据)的区别
现在万事具备,只等着生成token了。
3.2.2 创建accessToken
public abstract class AbstractTokenGranter implements TokenGranter {
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
这里的tokenservice 默认实现是DefaultTokenServices
public class DefaultTokenServices implements AuthorizationServerTokenServices, ResourceServerTokenServices,
ConsumerTokenServices, InitializingBean {
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
//❶创建accessToken
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
//❷存储accessToken
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
这里我们没有使用refreshToken机制,token的过期是通过redis控制的,后面再介绍。
❶createAccessToken
private OAuth2AccessToken createAccessToken(OAuth2Authentication authentication, OAuth2RefreshToken refreshToken) {
DefaultOAuth2AccessToken token = new DefaultOAuth2AccessToken(UUID.randomUUID().toString());
int validitySeconds = getAccessTokenValiditySeconds(authentication.getOAuth2Request());
if (validitySeconds > 0) {
token.setExpiration(new Date(System.currentTimeMillis() + (validitySeconds * 1000L)));
}
token.setRefreshToken(refreshToken);
token.setScope(authentication.getOAuth2Request().getScope());
return accessTokenEnhancer != null ? accessTokenEnhancer.enhance(token, authentication) : token;
}
在前面的配置扩展中讲到,我们使用的是jwtToken,我们注入了jwt的accessTokenEnhancer:JwtAccessTokenConverter
public class JwtAccessTokenConverter implements TokenEnhancer, AccessTokenConverter, InitializingBean {
public OAuth2AccessToken enhance(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
DefaultOAuth2AccessToken result = new DefaultOAuth2AccessToken(accessToken);
Map<String, Object> info = new LinkedHashMap<String, Object>(accessToken.getAdditionalInformation());
String tokenId = result.getValue();
if (!info.containsKey(TOKEN_ID)) {
info.put(TOKEN_ID, tokenId);
}
else {
tokenId = (String) info.get(TOKEN_ID);
}
result.setAdditionalInformation(info);
result.setValue(encode(result, authentication));
OAuth2RefreshToken refreshToken = result.getRefreshToken();
if (refreshToken != null) {
DefaultOAuth2AccessToken encodedRefreshToken = new DefaultOAuth2AccessToken(accessToken);
encodedRefreshToken.setValue(refreshToken.getValue());
// Refresh tokens do not expire unless explicitly of the right type
encodedRefreshToken.setExpiration(null);
try {
Map<String, Object> claims = objectMapper
.parseMap(JwtHelper.decode(refreshToken.getValue()).getClaims());
if (claims.containsKey(TOKEN_ID)) {
encodedRefreshToken.setValue(claims.get(TOKEN_ID).toString());
}
}
catch (IllegalArgumentException e) {
}
Map<String, Object> refreshTokenInfo = new LinkedHashMap<String, Object>(
accessToken.getAdditionalInformation());
refreshTokenInfo.put(TOKEN_ID, encodedRefreshToken.getValue());
refreshTokenInfo.put(ACCESS_TOKEN_ID, tokenId);
encodedRefreshToken.setAdditionalInformation(refreshTokenInfo);
DefaultOAuth2RefreshToken token = new DefaultOAuth2RefreshToken(
encode(encodedRefreshToken, authentication));
if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
Date expiration = ((ExpiringOAuth2RefreshToken) refreshToken).getExpiration();
encodedRefreshToken.setExpiration(expiration);
token = new DefaultExpiringOAuth2RefreshToken(encode(encodedRefreshToken, authentication), expiration);
}
result.setRefreshToken(token);
}
return result;
}
protected String encode(OAuth2AccessToken accessToken, OAuth2Authentication authentication) {
String content;
try {
content = objectMapper.formatMap(tokenConverter.convertAccessToken(accessToken, authentication));
}
catch (Exception e) {
throw new IllegalStateException("Cannot convert access token to JSON", e);
}
String token = JwtHelper.encode(content, signer).getEncoded();
return token;
}
public class CustomerUserAuthenticationConverter extends DefaultUserAuthenticationConverter{
@Override
public Map<String, ?> convertUserAuthentication(Authentication authentication) {
Map<String, Object> response = new LinkedHashMap<String, Object>();
//编码用户信息
return response;
}
@Override
public Authentication extractAuthentication(Map<String, ?> map) {
...
//抽取用户信息
}
相应的还有一个方法extractAuthentication ,用来抽取用户编码进token内的信息。主要用在认证过程中。
自定义的用户信息编码到jwt payload后,然后通过JwtHelper.encode(content, signer).getEncoded(),生成了携带用户信息的jwt token。
3.3 token状态维护
前面的一步主要是通过tokenGranter生成了accessToken
public abstract class AbstractTokenGranter implements TokenGranter {
protected OAuth2AccessToken getAccessToken(ClientDetails client, TokenRequest tokenRequest) {
return tokenServices.createAccessToken(getOAuth2Authentication(client, tokenRequest));
}
在讲第二步关于token过期时间和refreshToken时,我们没有用到默认的token状态维护,而是采用了redis去维护token.更加简单透明的维护token的状态。
然后整个生成token到tokenGranter这一步也就走完了,如何去切入呢?
public class TokenEndpoint extends AbstractEndpoint {
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
//从请求中获取clientId
String clientId = getClientId(principal);
//❶根据clientId获取clientSecret
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
//❷组装认证请求。
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
//❸获取tokenGrant开始授权操作
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
也就是说我们切入tokenGranter,而且还要保证默认的granter正常执行,相当于做了后置增强操作。如果你对Spring源码比较了解,那你可能就会联想到InitializingBean,在bean的生命周期中会起到增强的效果。
于是,扩展就很容易实现了。
@Configuration
class TokenEndPointConfiguration implements ApplicationContextAware, InitializingBean {
private ApplicationContext applicationContext;
@Autowired
private LoginProperties loginProperties;
@Autowired
private TokenStatusProperties tokenStatusProperties;
@Override
public void afterPropertiesSet() throws Exception {
//❶获取认证服务端点配置类
AuthorizationServerEndpointsConfiguration configuration =
applicationContext.getBean(AuthorizationServerEndpointsConfiguration.class);
//❷用自定义的tokerGranter包装默认实现
TokenGranter tokenGranter = new CustomerTokenGranterAdapter(
configuration.getEndpointsConfigurer().getTokenGranter(),tokenStatusProperties);
TokenEndpoint tokenEndpoint = applicationContext.getBean(TokenEndpoint.class);
//❸更换tokenGranter用自定义的tokenGranter
tokenEndpoint.setTokenGranter(tokenGranter);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.applicationContext = applicationContext;
}
}
❶获取认证服务端点配置类,SpringSecurity的核心配置都可以通过配置类来生成。
❷用自定义的tokerGranter包装默认实现
这里采用了装饰模式。
❸用自定义的tokenGranter更换tokenGranter
这样就用自定义的tokenGranter来增强原始的类了。
4.Token相关问题
4.1 多端多设备登录问题
4.1.1 key的设计
为了区分不同端的设备,我们需要给用户的请求header上加上设备标识。在传递过程中不能丢失。同时在生成token后,存入redis时,要带上标识。
这个时候关于token redis key的设计便是:(ps:和真实有做区分,不要想着去攻击我们系统了。。。。)
登陆后,首先会往redis中存入该token。同时用set 集合将相同用户的token放入redis set 集合中。
对应的token set key为 :
4.1.2 互斥操作
revoke key :
流程图如下:
4.2 token盗用问题

其他关于token鉴权问题,将在下篇网关鉴权再分析。

原文始发于微信公众号(小李的源码图):某安某大型国企项目Oauth2实践(二)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由半码博客整理,本文链接:https://www.bmabk.com/index.php/post/145316.html