Skip to content

Spring Security OAuth2 - 10 前后端分离案例

本文主要介绍如何在Spring Security中使用OAuth 2.0进行登录之前后端分离,是Spring Security系列第十篇文章,前九篇文章如下:

1. 前端界面

前端项目运行在http://127.0.0.1:5500/oauth地址(使用VS Code中的Live Sever插件),前端界面分为三个:

1.1 主页-main.html

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>主页</title>
</head>

<body>
    <h1>主页</h1>

    <div id="user"></div>

    <script>
        let userString = localStorage.getItem('user');
        if (userString) {
            // 用户已登录,显示用户信息
            document.getElementById('user').innerHTML = `欢迎您,${userString}`;
        } else {
            // 用户没登录,跳转到登录页

            // 为了效果明显,显示文字后跳转到登录界面
            document.getElementById('user').innerHTML = '请先登录,3秒后跳转到登录页...';

            setTimeout(() => {
                window.location.href = '/oauth/login.html'
            }, 3000);
        }
    </script>

</body>

</html>

1.2 登录界面-login.html

html
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>登录</title>

    <style>
        body {
            display: flex;
            justify-content: center;
            align-items: center;
            height: 100vh;
            margin: 0;
        }

        a {
            padding: 10px 20px;
            background-color: #007bff;
            color: white;
            text-decoration: none;
            border-radius: 5px;
        }
    </style>
</head>

<body>
    <div>
        <a href='http://127.0.0.1:8080/oauth2/authorization/github'>使用GitHub登录</a>
    </div>
</body>

</html>

1.3 回调界面-callback.html

java
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>跳转中...</title>
</head>

<body>
    <h1>跳转中...</h1>

    <script>
        // 从URL中提取code参数
        const urlParams = new URLSearchParams(window.location.search);
        const code = urlParams.get('code');
        const state = urlParams.get('state');

        // 发送POST请求到后台服务器
        if (code) {
            try {

                let url = 'http://127.0.0.1:8080/login/oauth2/code/github?' +
                    'code=' + encodeURIComponent(code) +
                    '&state=' + encodeURIComponent(state);
                fetch(url, {
                    method: 'GET',
                    headers: {
                        'Content-Type': 'application/json;charset=UTF-8',
                    }
                })
                    .then(response => {
                        return response.json();
                    })
                    .then(data => {
                        // 登录成功,保存用户信息, 跳转到主页
                        localStorage.setItem('user', JSON.stringify(data));
                        window.location.href = '/oauth/main.html';
                    })
                    .catch(error => console.error('Error:', error));
            } catch (error) {
                console.error('Error:', error);
            }

        }
        else {
            console.error('No code found in URL');
        }
    </script>
</body>

</html>

2. Github设置调整

由于我们更改了回调地址,所以需要在GitHub的OAuth设置中调整:

image-20250119222452853

3. 后台服务调整

由于更改了Spring Security OAuth默认的回调地址,所以需要在配置文件中指明更改后的回调地址:

properties
spring.security.oauth2.client.registration.github.redirect-uri=http://127.0.0.1:5500/oauth/callback.html

然后登录成功后,需要向前端返回Json,需要我们实现AuthenticationSuccessHandler接口:

java
@Component
public class OAuth2SuccessAuthenticationHandler implements AuthenticationSuccessHandler {

    @Resource
    private ObjectMapper objectMapper;

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request,
                                        HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {

        // 在这里将authentication强转为OAuth2AuthenticationToken,是因为OAuth2LoginAuthenticationFilter返回的
        OAuth2AuthenticationToken oAuth2AuthenticationToken = (OAuth2AuthenticationToken) authentication;
        Map<String, Object> attributes = oAuth2AuthenticationToken.getPrincipal().getAttributes();

        response.setStatus(HttpServletResponse.SC_OK);

        response.setContentType("application/json;charset=UTF-8");

        // 此处应该返回jwt,此处直接返回所有的个人信息,仅做测试
        response.getWriter().write(objectMapper.writeValueAsString(attributes));
        response.getWriter().flush();
        response.getWriter().close();

    }
}

然后在配置类中添加OAuth登录成功处理器以及跨域处理:

