You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何使用Spring OAuth2改造基于数据库登录的MVC应用?

问题描述

您好,我的英文不太好。我现有一个可通过常规方式登录的MVC应用,当前使用Spring Security的数据库认证方式,代码如下:

@Configuration @EnableWebSecurity public class LoginSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { System.out.println("LoginSecurityConfig :: configure"); auth.jdbcAuthentication().dataSource( getDataSource() ) .passwordEncoder( new BCryptPasswordEncoder(16) ) .usersByUsernameQuery( "select user_name as username,password,enabled from users where user_name=?") .authoritiesByUsernameQuery( "select user_name as username, role_name from users_roles ur join users u on ur.user_id = u.user_id and u.user_name = ?"); } @Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() .authorizeRequests() .antMatchers("/login*").anonymous() .antMatchers("/resources/**").permitAll() .antMatchers("/fotos/**").permitAll() .antMatchers("/users").access("hasRole('ROLE_ADMIN')") .antMatchers("/user").access("hasRole('ROLE_ADMIN')") .anyRequest().authenticated() .and() .formLogin() .loginPage("/loginPage") .defaultSuccessUrl("/home", true) .failureUrl("/loginPage?error=true") .loginProcessingUrl("/login") .usernameParameter("username") .passwordParameter("password") .and() .logout() .logoutSuccessUrl("/loginPage") .invalidateHttpSession(true); } }

该配置可实现访问受保护资源时跳转至loginPage,提交用户名密码后获取Principal并访问资源,运行正常。

但我需要移除数据库用户管理,改用OAuth2实现相同的访问控制,不再保留本地数据库用户。我需要登录界面,然后发送类似http://myserver/oauth/token?grant_type=password&username=admin&password=admin的请求,在Basic认证中携带client_id和client_secret。我已能通过Postman获取token和刷新token,但不知如何在Web应用代码中实现。找到的教程多是将OAuth2服务端与客户端放在同一应用中,未讲解如何对接远程OAuth2服务。

我尝试过Baeldung的Spring Security OAuth教程,虽接近需求但过于复杂。我已有如下ResourceServer配置代码,了解其可调用远程服务验证token,但不知如何提供登录界面获取用户凭证来完成请求:

@Configuration @EnableResourceServer public class OAuth2ResourceServerConfigRemoteTokenService extends ResourceServerConfigurerAdapter { @Override public void configure(final HttpSecurity http) throws Exception { http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .and() .authorizeRequests().anyRequest().permitAll(); } @Primary @Bean public RemoteTokenServices tokenServices() { final RemoteTokenServices tokenService = new RemoteTokenServices(); tokenService.setCheckTokenEndpointUrl("http://myoauthserver/oauth/check_token"); tokenService.setClientId("clientid"); tokenService.setClientSecret("password"); return tokenService; } }

请问如何改造系统,获取用户登录凭证并结合现有代码实现类似原数据库方式的认证控制?另外,OAuth2是否仅适用于保护REST API?我对Spring不太熟悉,请尽量通俗易懂地解答。


解决方案:一步步改造对接远程OAuth2服务

嗨,我来帮你一步步搞定这个改造,尽量说得直白些,毕竟你说对Spring不太熟~

核心思路

我们要保留你原来的登录页面,把“提交用户名密码到本地数据库验证”的逻辑,改成“提交到远程OAuth2服务换token”,然后把token存在会话里,后续访问受保护资源时,用这个token去远程服务验证权限,和你原来的访问控制逻辑保持一致。


第一步:调整原有Spring Security配置,去掉数据库认证

首先修改你的LoginSecurityConfig,删掉原来的数据库认证代码,改成处理表单登录并自定义登录逻辑:

@Configuration
@EnableWebSecurity
public class LoginSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomLoginSuccessHandler customLoginSuccessHandler;

    @Autowired
    private CustomAuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 删掉原来的jdbcAuthentication,不需要本地数据库认证了
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .authorizeRequests()
                .antMatchers("/login*").anonymous()
                .antMatchers("/resources/**", "/fotos/**").permitAll()
                .antMatchers("/users", "/user").hasRole("ADMIN")
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/loginPage")
                .loginProcessingUrl("/login") // 和原来一样的登录提交路径
                .usernameParameter("username") // 和登录页面的表单字段名一致
                .passwordParameter("password")
                .successHandler(customLoginSuccessHandler) // 自定义登录成功后的逻辑:去拿token
                .failureHandler(customAuthenticationFailureHandler) // 登录失败处理
            .and()
            .logout()
                .logoutSuccessUrl("/loginPage")
                .invalidateHttpSession(true)
                .deleteCookies("JSESSIONID"); // 注销时清理cookie
    }
}

第二步:实现自定义登录处理器,向远程OAuth2服务请求token

我们需要写两个处理器:一个处理登录成功,去远程服务拿token;一个处理登录失败,跳回登录页带错误信息。

1. 登录成功处理器(核心逻辑)

这个类会在用户提交用户名密码后,调用远程OAuth2的/oauth/token接口,获取token并存到session里:

