1. 概述
本文将演示如何通过 Spring Security OAuth 与 Reddit API 集成,实现用户认证功能。整个流程基于 OAuth2 协议,适用于希望接入 Reddit 登录的 Web 应用。
我们不会止步于“能跑就行”,而是会深入关键环节,比如踩坑点、非标准实现的处理方式,帮助你在实际项目中少走弯路。
2. Maven 依赖配置
要使用 Spring Security 的 OAuth2 客户端功能,首先需要引入核心依赖:
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.0.6.RELEASE</version>
</dependency>
⚠️ 注意:该库已进入维护模式,官方推荐迁移到 Spring Security 5+ 的原生 OAuth2 支持。但如果你维护的是老项目,或追求快速集成,这套方案依然稳定可用。
3. OAuth2 客户端配置
我们需要配置一个 OAuth2RestTemplate
来管理令牌获取和资源访问。同时通过 reddit.properties
管理敏感信息。
配置类示例
@Configuration
@EnableOAuth2Client
@PropertySource("classpath:reddit.properties")
protected static class ResourceConfiguration {
@Value("${accessTokenUri}")
private String accessTokenUri;
@Value("${userAuthorizationUri}")
private String userAuthorizationUri;
@Value("${clientID}")
private String clientID;
@Value("${clientSecret}")
private String clientSecret;
@Bean
public OAuth2ProtectedResourceDetails reddit() {
AuthorizationCodeResourceDetails details = new AuthorizationCodeResourceDetails();
details.setId("reddit");
details.setClientId(clientID);
details.setClientSecret(clientSecret);
details.setAccessTokenUri(accessTokenUri);
details.setUserAuthorizationUri(userAuthorizationUri);
details.setTokenName("oauth_token");
details.setScope(Arrays.asList("identity"));
details.setPreEstablishedRedirectUri("http://localhost/login");
details.setUseCurrentUri(false);
return details;
}
@Bean
public OAuth2RestTemplate redditRestTemplate(OAuth2ClientContext clientContext) {
OAuth2RestTemplate template = new OAuth2RestTemplate(reddit(), clientContext);
AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(
Arrays.<AccessTokenProvider> asList(
new MyAuthorizationCodeAccessTokenProvider(),
new ImplicitAccessTokenProvider(),
new ResourceOwnerPasswordAccessTokenProvider(),
new ClientCredentialsAccessTokenProvider())
);
template.setAccessTokenProvider(accessTokenProvider);
return template;
}
}
reddit.properties
clientID=your_client_id_here
clientSecret=your_client_secret_here
accessTokenUri=https://www.reddit.com/api/v1/access_token
userAuthorizationUri=https://www.reddit.com/api/v1/authorize
✅ 提示:前往 https://www.reddit.com/prefs/apps 创建应用,获取 clientID
和 clientSecret
。
📌 关键点说明:
scope="identity"
:这是访问用户基本信息(如用户名)所必需的权限。preEstablishedRedirectUri
必须与你在 Reddit 后台注册的回调地址完全一致,否则会报redirect_uri_mismatch
错误。
4. 自定义 AuthorizationCodeAccessTokenProvider
Reddit 的 OAuth2 实现有 非标准行为 —— 如果你想获取长期有效的 token(即“永久 token”),必须在授权请求中显式添加 duration=permanent
参数。
而标准的 AuthorizationCodeAccessTokenProvider
并不支持这个参数,所以我们必须自定义实现。
解决方案:继承并覆盖关键方法
public class MyAuthorizationCodeAccessTokenProvider extends AuthorizationCodeAccessTokenProvider {
@Override
protected String getRedirectForAuthorization(OAuth2ProtectedResourceDetails resource,
MultiValueMap<String, String> requestParams) {
requestParams.add("duration", "permanent"); // ⚠️ Reddit 特有参数
return super.getRedirectForAuthorization(resource, requestParams);
}
}
✅ 踩坑提醒:
- 忘记加
duration=permanent
?那拿到的 token 默认只有 1 小时有效期。 - 这个参数只能在 授权阶段 添加,不能在获取 token 阶段补。
完整源码可参考:GitHub - Baeldung Reddit App
5. ServerInitializer 配置
为了让 OAuth2 上下文在整个请求链中可用,我们需要注册 oauth2ClientContextFilter
这个关键过滤器。
public class ServletInitializer extends AbstractDispatcherServletInitializer {
@Override
protected WebApplicationContext createServletApplicationContext() {
AnnotationConfigWebApplicationContext context =
new AnnotationConfigWebApplicationContext();
context.register(WebConfig.class, SecurityConfig.class);
return context;
}
@Override
protected String[] getServletMappings() {
return new String[] { "/" };
}
@Override
protected WebApplicationContext createRootApplicationContext() {
return null;
}
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
super.onStartup(servletContext);
registerProxyFilter(servletContext, "oauth2ClientContextFilter");
registerProxyFilter(servletContext, "springSecurityFilterChain");
}
private void registerProxyFilter(ServletContext servletContext, String name) {
DelegatingFilterProxy filter = new DelegatingFilterProxy(name);
filter.setContextAttribute(
"org.springframework.web.servlet.FrameworkServlet.CONTEXT.dispatcher");
servletContext.addFilter(name, filter).addMappingForUrlPatterns(null, false, "/*");
}
}
📌 为什么需要这个?
oauth2ClientContextFilter
负责维护 OAuth2 的临时状态(如 code、state)。- 缺少它会导致
NoSuchBeanDefinitionException
或认证流程中断。
6. MVC 配置
基础 MVC 配置,用于支持 JSP 视图和静态资源:
@Configuration
@EnableWebMvc
@ComponentScan(basePackages = { "org.baeldung.web" })
public class WebConfig implements WebMvcConfigurer {
@Bean
public static PropertySourcesPlaceholderConfigurer
propertySourcesPlaceholderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
@Bean
public ViewResolver viewResolver() {
InternalResourceViewResolver viewResolver = new InternalResourceViewResolver();
viewResolver.setPrefix("/WEB-INF/jsp/");
viewResolver.setSuffix(".jsp");
return viewResolver;
}
@Override
public void configureDefaultServletHandling(
DefaultServletHandlerConfigurer configurer) {
configurer.enable();
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**").addResourceLocations("/resources/");
}
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/home.html");
}
}
📌 简单粗暴但够用,适合演示场景。生产环境建议结合 Thymeleaf 或前后端分离。
7. 安全配置(SecurityConfig)
核心安全逻辑:禁用匿名访问和 CSRF,所有受保护接口需登录。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(AuthenticationManagerBuilder auth)
throws Exception {
auth.inMemoryAuthentication();
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.anonymous().disable()
.csrf().disable()
.authorizeRequests()
.antMatchers("/home.html").hasRole("USER")
.and()
.httpBasic()
.authenticationEntryPoint(oauth2AuthenticationEntryPoint());
}
private LoginUrlAuthenticationEntryPoint oauth2AuthenticationEntryPoint() {
return new LoginUrlAuthenticationEntryPoint("/login");
}
}
关键点:
.authenticationEntryPoint()
指定未认证时跳转到/login
,触发 OAuth2 流程。hasRole("USER")
表示访问/home.html
必须拥有ROLE_USER
角色。
8. RedditController:处理登录与认证
这是整个流程的“发动机”——用户访问 /login
时,自动完成 OAuth2 授权码流程,并建立本地认证。
@Controller
public class RedditController {
@Autowired
private OAuth2RestTemplate redditRestTemplate;
@RequestMapping("/login")
public String redditLogin() {
JsonNode node = redditRestTemplate.getForObject(
"https://oauth.reddit.com/api/v1/me", JsonNode.class);
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(
node.get("name").asText(),
redditRestTemplate.getAccessToken().getValue(),
Arrays.asList(new SimpleGrantedAuthority("ROLE_USER"))
);
SecurityContextHolder.getContext().setAuthentication(auth);
return "redirect:home.html";
}
}
✅ 核心机制说明:
redditRestTemplate.getForObject()
会自动:- 检查是否有有效 access token
- 若无,则跳转 Reddit 授权页获取 code
- 用 code 换取 token
- 再发起原始请求
https://oauth.reddit.com/api/v1/me
是 Reddit 提供的获取当前用户信息的接口。- 我们将 Reddit 用户名作为 principal 存入 Spring Security 上下文。
⚠️ 注意事项:
- 使用
oauth.reddit.com
而非www.reddit.com
,否则会返回 403。 - 需要 Jackson 支持
JsonNode
反序列化。
9. 前端页面:home.jsp
最简单的视图,展示已登录用户:
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags"%>
<html>
<body>
<h1>Welcome, <small><sec:authentication property="principal.username" /></small></h1>
</body>
</html>
📌 使用 Spring Security Taglib 直接读取当前认证用户的用户名。
10. 总结
本文完整实现了通过 Reddit OAuth2 登录 Spring Web 应用的流程,重点包括:
✅ 成功集成 spring-security-oauth2
✅ 处理 Reddit 非标准参数 duration=permanent
✅ 自动获取 token 并建立本地认证上下文
✅ 展示用户信息
虽然这套方案基于较老的技术栈(Spring Security OAuth),但在维护旧项目或快速原型开发中依然非常实用。
下一步可以扩展的功能:
- 实现自动刷新 token
- 绑定本地用户账户
- 访问更多 Reddit API(如发帖、点赞)
原文系列后续将深入这些高级主题,值得持续关注。