尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

OAuth2 授权码模式单点登录实战:双客户端与迁移指南

OAuth2 授权码模式单点登录实战:双客户端与迁移指南 简介这份 PDF 文档围绕使用 Spring Security OAuth2 实现单点登录展开面向具备 Spring Boot 与 Spring Security 基础、希望为多应用系统搭建统一认证入口的 Java 后端开发者。文档以授权服务器加两个客户端应用的三模块结构为主线依次讲解 Maven 依赖引入、EnableOAuth2Sso 与 WebSecurityConfigurerAdapter 的安全配置、首页与登录页放行规则、application.yml 中 clientId、accessTokenUri、userInfoUri 等参数含义以及 EnableAuthorizationServer 下授权服务器与资源服务器合并部署的写法并采用授权码模式驱动认证委派涵盖 UISESSION 会话 cookie、未认证访问受保护页面时的重定向流程也提示了可替换为第三方身份提供商的思路。压缩包仅 1 个 PDF 文件约 75KB篇幅精炼便于离线阅读与随手查阅。目前已有 3578 人学习下载对需要理解 OAuth2 授权码流程在 Spring 生态中如何落地的读者可作为一份轻量的配置对照与流程梳理笔记。1. 为什么单点登录不能只靠共享 SessionOAuth2 授权码模式拆解假设你手上有两个内部后台分别部署在/ui和/ui2运维希望用户登录一次就能访问两个系统。常见做法是把 Session 存到 Redis 并让两个应用共享但这样每个应用都要读同一份用户表权限模型也容易耦合。Spring Security OAuth2 给出的方案是引入独立授权服务器客户端不碰密码只把未认证请求重定向到授权服务器由授权服务器完成登录并颁发授权码客户端再用授权码换取访问令牌最后通过userInfoUri拉取当前用户。EnableOAuth2Sso把这个授权码流程封装成注解适合快速给多个 Spring Boot 应用加统一登录。注意 Spring Security OAuth2 已进入维护阶段但存量项目仍多理解它的端点契约对迁移到 Spring Authorization Server 也有帮助。2. 授权服务器EnableAuthorizationServer 与 authorization_code 端点契约2.1 依赖与双角色启动类授权服务器需要同时扮演两个角色发放令牌的授权服务器以及提供/user/me的资源服务器。先看pom.xml依赖。依赖坐标版本作用spring-boot-starter-web由 Spring Boot 管理提供 Web 容器与 MVCspring-security-oauth22.3.3.RELEASE授权服务器与资源服务器实现启动类上同时使用SpringBootApplication和EnableResourceServer让本应用保护资源端点并让/oauth/check_token可用。SpringBootApplication EnableResourceServer public class AuthorizationServerApplication extends SpringBootServletInitializer { public static void main(String[] args) { SpringApplication.run(AuthorizationServerApplication.class, args); } }EnableResourceServer会创建OAuth2AuthenticationProcessingFilter从请求头Authorization: Bearer token中解析令牌。如果后续要给其他微服务做资源服务器可以单独拆出去但在最小 SSO 演示里放在一起能减少启动成本。2.2 AuthorizationServerConfigurerAdapter 三组配置核心配置类继承AuthorizationServerConfigurerAdapter重写三个configure方法。Configuration EnableAuthorizationServer public class AuthServerConfig extends AuthorizationServerConfigurerAdapter { Autowired private BCryptPasswordEncoder passwordEncoder; Override public void configure(AuthorizationServerSecurityConfigurer oauthServer) throws Exception { oauthServer.tokenKeyAccess(permitAll()) .checkTokenAccess(isAuthenticated()); } Override public void configure(ClientDetailsServiceConfigurer clients) throws Exception { clients.inMemory() .withClient(SampleClientId) .secret(passwordEncoder.encode(secret)) .authorizedGrantTypes(authorization_code) .scopes(user_info) .autoApprove(true) .redirectUris(http://localhost:8082/ui/login, http://localhost:8083/ui2/login); } }tokenKeyAccess(permitAll())允许公开获取令牌签名公钥checkTokenAccess(isAuthenticated())要求调用/oauth/check_token时携带客户端凭证。authorizedGrantTypes只开authorization_code不暴露密码模式和客户端模式。autoApprove(true)跳过用户手动授权页适合内部系统如果面向第三方必须改为false。redirectUris是白名单必须精确到客户端的 context-path 加/login多一个斜杠都会导致invalid redirect_uri。2.3 表单登录与内存用户授权服务器需要一个登录页配置类用Order(1)限定过滤器链只处理/login和/oauth/authorize。Configuration Order(1) public class SecurityConfig extends WebSecurityConfigurerAdapter { Override protected void configure(HttpSecurity http) throws Exception { http.requestMatchers() .antMatchers(/login, /oauth/authorize) .and() .authorizeRequests() .anyRequest().authenticated() .and() .formLogin().permitAll(); } Override protected void configure(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication() .withUser(john) .password(passwordEncoder().encode(123)) .roles(USER); } Bean public BCryptPasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }Order(1)避免这套表单登录规则影响/oauth/token等端点。内存用户john/123只用于演示生产环境替换为userDetailsService或对接 LDAP。密码必须经过BCryptPasswordEncoder编码否则登录时直接返回 401。2.4 用户信息端点与端点清单资源服务器需要一个返回当前用户详情的接口。RestController public class UserController { GetMapping(/user/me) public Principal user(Principal principal) { return principal; } }授权服务器 context-path 为/auth所以完整路径是http://localhost:8081/auth/user/me。携带令牌调用curl -H Authorization: Bearer $ACCESS_TOKEN \ http://localhost:8081/auth/user/me返回 JSON 中name字段是john客户端会用这个字段渲染用户名。端点URL说明授权端点/auth/oauth/authorize浏览器重定向到此完成登录令牌端点/auth/oauth/token客户端后台用授权码换令牌校验端点/auth/oauth/check_token资源服务器校验令牌公钥端点/auth/oauth/token_key获取 JWT 签名公钥用户信息/auth/user/me返回当前用户 Principal2.5 手工走一遍授权码流程不启动客户端直接用浏览器验证授权服务器。访问http://localhost:8081/auth/oauth/authorize?response_typecodeclient_idSampleClientIdredirect_urihttp://localhost:8082/ui/loginscopeuser_info会重定向到/auth/login输入john/123后因为autoApprove(true)直接 302 到http://localhost:8082/ui/login?codexxx。复制code换令牌curl -X POST http://localhost:8081/auth/oauth/token \ -u SampleClientId:secret \ -d grant_typeauthorization_code \ -d code替换为实际code \ -d redirect_urihttp://localhost:8082/ui/login-u用客户端 ID 和 secret 做 Basic 认证grant_type固定authorization_coderedirect_uri必须与授权请求和注册值完全一致否则返回invalid_grant。成功时返回access_token、token_type、expires_in、scope这一步能快速定位授权服务器自身的问题。3. 客户端接入EnableOAuth2Sso 与 application.yml 参数映射3.1 客户端依赖与版本边界客户端pom.xml依赖如下。依赖坐标版本作用spring-boot-starter-web由 Spring Boot 管理Web 容器spring-boot-starter-security由 Spring Boot 管理安全过滤链spring-security-oauth2-autoconfigure2.0.1.RELEASE提供EnableOAuth2Ssospring-boot-starter-thymeleaf由 Spring Boot 管理页面渲染thymeleaf-extras-springsecurity4由 Spring Boot 管理页面获取认证信息spring-security-oauth2-autoconfigure只适用于 Spring Boot 2.0.x/2.1.xSpring Boot 2.7 之后应改用spring-boot-starter-oauth2-client。版本不匹配会出现EnableOAuth2Sso找不到或自动配置冲突排查时先看mvn dependency:tree中是否混入多个 OAuth2 自动配置包。3.2 UiSecurityConfig一个注解开启 SSO客户端安全配置类只需一个注解加一个configure方法。Configuration EnableOAuth2Sso public class UiSecurityConfig extends WebSecurityConfigurerAdapter { Override public void configure(HttpSecurity http) throws Exception { http.antMatcher(/**) .authorizeRequests() .antMatchers(/, /login**) .permitAll() .anyRequest() .authenticated(); } }EnableOAuth2Sso会注册OAuth2SsoConfigurer和OAuth2ClientAuthenticationProcessingFilter。extends WebSecurityConfigurerAdapter后必须重写configure(HttpSecurity)否则默认所有请求都要认证连首页都进不去。antMatcher(/**)限定范围permitAll放行首页和登录回调。/login是EnableOAuth2Sso默认回调路径授权服务器redirectUris必须匹配客户端 context-path 加/login。3.3 application.yml 中 OAuth2 客户端参数逐项解释客户端配置文件如下。server: port: 8082 servlet: context-path: /ui session: cookie: name: UISESSION security: basic: enabled: false oauth2: client: clientId: SampleClientId clientSecret: secret accessTokenUri: http://localhost:8081/auth/oauth/token userAuthorizationUri: http://localhost:8081/auth/oauth/authorize resource: userInfoUri: http://localhost:8081/auth/user/me spring: thymeleaf: cache: false参数示例值对应端点或作用server.port8082客户端监听端口servlet.context-path/ui影响回调地址session.cookie.nameUISESSION区分不同客户端的会话 Cookiesecurity.basic.enabledfalse关闭 HTTP Basic 弹窗clientIdSampleClientId授权服务器注册的客户端 IDclientSecretsecret授权服务器注册的明文 secretaccessTokenUri/auth/oauth/token客户端后台换令牌userAuthorizationUri/auth/oauth/authorize浏览器重定向的授权地址userInfoUri/auth/user/me携带令牌拉取用户信息security.basic.enabledfalse必须设置否则未认证时会先弹 HTTP Basic 对话框而不是跳转 SSO。session.cookie.name每个客户端必须不同否则同域下两个客户端的JSESSIONID会互相覆盖。userInfoUri的路径要跟授权服务器UserController的映射一致写成/auth/userinfo会直接 404。3.4 前端页面与认证信息渲染客户端页面保持极简index.html放一个进入受保护页的链接。h1Spring Security SSO/h1 a hrefsecuredPageLogin/asecuredPage.html用 Thymeleaf 显示当前用户名。h1Secured Page/h1 Welcome, span th:text${#authentication.name}Name/span#authentication来自 Spring Security 上下文EnableOAuth2Sso在回调后会把用户信息封装成OAuth2Authentication。如果页面显示Welcome,后面空白先检查userInfoUri返回的 JSON 是否有name字段再检查thymeleaf-extras-springsecurity4是否与 Spring Security 版本匹配。常见做法是在securedPage中同时输出${#authentication.authorities}确认权限是否加载。3.5 启动顺序与验证先启动授权服务器 8081再启动客户端 8082。访问http://localhost:8082/ui/点击 Login观察浏览器地址栏先 302 到http://localhost:8081/auth/oauth/authorize?...登录后 302 回http://localhost:8082/ui/login?code...最终到securedPage。用 curl 看重定向curl -I http://localhost:8082/ui/securedPage应返回 302Location指向授权服务器。如果直接返回 401检查security.basic.enabled是否为false以及UiSecurityConfig是否被 Spring 扫描到。如果返回 404检查context-path和securedPage控制器是否在同一路径下。4. 双客户端 SSO 实战端口、Cookie 与 /user/me 用户信息回传4.1 第二个客户端的差异配置复制客户端 A修改application.yml中以下三项其余 OAuth2 客户端参数保持一致。server: port: 8083 servlet: context-path: /ui2 session: cookie: name: UI2SESSION同时把授权服务器redirectUris增加http://localhost:8083/ui2/login。两个客户端的配置差异如下。配置项客户端 A客户端 Bserver.port80828083servlet.context-path/ui/ui2session.cookie.nameUISESSIONUI2SESSION注册的redirectUrishttp://localhost:8082/ui/loginhttp://localhost:8083/ui2/loginclientId可以共用因为授权服务器按redirectUri白名单校验。生产环境建议每个客户端独立clientId和secret便于审计和吊销。如果两个客户端部署在同一域名不同路径下Cookie 名不同尤其重要如果部署在不同子域还可以通过cookie.path进一步隔离。4.2 验证 SSO 是否真正生效启动三个应用授权服务器 8081、客户端 A 8082、客户端 B 8083。浏览器先访问http://localhost:8082/ui/securedPage完成john/123登录。然后访问http://localhost:8083/ui2/securedPage。预期不再提示输入密码直接显示Welcome, john。原理是第一次登录后授权服务器在 8081 域下建立了会话。第二次访问客户端 B 时客户端 B 把用户重定向到授权服务器/oauth/authorize授权服务器发现已有会话直接生成 code 回调客户端 B客户端 B 换 token 并拉取/user/me。用浏览器开发者工具 Network 面板观察 302 链路或者用 curl 观察首次跳转curl -I http://localhost:8083/ui2/securedPage第一次会返回 302Location指向授权服务器。由于 curl 不会自动完成表单登录这个命令主要用于确认客户端 B 已经受保护并会触发 SSO 重定向。4.3 用户信息回传的结构与字段映射UserController返回Principal的 JSON 序列化结果包含name、authorities、authenticated等字段。EnableOAuth2Sso默认使用PrincipalName从userInfoUri响应中取name字段所以返回 JSON 必须有name。如果返回自定义 DTO需要保证字段名可映射或显式配置security.oauth2.resource.principal-attribute。配置项默认值作用security.oauth2.resource.userInfoUri无拉取用户信息的地址security.oauth2.resource.principal-attributename从响应中取哪个字段作为用户名security.oauth2.resource.prefer-token-infofalse为true时优先调用/oauth/check_tokenprefer-token-info为true时优先调用/oauth/check_token解析令牌而不是userInfoUri在授权服务器与资源服务器同进程时可用但跨服务时建议保持userInfoUri避免资源服务器依赖授权服务器的校验端点。4.4 会话与 Cookie 的边界两个客户端跑在同一台机器的不同端口浏览器把localhost视为同一域Cookie 不隔离端口所以两个客户端如果都用默认JSESSIONID会互相覆盖。这就是session.cookie.name必须显式区分的原因。授权服务器的会话 Cookie 默认也是JSESSIONID但它位于 8081 域与客户端 Cookie 名不同域不会冲突。如果部署在不同子域常见做法是设置cookie.path或使用不同的 cookie name。注意 SameSite 策略如果客户端和授权服务器跨站需检查浏览器是否阻止了回调 CookieSpring Boot 2.6 之后可通过server.servlet.session.cookie.same-site调整。授权服务器会话超时后客户端 B 会重新要求登录这说明 SSO 依赖的是授权服务器会话而不是客户端本地会话。5. OAuth2 单点登录排错与迁移从 token 刷新到 Spring Authorization Server5.1 高频故障对照表现象可能原因排查动作重定向循环redirectUris与客户端实际回调不一致看授权服务器日志中Invalid redirect URI用curl -I看Location回调后 401userInfoUri返回 401令牌未正确携带先用/oauth/token换令牌再手动请求/user/meuserInfoUri404路径写错确认 context-path 是/auth映射是/user/me页面显示Welcome,空白JSON 缺少name字段检查principal-attribute和UserController返回值第二个客户端仍要登录授权服务器会话失效或redirectUris未注册检查授权服务器内存redirectUris列表invalid_grant授权码已使用、过期或redirect_uri不一致重新发起授权核对redirect_uri大小写和斜杠5.2 token 刷新与退出登录EnableOAuth2Sso默认不处理refresh_token因为授权服务器只开了authorization_code。如果要支持刷新需在客户端授权类型中增加refresh_token并在客户端配置中保持access-token-uri不变。常见做法是配合OAuth2RestTemplate手动刷新或升级到 Spring Security 5 的OAuth2AuthorizedClientManager。退出登录时客户端/logout只清本地会话授权服务器会话仍在下次访问会直接 SSO 登录。要真正全局退出需要客户端重定向到授权服务器的退出端点或调用/oauth/token撤销令牌。一个简单的客户端退出配置http.logout().logoutSuccessUrl(http://localhost:8081/auth/login?logout);这会让客户端退出后回到授权服务器登录页但授权服务器会话是否清除取决于其自身/logout配置。生产环境通常用 OIDC 的end_session_endpoint完成单点退出。5.3 从 Spring Security OAuth2 迁移到 Spring Authorization ServerEnableAuthorizationServer在 2020 年后进入维护模式新项目建议 Spring Authorization Server。迁移对照如下。旧实现新实现EnableAuthorizationServerOAuth2AuthorizationServerConfigurationRegisteredClientRepositoryEnableOAuth2Ssospring-boot-starter-oauth2-clientoauth2Login()security.oauth2.client.*spring.security.oauth2.client.registration.*自定义/user/meOIDC UserInfo 或自定义端点迁移时redirectUris仍然要精确配置authorization_code授权类型不变但客户端认证方式默认从 Basic 改为client_secret_basic或client_secret_post需在RegisteredClient中设置ClientAuthenticationMethod。一个注册片段RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(SampleClientId) .clientSecret({bcrypt}$2a$10$...) .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .redirectUri(http://localhost:8082/ui/login) .scope(user_info) .build();clientSecret前缀{bcrypt}表示编码方式redirectUri必须与客户端请求完全一致scope大小写敏感。迁移后先用以下命令确认客户端仍然受保护curl -s -o /dev/null -w %{http_code}\n http://localhost:8082/ui/securedPage返回 302 表示客户端已受保护并会触发 SSO 重定向。本文还有配套的精品资源点击获取
返回列表