java
@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Resource
    private OAuth2SuccessAuthenticationHandler oAuth2SuccessAuthenticationHandler;

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        DefaultSecurityFilterChain build = http
                .cors(Customizer.withDefaults())
                .oauth2Login(x -> x.successHandler(oAuth2SuccessAuthenticationHandler))
                .formLogin(x->x.disable())
                .authorizeHttpRequests(x -> x.anyRequest().authenticated())
                .sessionManagement(x -> x.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .build();

        return build;
    }

    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration configuration = new CorsConfiguration();
        configuration.setAllowedOrigins(Arrays.asList("http://127.0.0.1:5500")); // 允许的来源
        configuration.setAllowedMethods(Arrays.asList("GET","POST", "PUT", "DELETE")); // 允许的 HTTP 方法
        configuration.setAllowedHeaders(Arrays.asList("Content-Type", "Authorization")); // 允许的请求头
        configuration.setAllowCredentials(true); // 允许发送 Cookie
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", configuration); // 所有路径都应用此配置
        return source;
    }
}

第15行:由于是前后端分离项目,所以我们不需要创建SESSION。

但是,Spring Security OAuth Client中默认使用Session保存 OAuth2AuthorizationRequest,所以会导致回调接口因为找不到OAuth2AuthorizationRequest报错,我们需要自己实现保存OAuth2AuthorizationRequest的逻辑:

java
public class InMemoryOAuth2AuthorizationRequestRepo
        implements AuthorizationRequestRepository<OAuth2AuthorizationRequest> {

    private Map<String, OAuth2AuthorizationRequest> authorizationRequestMap;

    public InMemoryOAuth2AuthorizationRequestRepo() {
        authorizationRequestMap = new HashMap<>();
    }

    @Override
    public OAuth2AuthorizationRequest loadAuthorizationRequest(HttpServletRequest request) {
        String state = getStateParameter(request);
        if (state == null) {
            return null;
        }

        return authorizationRequestMap.getOrDefault(state, null);
    }

    @Override
    public void saveAuthorizationRequest(OAuth2AuthorizationRequest authorizationRequest,
                                         HttpServletRequest request,
                                         HttpServletResponse response) {
        String state = authorizationRequest.getState();

        if (state == null) {
            return ;
        }

        authorizationRequestMap.put(state, authorizationRequest);
    }

    @Override
    public OAuth2AuthorizationRequest removeAuthorizationRequest(HttpServletRequest request,
                                                                 HttpServletResponse response) {
        String state = getStateParameter(request);
        if (state == null) {
            return null;
        }

        return authorizationRequestMap.remove(state);
    }

    private String getStateParameter(HttpServletRequest request) {
        return request.getParameter(OAuth2ParameterNames.STATE);
    }
}

然后在配置类中使用我们的InMemoryOAuth2AuthorizationRequestRepo

java
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    DefaultSecurityFilterChain build = http
            .cors(Customizer.withDefaults())
            .oauth2Login(x -> x.successHandler(oAuth2SuccessAuthenticationHandler))
            .formLogin(x->x.disable())
            .authorizeHttpRequests(x -> x.anyRequest().authenticated())
            .sessionManagement(x -> x.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .build();


    List<Filter> filters = build.getFilters();
    InMemoryOAuth2AuthorizationRequestRepo inMemoryOAuth2AuthorizationRequestRepo = new InMemoryOAuth2AuthorizationRequestRepo();
    for (Filter filter : filters) {
        if(filter instanceof OAuth2LoginAuthenticationFilter) {
            ((OAuth2LoginAuthenticationFilter) filter).setAuthorizationRequestRepository(inMemoryOAuth2AuthorizationRequestRepo);
        }
        if(filter instanceof OAuth2AuthorizationRequestRedirectFilter){
            ((OAuth2AuthorizationRequestRedirectFilter) filter).setAuthorizationRequestRepository(inMemoryOAuth2AuthorizationRequestRepo);
        }
    }

    return build;
}

4. 测试效果

首先访问主页,由于没有登录,会自动跳转到登录界面:

image-20250120185107922

然后在登录界面点击按钮:

image-20250120185154875

在回调界面显示跳转中:

image-20250120185234147

跳转回主页,已经有GitHub人员信息了:

image-20250120185038397