SpringSecurityOAuth核心源码解析

上一篇文章,实现了一个简单的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;
	}

完结

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值