一、OAuth2 简介
1.1 什么是 OAuth2?
- 定义:OAuth2(Open Authorization)是一种用于安全授权的开放标准协议。
- 作用:允许第三方应用安全地访问用户资源,而无需暴露用户的身份凭证。
1.2 OAuth2 的基本概念
- Resource Owner(资源拥有者):通常是用户。
- Client(客户端):需要访问用户资源的应用程序。
- Resource Server(资源服务器):存储用户资源并提供受保护资源访问接口的服务器。
- Authorization Server(授权服务器):负责验证用户身份并向客户端颁发访问令牌。
1.3 OAuth2 的应用场景
- 适用于单点登录(SSO)、开放平台授权以及移动应用和第三方Web服务集成。
二、OAuth2 认证流程
2.1 四种授权方式
- 授权码模式(Authorization Code):适合服务器端应用。
- 简化模式(Implicit):适用于客户端应用。
- 密码模式(Resource Owner Password Credentials):适合用户信任的应用。
- 客户端凭证模式(Client Credentials):适合服务与服务之间的通信。
2.2 授权码模式流程分析
授权码模式较为常见,主要流程如下:
- 用户授权请求:客户端将用户重定向到授权服务器,用户登录并授予权限。
- 返回授权码:授权服务器重定向回客户端并携带授权码。
- 交换令牌:客户端使用授权码向授权服务器请求令牌。
- 访问资源:客户端使用令牌访问资源服务器上的受保护资源。
2.3 时序图
三、OAuth2 授权机制详解
3.1 访问令牌(Access Token)
- 访问令牌是 OAuth2 认证的关键,通常是短生命周期的随机字符串。
- 存储方式:常见存储方法有JWT(JSON Web Token)和Opaque Token。
3.2 刷新令牌(Refresh Token)
- 作用:延长授权时间,通过刷新令牌请求新的访问令牌。
- 使用场景:当访问令牌过期时,客户端可以使用刷新令牌重新获取授权。
3.3 令牌的存储与安全
- 建议加密存储令牌,避免令牌泄露。
- 在客户端使用 HTTPS 进行传输,防止中间人攻击。
四、Spring Boot 集成 OAuth2 和 JWT:实现短信验证登录
4.1 架构概述
在这次实现中,我们将使用 OAuth2 认证流程并结合 JWT 进行用户身份验证。用户通过短信验证码登录,经过验证后,生成 JWT 令牌,供前端用于后续请求。
流程如下:
- 请求验证码:用户在前端输入手机号,前端请求验证码接口。
- 验证码校验:后端生成验证码并存储在缓存中。
- 登录验证:前端发送验证码与手机号,后端校验验证码,生成 JWT 令牌。
- 后续请求验证:后续接口需附带 JWT 进行验证。
4.2 项目配置
在 pom.xml
中引入必要的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
4.3 JWT 配置
在 application.yml
中配置 JWT 私钥和过期时间:
jwt:
secret: mySecretKey
expiration: 3600 # 1小时,单位秒
4.4 发送短信验证码接口
实现短信服务
此接口生成验证码并缓存,实际中可以使用 Redis 实现缓存。
@RestController
@RequestMapping("/auth")
public class AuthController {
private final Map<String, String> smsCache = new HashMap<>(); // 临时缓存模拟
@GetMapping("/send-sms")
public ResponseEntity<?> sendSmsCode(@RequestParam String phoneNumber) {
String code = String.valueOf((int)((Math.random() * 9 + 1) * 1000));
smsCache.put(phoneNumber, code);
// 真实场景中需调用短信发送服务,如阿里云、Twilio
System.out.println("SMS Code for " + phoneNumber + " is " + code);
return ResponseEntity.ok("验证码已发送");
}
}
4.5 验证验证码并生成 JWT 令牌
在该接口中,校验验证码是否正确,正确则生成 JWT 令牌:
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
@RestController
@RequestMapping("/auth")
public class AuthController {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private long expiration;
@PostMapping("/login")
public ResponseEntity<?> login(@RequestParam String phoneNumber, @RequestParam String code) {
if (!smsCache.containsKey(phoneNumber) || !smsCache.get(phoneNumber).equals(code)) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("验证码错误");
}
String token = generateToken(phoneNumber);
return ResponseEntity.ok(Collections.singletonMap("token", token));
}
private String generateToken(String phoneNumber) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setSubject(phoneNumber)
.setIssuedAt(new Date())
.setExpiration(expiryDate)
.signWith(SignatureAlgorithm.HS512, secret)
.compact();
}
}
4.6 JWT 验证配置
在 SecurityConfig
中配置 JWT 过滤器,校验请求头中的 JWT 令牌。
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.anyRequest().authenticated()
.and()
.oauth2ResourceServer()
.jwt();
}
}
4.7 前端代码示例(HTML + JavaScript)
以下代码展示如何通过 HTML 和 JavaScript 调用短信验证码和登录接口,并获取 JWT。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>SMS Login</title>
<script>
async function sendSms() {
const phoneNumber = document.getElementById('phone').value;
const response = await fetch(`/auth/send-sms?phoneNumber=${phoneNumber}`);
const data = await response.text();
alert(data);
}
async function login() {
const phoneNumber = document.getElementById('phone').value;
const code = document.getElementById('code').value;
const response = await fetch('/auth/login', {
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
body: `phoneNumber=${phoneNumber}&code=${code}`
});
const data = await response.json();
if (data.token) {
alert("登录成功");
localStorage.setItem('token', data.token);
} else {
alert("验证码错误");
}
}
</script>
</head>
<body>
<h2>短信验证码登录</h2>
<input type="text" id="phone" placeholder="手机号" required>
<button onclick="sendSms()">发送验证码</button>
<br><br>
<input type="text" id="code" placeholder="验证码" required>
<button onclick="login()">登录</button>
</body>
</html>
五、OAuth2 安全性
5.1 HTTPS 加密传输
所有的令牌和用户数据都应通过 HTTPS 传输,以避免被中间人拦截或篡改。特别是在生产环境下,未加密的 HTTP 传输会导致敏感数据暴露。
5.2 访问令牌的存储与保护
- 将访问令牌存储在客户端的安全存储中(如浏览器的
HttpOnly
Cookie 中)以防止跨站点脚本攻击(XSS)。 - 避免在 URL 中传递访问令牌。OAuth2 的
Authorization
头部是一种更为安全的传递方式,减少 URL 曝露令牌的风险。
5.3 限制令牌的生命周期
访问令牌应设置较短的有效期,推荐 10 分钟以内的生命周期,以减少令牌泄露后的风险。可使用刷新令牌来延长会话,以便在令牌失效后通过刷新令牌重新获取授权。
5.4 使用基于作用域的权限控制
OAuth2 支持定义令牌的访问作用域(Scope)。为提高安全性,应根据用户角色和 API 需求定义精确的权限范围,避免过度授权。示例如下:
spring:
security:
oauth2:
client:
registration:
google:
scope: profile, email # 精细化权限控制
5.5 使用状态参数防止 CSRF 攻击
在 OAuth2 授权过程中,使用 state
参数来防止跨站请求伪造(CSRF)攻击。每次授权请求时,生成一个随机的 state
值,并在重定向后验证其一致性。
5.6 日志记录与监控
在 OAuth2 授权过程中记录关键操作日志(如授权请求、令牌交换、用户信息请求等),以便进行审计和分析。例如,可以在 Spring Boot 中使用日志工具记录请求信息和异常信息。
示例代码:记录授权请求日志
@RestController
public class AuthLoggingController {
private static final Logger logger = LoggerFactory.getLogger(AuthLoggingController.class);
@GetMapping("/oauth2/callback")
public String callback(OAuth2AuthenticationToken authToken) {
logger.info("User authenticated with OAuth2: {}", authToken.getPrincipal());
return "You are authenticated!";
}
}
5.7 授权服务器的防护
如果在应用中配置了自定义授权服务器(如通过 Spring Authorization Server 实现),则应额外注意以下几点:
- 防暴力破解:限制尝试登录的次数,防止凭证暴力破解。
- 防止重放攻击:为授权请求和令牌交换实现唯一的事务 ID,确保同一请求不会被重复处理。