第 31 章:认证与授权
学习目标
- 掌握 OAuth2.0 流程
- 学会 JWT 与 Token 实战
- 实现 RBAC 权限模型
- 避免 Token 泄漏、权限过大的坑
一、认证 vs 授权
text
认证(Authentication):你是谁?
授权(Authorization):你能做什么?java
// 认证:校验用户名密码
public User login(String username, String password) {
User user = userRepository.findByUsername(username);
if (!encoder.matches(password, user.getPassword())) {
throw new BadCredentialsException();
}
return user;
}
// 授权:校验用户能做什么
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}二、Session 认证
java
// 1. 登录后创建 session
@PostMapping("/login")
public void login(@RequestBody LoginRequest req, HttpSession session) {
User user = authService.login(req);
session.setAttribute("user", user);
}
// 2. 拦截器校验
@Component
public class AuthInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
HttpSession session = req.getSession();
User user = (User) session.getAttribute("user");
if (user == null) {
res.setStatus(401);
return false;
}
return true;
}
}text
Session 缺点
├── 需要服务端存储
├── 多节点要共享 Session(Redis)
└── CSRF 风险三、Token 认证
java
// 1. 登录后返回 token
@PostMapping("/login")
public TokenDto login(@RequestBody LoginRequest req) {
User user = authService.login(req);
String token = jwtService.generate(user);
return new TokenDto(token, jwtService.getExpiry());
}
// 2. 拦截器校验
public class TokenInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object handler) {
String token = req.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
res.setStatus(401);
return false;
}
String userId = jwtService.parseUserId(token);
req.setAttribute("userId", userId);
return true;
}
}四、JWT 实战
java
// 1. 引入依赖
// io.jsonwebtoken:jjwt-api, jjwt-impl, jjwt-jackson
// 2. 生成 JWT
@Service
public class JwtService {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiry:3600}")
private long expirySec;
public String generate(User user) {
return Jwts.builder()
.setSubject(user.getId().toString())
.claim("username", user.getUsername())
.claim("role", user.getRole())
.setIssuedAt(new Date())
.setExpiration(new Date(System.currentTimeMillis() + expirySec * 1000))
.signWith(SignatureAlgorithm.HS256, secret)
.compact();
}
public Claims parse(String token) {
return Jwts.parserBuilder()
.setSigningKey(secret)
.build()
.parseClaimsJws(token)
.getBody();
}
}java
// 3. 过滤链
public class JwtFilter extends OncePerRequestFilter {
@Resource private JwtService jwtService;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) {
String header = req.getHeader("Authorization");
if (header != null && header.startsWith("Bearer ")) {
String token = header.substring(7);
try {
Claims claims = jwtService.parse(token);
Long userId = Long.parseLong(claims.getSubject());
UsernamePasswordAuthenticationToken auth =
new UsernamePasswordAuthenticationToken(userId, null, getAuthorities(claims));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (ExpiredJwtException e) {
res.setStatus(401);
res.getWriter().write("token expired");
return;
}
}
chain.doFilter(req, res);
}
}⚠️ 坑 1:JWT 默认无刷新机制,过期就要重新登录。双 Token:
access_token(短)+refresh_token(长)。
五、Refresh Token
java
// 1. 登录返回两个 token
public TokenPair login(LoginRequest req) {
User user = authService.login(req);
String accessToken = jwtService.generateAccess(user);
String refreshToken = jwtService.generateRefresh(user);
refreshTokenRepository.save(new RefreshToken(user.id(), refreshToken));
return new TokenPair(accessToken, refreshToken);
}
// 2. 用 refresh_token 换 access_token
@PostMapping("/refresh")
public TokenDto refresh(@RequestBody RefreshRequest req) {
Claims claims = jwtService.parse(req.getRefreshToken());
Long userId = Long.parseLong(claims.getSubject());
RefreshToken stored = refreshTokenRepository.findByUserIdAndToken(userId, req.getRefreshToken());
if (stored == null || stored.isExpired()) {
throw new UnauthorizedException();
}
// 重新生成
User user = userRepository.findById(userId).orElseThrow();
return new TokenDto(jwtService.generateAccess(user));
}六、OAuth2.0
text
角色
├── Resource Owner: 用户
├── Client: 第三方应用
├── Authorization Server: 授权服务器(发 token)
└── Resource Server: 资源服务器(API)
四种模式
├── Authorization Code(最常用)
├── Implicit(已废弃)
├── Client Credentials
└── Password(已废弃,推荐用 Authorization Code)java
// Authorization Server(Spring Authorization Server)
@Bean
public SecurityFilterChain authServerSecurity(HttpSecurity http) throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
return http.build();
}
// 客户端注册
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient client = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("order-client")
.clientSecret("{noop}secret")
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("https://app.example.com/callback")
.scope("read")
.scope("write")
.build();
return new InMemoryRegisteredClientRepository(client);
}java
// 资源服务器校验 token
@Bean
public SecurityFilterChain resourceServerSecurity(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth.anyRequest().authenticated())
.oauth2ResourceServer(oauth2 -> oauth2.jwt(Customizer.withDefaults()));
return http.build();
}⚠️ 坑 2:OAuth2 client_secret 写死在代码,泄漏 = 被攻击。用环境变量或 K8s Secret。
七、RBAC 权限模型
sql
-- 用户
CREATE TABLE user (
id BIGINT PRIMARY KEY,
username VARCHAR(50)
);
-- 角色
CREATE TABLE role (
id BIGINT PRIMARY KEY,
name VARCHAR(50) -- ADMIN / USER
);
-- 权限
CREATE TABLE permission (
id BIGINT PRIMARY KEY,
name VARCHAR(50) -- order:read, order:write
);
-- 用户-角色
CREATE TABLE user_role (
user_id BIGINT,
role_id BIGINT
);
-- 角色-权限
CREATE TABLE role_permission (
role_id BIGINT,
permission_id BIGINT
);java
// 实体关系
public class User {
private Long id;
private String username;
private List<Role> roles;
}
public class Role {
private Long id;
private String name;
private List<Permission> permissions;
}
public class Permission {
private Long id;
private String name;
private String resource;
private String action;
}java
// 授权
@PreAuthorize("hasAuthority('order:write')")
public Order createOrder(@RequestBody OrderDto dto) {
return orderService.create(dto);
}
@PreAuthorize("hasRole('ADMIN')")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}八、Spring Security 配置
java
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.addFilterBefore(jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}⚠️ 坑 3:Spring Security 默认生成 URL 静态资源权限,生产环境必须限制
/api/**需认证,但静态资源放行。
九、Token 存储策略
java
// 1. 客户端:localStorage(不安全,XSS 风险)
// 2. 客户端:HttpOnly Cookie(防 XSS,但 CSRF 风险)
// 3. 服务端:Redis(可主动失效)
// 实战:Refresh Token 必须服务端存储
public class RefreshToken {
private Long id;
private Long userId;
private String token;
private LocalDateTime expiresAt;
private boolean revoked;
}java
// 登出 = 撤销 token
@PostMapping("/logout")
public void logout(@RequestHeader("Authorization") String auth) {
String token = auth.substring(7);
String jti = jwtService.getJti(token);
blacklistService.add(jti, Duration.ofSeconds(jwtService.getExpiry()));
}十、单点登录(SSO)
text
SSO 流程
1. 用户访问 app1.example.com
2. app1 重定向到 sso.example.com/login
3. 用户登录,sso 写 sso.example.com Cookie
4. sso 重定向回 app1?token=xxx
5. app1 拿 token 调 sso /verify,获取用户信息
6. 后续访问其他 app,同样流程,有 sso Cookie 不登录java
// JWT 是天然 SSO 方案
// 同一授权服务签发的 token,所有资源服务共享本章小结
| 方案 | 适用 |
|---|---|
| Session | 传统 Web |
| JWT | 前后端分离 |
| OAuth2 | 第三方授权 |
| SSO | 多应用统一 |
| 关键点 | 建议 |
|---|---|
| Token 存储 | HttpOnly Cookie |
| Refresh Token | 服务端可撤销 |
| 密码 | BCrypt |
| 权限 | RBAC + 注解 |
动手练习
- JWT 集成:实现 JWT 生成与校验,加在过滤器链
- 双 Token:实现 access + refresh,过期自动换
- RBAC 模型:设计 5 个权限,2 个角色,验证越权拦截
- OAuth2:用 Spring Authorization Server 部署一个授权服务
下一章:第 32 章:数据加密 →