Spring Boot结合Google OAuth2实现Android客户端有状态会话身份认证的问题
老哥,你的场景我太熟了——Android端自己搞定Google登录拿到token,后端要接这个token做有状态会话,不用密码,还纠结oauth2login的问题是吧?别慌,我给你捋得明明白白:
先搞懂:为啥oauth2login不适合你
那个http.oauth2login()是给Web应用设计的授权码流,后端会自己跳转到Google的授权页引导用户登录,完全和你现在Android端先拿token的场景不搭,所以直接把这个配置从你的filterChain里删掉就行,不用在这上面浪费时间。
Android端该怎么传token?
标准且最省心的做法是:把Google返回的ID Token(划重点,是ID Token不是Access Token!Access Token是用来调Google API的,身份认证用ID Token才对)放在请求头里,格式如下:Authorization: Bearer <你的Google ID Token>
别搞什么把token放请求体里的操作,用请求头是行业惯例,后端解析也方便。
后端具体实现步骤
1. 添依赖&配置Google JWT验证
先在你的pom.xml(或者Gradle配置)里加上spring-boot-starter-security和spring-boot-starter-oauth2-resource-server这俩依赖,它们会帮你搞定JWT的签名验证,不用自己写复杂的加密逻辑。
然后在application.yml里配置Google的JWT解析规则:
spring: security: oauth2: resourceserver: jwt: issuer-uri: https://accounts.google.com jwk-set-uri: https://www.googleapis.com/oauth2/v3/certs
这个配置会自动拉取Google的公钥来验证ID Token的合法性,还会自动校验token的发行方是不是Google的账号服务器,省了你好多事。
2. 写自定义的认证接口
不用搞复杂的过滤器,先写个简单的POST接口来接收Android端的token,验证通过后直接创建有状态会话:
@RestController @RequestMapping("/api/auth") public class GoogleAuthController { private final JwtDecoder jwtDecoder; // 构造注入JwtDecoder,Spring会自动用我们刚才的配置实例化它 public GoogleAuthController(JwtDecoder jwtDecoder) { this.jwtDecoder = jwtDecoder; } @PostMapping("/google") public ResponseEntity<?> authenticateWithGoogleToken(@RequestHeader("Authorization") String authHeader, HttpServletRequest request) { // 先校验请求头格式对不对 if (!authHeader.startsWith("Bearer ")) { return ResponseEntity.badRequest().body("授权头格式错误,必须以Bearer开头"); } // 提取出真正的ID Token String idToken = authHeader.substring(7); try { // 解析并验证ID Token,不合法的话会直接抛JwtException Jwt jwt = jwtDecoder.decode(idToken); // 额外加个安全校验:确保这个token是发给你的Android应用的 String yourAndroidClientId = "这里替换成你在Google Cloud控制台给Android应用创建的Client ID"; if (!jwt.getAudience().contains(yourAndroidClientId)) { throw new JwtException("该Token不属于你的应用,禁止使用"); } // 从token里提取用户核心信息,sub是Google给每个用户分配的唯一不变ID String userId = jwt.getSubject(); String userEmail = jwt.getClaimAsString("email"); String userName = jwt.getClaimAsString("name"); // 创建已认证的Authentication对象,把它塞进SecurityContext List<GrantedAuthority> userAuthorities = Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER")); Authentication authenticatedUser = new UsernamePasswordAuthenticationToken( userId, null, userAuthorities ); SecurityContextHolder.getContext().setAuthentication(authenticatedUser); // 触发Spring创建有状态会话,默认就是有状态的,这里手动拿到会话可以存点用户信息方便后续使用 HttpSession userSession = request.getSession(true); userSession.setAttribute("userDetails", Map.of( "userId", userId, "email", userEmail, "nickname", userName )); return ResponseEntity.ok("登录成功,会话已创建"); } catch (JwtException e) { return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Token无效或已过期:" + e.getMessage()); } } }
3. 配置SecurityFilterChain,搞定权限和会话
把没用的oauth2login去掉,配置好权限规则和会话策略:
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http // 如果Android端用Cookie管理会话,生产环境建议开启CSRF防护,开发阶段可以先禁用省麻烦 .csrf(csrf -> csrf.disable()) .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/google").permitAll() // 认证接口必须放开,让Android能调用 .anyRequest().authenticated() // 其他所有接口都需要认证才能访问 ) // 明确配置有状态会话,默认就是IF_REQUIRED(需要时创建),写出来更清晰 .sessionManagement(session -> session .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .maximumSessions(1) // 可选配置:限制同一用户只能同时登录一个设备 ); return http.build(); } // 注入符合Google配置的JwtDecoder @Bean public JwtDecoder jwtDecoder() { return JwtDecoders.fromIssuerLocation("https://accounts.google.com"); } }
最后,Android端的后续请求处理
当Android端调用完上面的认证接口后,后端会在响应头里返回Set-Cookie,里面包含JSESSIONID(Spring默认的会话ID)。你用OkHttp或者Retrofit的话,只要配置自动管理Cookie,比如给OkHttp加个CookieJar实现,后续所有请求都会自动带上这个Cookie,后端就能通过JSESSIONID找到对应的会话,自动识别用户身份,根本不用每次再传Google的token了。
哦对了,再补个小提醒:别用Google的Access Token来做后端认证!Access Token是用来访问Google自身API的,有效期短,而且结构不适合做身份校验,ID Token才是专门用来做跨应用身份认证的标准凭证。
内容来源于stack exchange

