xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
yaml
spring:
security:
oauth2:
client:
registration:
wechat:
client-id: your-client-id
client-secret: your-client-secret
client-name: WeChat
scope: snsapi_login
redirect-uri: /login/oauth2/code/wechat
provider:
wechat:
authorization-uri: https://open.weixin.qq.com/connect/qrconnect
token-uri: https://api.weixin.qq.com/sns/oauth2/access_token
user-info-uri: https://api.weixin.qq.com/sns/userinfo
user-name-attribute: openid
替换 your-client-id 和 your-client-secret 为您的微信开放平台应用程序的实际值。
@Service
public class WeChatOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 根据 userRequest 获取微信用户信息
// 创建用户对象并返回
}
}
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private WeChatOAuth2UserService weChatOAuth2UserService;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login", "/login/oauth2/code/wechat")
.permitAll()
.anyRequest().authenticated()
.and()
.oauth2Login()
.loginPage("/login")
.userInfoEndpoint()
.userService(weChatOAuth2UserService);
}
}
在上述配置中,我们允许 /login 和 /login/oauth2/code/wechat 路径的所有请求,其他请求需要经过身份验证。使用 oauth2Login() 方法启用 OAuth2 登录,并指定登录页面和自定义的 OAuth2UserService 实现。
html
<html>
<body>
<h1>欢迎登录</h1>
<a href="/login/oauth2/authorization/wechat">微信登录</a>
</body>
</html>
在上述代码中,我们使用 /login/oauth2/authorization/wechat 链接来触发微信登录流程。
完成上述步骤后,您的 Spring Boot 应用程序将支持使用微信进行登录。用户访问登录页面并选择微信登录,将被重定向到微信登录页面进行授权。一旦授权成功,用户将被重定向回您的应用程序,并且您的 WeChatOAuth2UserService 将被调用来加载用户信息。根据需要,您可以将用户信息存储在数据库中或执行其他操作。
我们也可以把jwt信息放入到对象OAuth2User中返回给页面,后边请求可以通过自定义过滤器拦截jwt信息校验。
@Component
public class TokenFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
// 获取请求中的 Token
String token = extractTokenFromRequest((HttpServletRequest) request);
// 校验 Token
if (isValidToken(token)) {
// Token 有效,继续处理请求
chain.doFilter(request, response);
} else {
// Token 无效,返回错误响应
HttpServletResponse httpResponse = (HttpServletResponse) response;
httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
}
}
private String extractTokenFromRequest(HttpServletRequest request) {
// 从请求中提取 Token,例如从请求头或请求参数中获取
// 返回 Token 字符串
}
private boolean isValidToken(String token) {
// 校验 Token 的有效性,例如验证签名、过期时间等
// 返回校验结果
}
}
@Configuration
public class FilterConfig {
@Bean
public FilterRegistrationBean<TokenFilter> tokenFilterRegistration() {
FilterRegistrationBean<TokenFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new TokenFilter());
registration.addUrlPatterns("/api/*"); // 配置过滤的路径
return registration;
}
}
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.addFilterBefore(new TokenFilter(), UsernamePasswordAuthenticationFilter.class);
}
}
在上述代码中,我们创建了一个名为 TokenFilter 的自定义过滤器,并在 doFilter() 方法中实现了自定义的过滤逻辑,包括从请求中提取 Token 和校验 Token 的有效性。然后,在 FilterConfig 配置类中,使用 @WebFilter 注解将自定义过滤器配置为 Spring Bean,并通过 FilterRegistrationBean 注册到过滤器链中。接下来,在 Spring Security 配置类中,使用 HttpSecurity 对象配置安全规则,并使用 .addFilterBefore() 方法将自定义过滤器添加到过滤器链中。
请注意,上述代码仅提供了基本的配置示例,实际使用中可能需要根据具体需求进行调整和扩展。
使用Oauth2做登录的时候,主要涉及到以下两个过滤器:
OAuth2AuthorizationRequestRedirectFilter :重定向过滤器,即当未认证时,重定向到登录页。当我们点击页面上的微信登录的时候,请求会流转到此过滤器。
OAuth2LoginAuthenticationFilter:授权登录过滤器,处理指定的授权登录。当我们微信登录成功后,会回调到我们的服务,此时请求会流转到此过滤器。
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
//构建第三方授权信息请求对象
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestResolver.resolve(request);
if (authorizationRequest != null) {
//发起重定向,到第三方
this.sendRedirectForAuthorization(request, response, authorizationRequest);
return;
}
}
上边是主要逻辑,以我们上边的示例为例,这段逻辑就是从配置文件yml中拿到微信的配置,发起第三方调用。
@Override
public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
//获取registrationId
String registrationId = this.resolveRegistrationId(request);
if (registrationId == null) {
return null;
}
//获取action参数,默认值是: login
String redirectUriAction = getAction(request, "login");
return resolve(request, registrationId, redirectUriAction);
}
private void sendRedirectForAuthorization(HttpServletRequest request, HttpServletResponse response,
OAuth2AuthorizationRequest authorizationRequest) throws IOException {
if (AuthorizationGrantType.AUTHORIZATION_CODE.equals(authorizationRequest.getGrantType())) {
//保存本次请求相关的信息,以用于三方平台回调时可以再次获取,例如当回调时需要检查state参数是否一致,以保证安全;
this.authorizationRequestRepository.saveAuthorizationRequest(authorizationRequest, request, response);
}
//调转到第三方登录页面
this.authorizationRedirectStrategy.sendRedirect(request, response,
authorizationRequest.getAuthorizationRequestUri());
}
@Override
public void sendRedirect(HttpServletRequest request, HttpServletResponse response, String url) throws IOException {
String redirectUrl = calculateRedirectUrl(request.getContextPath(), url);
redirectUrl = response.encodeRedirectURL(redirectUrl);
if (this.logger.isDebugEnabled()) {
this.logger.debug(LogMessage.format("Redirecting to %s", redirectUrl));
}
//重定向到第三方授权登录页面
response.sendRedirect(redirectUrl);
}
可以看到,这个主要是拼接参数,重定向到第三方登录页面,比如微信登录页面。
OAuth2LoginAuthenticationFilter没有重写AbstractAuthenticationProcessingFilter的doFilter方法,我们看抽象类AbstractAuthenticationProcessingFilter的doFilter方法。
private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
if (!requiresAuthentication(request, response)) {
chain.doFilter(request, response);
return;
}
try {
//attemptAuthentication是抽象方法,可被子类重写
Authentication authenticationResult = attemptAuthentication(request, response);
if (authenticationResult == null) {
// return immediately as subclass has indicated that it hasn't completed
return;
}
//成功后会
this.sessionStrategy.onAuthentication(authenticationResult, request, response);
// Authentication success
if (this.continueChainBeforeSuccessfulAuthentication) {
chain.doFilter(request, response);
}
//保存用户信息等
successfulAuthentication(request, response, chain, authenticationResult);
}
catch (InternalAuthenticationServiceException failed) {
this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
unsuccessfulAuthentication(request, response, failed);
}
catch (AuthenticationException ex) {
// Authentication failed
unsuccessfulAuthentication(request, response, ex);
}
}
OAuth2LoginAuthenticationFilter类重写了attemptAuthentication方法。
@Override
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
throws AuthenticationException {
//参数集合
MultiValueMap<String, String> params = org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.toMultiMap(request.getParameterMap());
if (!org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.isAuthorizationResponse(params)) {
OAuth2Error oauth2Error = new OAuth2Error(OAuth2ErrorCodes.INVALID_REQUEST);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
//根据state参数从会话中查询授权登录之前保存的请求对象(请求对象也有state参数),如果找不到则抛出异常:AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE
OAuth2AuthorizationRequest authorizationRequest = this.authorizationRequestRepository
.removeAuthorizationRequest(request, response);
if (authorizationRequest == null) {
OAuth2Error oauth2Error = new OAuth2Error(AUTHORIZATION_REQUEST_NOT_FOUND_ERROR_CODE);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
//获取ClientRegistration信息,配置文件中的第三方配置信息
String registrationId = authorizationRequest.getAttribute(OAuth2ParameterNames.REGISTRATION_ID);
ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
if (clientRegistration == null) {
OAuth2Error oauth2Error = new OAuth2Error(CLIENT_REGISTRATION_NOT_FOUND_ERROR_CODE,
"Client Registration not found with Id: " + registrationId, null);
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
// @formatter:off
String redirectUri = UriComponentsBuilder.fromHttpUrl(UrlUtils.buildFullRequestUrl(request))
.replaceQuery(null)
.build()
.toUriString();
// @formatter:on
OAuth2AuthorizationResponse authorizationResponse = org.springframework.security.oauth2.client.web.OAuth2AuthorizationResponseUtils.convert(params,
redirectUri);
Object authenticationDetails = this.authenticationDetailsSource.buildDetails(request);
//构造认证请求,然后使用工厂模式执行认证,这个和用户名密码认证是一样的
OAuth2LoginAuthenticationToken authenticationRequest = new OAuth2LoginAuthenticationToken(clientRegistration,
new OAuth2AuthorizationExchange(authorizationRequest, authorizationResponse));
authenticationRequest.setDetails(authenticationDetails);
//认证
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this
.getAuthenticationManager().authenticate(authenticationRequest);
OAuth2AuthenticationToken oauth2Authentication = new OAuth2AuthenticationToken(
authenticationResult.getPrincipal(), authenticationResult.getAuthorities(),
authenticationResult.getClientRegistration().getRegistrationId());
oauth2Authentication.setDetails(authenticationDetails);
OAuth2AuthorizedClient authorizedClient = new OAuth2AuthorizedClient(
authenticationResult.getClientRegistration(), oauth2Authentication.getName(),
authenticationResult.getAccessToken(), authenticationResult.getRefreshToken());
this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
return oauth2Authentication;
}
这段逻辑前半部分主要做了配置获取,认证请求的构建,主要逻辑是认证。也就是ProviderManager的authenticate()方法。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
Class<? extends Authentication> toTest = authentication.getClass();
AuthenticationException lastException = null;
AuthenticationException parentException = null;
Authentication result = null;
Authentication parentResult = null;
int currentPosition = 0;
int size = this.providers.size();
//支持多种认证,遍历所有AuthenticationProvider
for (org.springframework.security.authentication.AuthenticationProvider provider : getProviders()) {
//匹配当前的Authentication
if (!provider.supports(toTest)) {
continue;
}
if (logger.isTraceEnabled()) {
logger.trace(LogMessage.format("Authenticating request with %s (%d/%d)",
provider.getClass().getSimpleName(), ++currentPosition, size));
}
try {
//执行匹配到的AuthenticationProvider逻辑
result = provider.authenticate(authentication);
if (result != null) {
copyDetails(authentication, result);
break;
}
}
核心逻辑是provider.authenticate(authentication),我们继续往下看。具体实现是OAuth2LoginAuthenticationProvider的authenticate()方法。
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken loginAuthenticationToken = (org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken) authentication;
// Section 3.1.2.1 Authentication Request -
// https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest scope
// REQUIRED. OpenID Connect requests MUST contain the "openid" scope value.
if (loginAuthenticationToken.getAuthorizationExchange().getAuthorizationRequest().getScopes()
.contains("openid")) {
// This is an OpenID Connect Authentication Request so return null
// and let OidcAuthorizationCodeAuthenticationProvider handle it instead
return null;
}
//构建OAuth2AuthorizationCodeAuthenticationToken对象
org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken authorizationCodeAuthenticationToken;
try {
authorizationCodeAuthenticationToken = (org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken) this.authorizationCodeAuthenticationProvider
.authenticate(new org.springframework.security.oauth2.client.authentication.OAuth2AuthorizationCodeAuthenticationToken(
loginAuthenticationToken.getClientRegistration(),
loginAuthenticationToken.getAuthorizationExchange()));
}
catch (OAuth2AuthorizationException ex) {
OAuth2Error oauth2Error = ex.getError();
throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString());
}
//构建OAuth2User对象,拿到用户信息
OAuth2AccessToken accessToken = authorizationCodeAuthenticationToken.getAccessToken();
Map<String, Object> additionalParameters = authorizationCodeAuthenticationToken.getAdditionalParameters();
//可以自己实现loadUser接口,自定义逻辑
OAuth2User oauth2User = this.userService.loadUser(new OAuth2UserRequest(
loginAuthenticationToken.getClientRegistration(), accessToken, additionalParameters));
Collection<? extends GrantedAuthority> mappedAuthorities = this.authoritiesMapper
.mapAuthorities(oauth2User.getAuthorities());
//构建认证结果
org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken authenticationResult = new org.springframework.security.oauth2.client.authentication.OAuth2LoginAuthenticationToken(
loginAuthenticationToken.getClientRegistration(), loginAuthenticationToken.getAuthorizationExchange(),
oauth2User, mappedAuthorities, accessToken, authorizationCodeAuthenticationToken.getRefreshToken());
authenticationResult.setDetails(loginAuthenticationToken.getDetails());
return authenticationResult;
}
我们在这段逻辑中看到了this.userService.loadUser,OAuth2UserService的方法loadUser()方法我们可以自定义实现。也就是上边代码示例的第三点。
后续逻辑就是认证成功后的通用逻辑,基本上核心源码就是这些。里边还有很多细节点,比如令牌存储与生产,授权令牌与资源的安全配置,自定义认证成功后处理器中完成用户匹配等等。详细逻辑可以自行查看源码。
Spring Security OAuth2 登录的原理如下:
在 Spring Security OAuth2 中,配置文件中定义了客户端信息、授权服务器信息和资源服务器信息。客户端信息包括客户端ID和客户端密钥,用于与授权服务器进行身份验证和授权。授权服务器信息包括授权服务器的URL和令牌端点,用于与授权服务器进行通信。资源服务器信息包括资源服务器的ID和受保护资源的URL,用于限制对受保护资源的访问。
通过配置 Spring Security OAuth2,应用程序可以使用授权服务器进行用户身份验证和授权,并使用访问令牌来访问受保护的资源。