@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private final RestTemplate restTemplate = new RestTemplate();

    // 远程OAuth2服务的token接口地址
    private static final String OAUTH_TOKEN_URL = "http://myoauthserver/oauth/token";
    // 你的client_id和client_secret
    private static final String CLIENT_ID = "clientid";
    private static final String CLIENT_SECRET = "password";

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        // 获取用户提交的用户名密码
        String username = authentication.getName();
        String password = ((UsernamePasswordAuthenticationToken) authentication).getCredentials().toString();

        // 构造请求参数:grant_type=password,加上用户名密码
        MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
        params.add("grant_type", "password");
        params.add("username", username);
        params.add("password", password);

        // 设置Basic认证头:把client_id和client_secret用base64编码
        String auth = CLIENT_ID + ":" + CLIENT_SECRET;
        String encodedAuth = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8));
        HttpHeaders headers = new HttpHeaders();
        headers.add("Authorization", "Basic " + encodedAuth);
        headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);

        // 发送POST请求到远程OAuth2服务拿token
        HttpEntity<MultiValueMap<String, String>> requestEntity = new HttpEntity<>(params, headers);
        try {
            ResponseEntity<OAuth2Token> tokenResponse = restTemplate.postForEntity(OAUTH_TOKEN_URL, requestEntity, OAuth2Token.class);
            
            if (tokenResponse.getStatusCode().is2xxSuccessful() && tokenResponse.getBody() != null) {
                // 把token存到session里,后续请求要用
                request.getSession().setAttribute("oauth_token", tokenResponse.getBody());
                // 跳转到原来的默认成功页面/home
                getRedirectStrategy().sendRedirect(request, response, "/home");
            } else {
                // 拿token失败,跳回登录页带错误
                response.sendRedirect("/loginPage?error=true");
            }
        } catch (Exception e) {
            // 请求失败(比如网络问题、用户名密码错误),跳回登录页
            response.sendRedirect("/loginPage?error=true");
        }
    }

    // 定义一个简单的类来接收OAuth2返回的token
    public static class OAuth2Token {
        private String access_token;
        private String token_type;
        private String refresh_token;
        private int expires_in;
        
        // getter和setter方法
        public String getAccess_token() { return access_token; }
        public void setAccess_token(String access_token) { this.access_token = access_token; }
        public String getToken_type() { return token_type; }
        public void setToken_type(String token_type) { this.token_type = token_type; }
        public String getRefresh_token() { return refresh_token; }
        public void setRefresh_token(String refresh_token) { this.refresh_token = refresh_token; }
        public int getExpires_in() { return expires_in; }
        public void setExpires_in(int expires_in) { this.expires_in = expires_in; }
    }
}

2. 登录失败处理器

这个很简单,就是跳回登录页带错误参数:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        // 跳回登录页,带上错误标记
        response.sendRedirect("/loginPage?error=true");
    }
}

第三步:调整资源服务器配置,实现权限验证

修改你的OAuth2ResourceServerConfigRemoteTokenService,把授权规则改成和原来一致,同时添加一个过滤器,把session里的token放到请求头里,这样资源服务器能自动去远程验证:

@Configuration
@EnableResourceServer
public class OAuth2ResourceServerConfigRemoteTokenService extends ResourceServerConfigurerAdapter {

    @Override
    public void configure(final HttpSecurity http) throws Exception {
        http
            .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
            .and()
            .addFilterBefore(new OAuthTokenSessionFilter(), UsernamePasswordAuthenticationFilter.class) // 新增过滤器,从session拿token放到请求头
            .authorizeRequests()
                .antMatchers("/login*", "/resources/**", "/fotos/**").permitAll()
                .antMatchers("/users", "/user").hasRole("ADMIN") // 和原来的权限规则一致
                .anyRequest().authenticated();
    }

    @Primary
    @Bean
    public RemoteTokenServices tokenServices() {
        final RemoteTokenServices tokenService = new RemoteTokenServices();
        tokenService.setCheckTokenEndpointUrl("http://myoauthserver/oauth/check_token");
        tokenService.setClientId("clientid");
        tokenService.setClientSecret("password");
        return tokenService;
    }

    // 自定义过滤器:从session取出token,放到Authorization请求头里
    private static class OAuthTokenSessionFilter extends OncePerRequestFilter {
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
            // 从session里获取之前存的token
            CustomLoginSuccessHandler.OAuth2Token token = (CustomLoginSuccessHandler.OAuth2Token) request.getSession().getAttribute("oauth_token");
            if (token != null && token.getAccess_token() != null) {
                // 构造Bearer token头,放到请求里
                request.addHeader("Authorization", "Bearer " + token.getAccess_token());
            }
            filterChain.doFilter(request, response);
        }
    }
}

第四步:处理token过期(可选)

如果你的token有过期时间,你可以在过滤器里检查token是否过期,用refresh_token去刷新token,这个逻辑可以加在上面的OAuthTokenSessionFilter里,或者单独写一个过滤器,先保证核心功能跑通即可。


关于你的额外问题:OAuth2是否仅适用于保护REST API?

当然不是!OAuth2最初是为API设计的,但完全可以用来保护传统的MVC应用,就像我们现在做的这样:把token存在会话(session)里,每次请求自动带上token去远程验证,和你原来用session保存用户信息的逻辑几乎一样,只是用户的身份信息和权限是从远程OAuth2服务获取的,而不是本地数据库。


内容的提问来源于stack exchange,提问作者Magno C

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.29 08:17:33