学习时间: 4-5小时
学习目标: 掌握Spring Security安全框架,实现JWT认证授权,保护API接口安全
详细学习清单
✅ 第一部分:Spring Security基础概念(60分钟)
1. Spring Security与前端安全对比
Vue.js (你熟悉的前端安全)
// 前端路由守卫
router.beforeEach((to, from, next) => {
const token = localStorage.getItem('token')
if (to.meta.requiresAuth && !token) {
next('/login')
} else if (to.path === '/login' && token) {
next('/dashboard')
} else {
next()
}
})
// 请求拦截器
axios.interceptors.request.use(config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
axios.interceptors.response.use(
response => response,
error => {
if (error.response.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
return Promise.reject(error)
}
)
Spring Security (今天学习的后端安全)
// 安全配置类
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
)
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
2. 安全概念理解
认证(Authentication)vs 授权(Authorization)
认证:你是谁? → 验证用户身份
授权:你能做什么? → 控制用户权限
例如:
- 登录系统 → 认证(验证用户名密码)
- 访问管理页面 → 授权(检查是否有ADMIN角色)
✅ 第二部分:JWT令牌机制(60分钟)
1. JWT结构理解
JWT组成
Header.Payload.Signature
Header: 算法类型和令牌类型
Payload: 用户信息、权限、过期时间
Signature: 签名验证,防止篡改
JWT工具类
// JwtTokenUtil.java
package com.example.demo.security;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
@Component
public class JwtTokenUtil {
@Value("${jwt.secret}")
private String secret;
@Value("${jwt.expiration}")
private Long expiration;
// 生成JWT令牌
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
// 生成JWT令牌(带额外信息)
public String generateToken(UserDetails userDetails, Map<String, Object> claims) {
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map<String, Object> claims, String subject) {
Date now = new Date();
Date expiryDate = new Date(now.getTime() + expiration * 1000);
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
// 从令牌中获取用户名
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
// 从令牌中获取过期时间
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
// 从令牌中获取指定声明
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
// 从令牌中获取所有声明
private Claims getAllClaimsFromToken(String token) {
return Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
}
// 检查令牌是否过期
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
// 验证令牌
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
// 获取签名密钥
private SecretKey getSigningKey() {
byte[] keyBytes = secret.getBytes();
return Keys.hmacShaKeyFor(keyBytes);
}
// 从请求头中提取令牌
public String extractTokenFromHeader(String header) {
if (header != null && header.startsWith("Bearer ")) {
return header.substring(7);
}
return null;
}
}
2. JWT配置属性
application.yml配置
# JWT配置
jwt:
secret: your-secret-key-must-be-at-least-256-bits-long
expiration: 86400 # 24小时,单位:秒
refresh-expiration: 604800 # 7天,单位:秒
# 安全配置
security:
cors:
allowed-origins: "http://localhost:3000,http://localhost:8080"
allowed-methods: "GET,POST,PUT,DELETE,OPTIONS"
allowed-headers: "*"
allow-credentials: true
✅ 第三部分:用户认证与授权(90分钟)
1. 用户实体与角色
User实体类
// User.java - 增强版用户实体
package com.example.demo.model;
import java.time.LocalDateTime;
import java.util.Set;
public class User {
private Long id;
private String username;
private String email;
private String password;
private String firstName;
private String lastName;
private String phone;
private String avatar;
private boolean enabled;
private boolean accountNonExpired;
private boolean credentialsNonExpired;
private boolean accountNonLocked;
private LocalDateTime lastLoginTime;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Set<Role> roles;
// 构造函数
public User() {}
public User(String username, String email, String password) {
this.username = username;
this.email = email;
this.password = password;
this.enabled = true;
this.accountNonExpired = true;
this.credentialsNonExpired = true;
this.accountNonLocked = true;
this.createTime = LocalDateTime.now();
this.updateTime = LocalDateTime.now();
}
// Getter和Setter方法
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
public String getPhone() { return phone; }
public void setPhone(String phone) { this.phone = phone; }
public String getAvatar() { return avatar; }
public void setAvatar(String avatar) { this.avatar = avatar; }
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public boolean isAccountNonExpired() { return accountNonExpired; }
public void setAccountNonExpired(boolean accountNonExpired) { this.accountNonExpired = accountNonExpired; }
public boolean isCredentialsNonExpired() { return credentialsNonExpired; }
public void setCredentialsNonExpired(boolean credentialsNonExpired) { this.credentialsNonExpired = credentialsNonExpired; }
public boolean isAccountNonLocked() { return accountNonLocked; }
public void setAccountNonLocked(boolean accountNonLocked) { this.accountNonLocked = accountNonLocked; }
public LocalDateTime getLastLoginTime() { return lastLoginTime; }
public void setLastLoginTime(LocalDateTime lastLoginTime) { this.lastLoginTime = lastLoginTime; }
public LocalDateTime getCreateTime() { return createTime; }
public void setCreateTime(LocalDateTime createTime) { this.createTime = createTime; }
public LocalDateTime getUpdateTime() { return updateTime; }
public void setUpdateTime(LocalDateTime updateTime) { this.updateTime = updateTime; }
public Set<Role> getRoles() { return roles; }
public void setRoles(Set<Role> roles) { this.roles = roles; }
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", email='" + email + '\'' +
", enabled=" + enabled +
", roles=" + roles +
'}';
}
}
Role角色类
// Role.java - 角色实体
package com.example.demo.model;
public class Role {
private Long id;
private String name;
private String description;
private String code; // ROLE_USER, ROLE_ADMIN
public Role() {}
public Role(String name, String description, String code) {
this.name = name;
this.description = description;
this.code = code;
}
// Getter和Setter
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getCode() { return code; }
public void setCode(String code) { this.code = code; }
}
2. 用户详情服务
CustomUserDetailsService.java
// CustomUserDetailsService.java
package com.example.demo.security;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.stream.Collectors;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
try {
User user = userService.getUserByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在: " + username);
}
// 转换角色为Spring Security的权限格式
var authorities = user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority(role.getCode()))
.collect(Collectors.toList());
return org.springframework.security.core.userdetails.User.builder()
.username(user.getUsername())
.password(user.getPassword())
.authorities(authorities)
.accountExpired(!user.isAccountNonExpired())
.accountLocked(!user.isAccountNonLocked())
.credentialsExpired(!user.isCredentialsNonExpired())
.disabled(!user.isEnabled())
.build();
} catch (Exception e) {
throw new UsernameNotFoundException("加载用户信息失败: " + username, e);
}
}
}
3. JWT认证过滤器
JwtAuthenticationFilter.java
// JwtAuthenticationFilter.java
package com.example.demo.security;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.web.filter.OncePerRequestFilter;
import java.io.IOException;
@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
final String authorizationHeader = request.getHeader("Authorization");
String username = null;
String jwt = null;
// 提取JWT令牌
if (authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = jwtTokenUtil.getUsernameFromToken(jwt);
}
// 验证令牌并设置认证信息
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(jwt, userDetails)) {
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
} catch (Exception e) {
logger.error("JWT认证过滤器处理失败", e);
}
filterChain.doFilter(request, response);
}
}
✅ 第四部分:Spring Security配置(60分钟)
1. 主安全配置类
SecurityConfig.java
// SecurityConfig.java
package com.example.demo.config;
import com.example.demo.security.CustomUserDetailsService;
import com.example.demo.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import java.util.Arrays;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private JwtAuthenticationFilter jwtAuthenticationFilter;
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 禁用CSRF(因为使用JWT)
.csrf(csrf -> csrf.disable())
// 配置CORS
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
// 配置会话管理(无状态)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
)
// 配置授权规则
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/actuator/**").permitAll()
.requestMatchers("/h2-console/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasRole("USER")
.anyRequest().authenticated()
)
// 添加JWT过滤器
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(userDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("*"));
configuration.setAllowCredentials(true);
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
2. 认证控制器
AuthController.java
// AuthController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.security.JwtTokenUtil;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@RestController
@RequestMapping("/api/auth")
public class AuthController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private JwtTokenUtil jwtTokenUtil;
@Autowired
private UserService userService;
// 用户登录
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
try {
// 认证用户
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginRequest.getUsername(),
loginRequest.getPassword()
)
);
// 生成JWT令牌
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
String token = jwtTokenUtil.generateToken(userDetails);
// 获取用户信息
User user = userService.getUserByUsername(userDetails.getUsername());
// 更新最后登录时间
userService.updateLastLoginTime(user.getId());
Map<String, Object> response = new HashMap<>();
response.put("success", true);
response.put("message", "登录成功");
response.put("token", token);
response.put("user", Map.of(
"id", user.getId(),
"username", user.getUsername(),
"email", user.getEmail(),
"roles", user.getRoles()
));
return ResponseEntity.ok(response);
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "用户名或密码错误"
));
}
}
// 用户注册
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody RegisterRequest registerRequest) {
try {
// 检查用户名是否已存在
if (userService.getUserByUsername(registerRequest.getUsername()) != null) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "用户名已存在"
));
}
// 检查邮箱是否已存在
if (userService.getUserByEmail(registerRequest.getEmail()) != null) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "邮箱已被注册"
));
}
// 创建用户
User user = new User(
registerRequest.getUsername(),
registerRequest.getEmail(),
registerRequest.getPassword()
);
User createdUser = userService.createUser(user);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "注册成功",
"data", createdUser
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 刷新令牌
@PostMapping("/refresh")
public ResponseEntity<?> refreshToken(@RequestHeader("Authorization") String authHeader) {
try {
String token = jwtTokenUtil.extractTokenFromHeader(authHeader);
if (token == null) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "无效的令牌"
));
}
String username = jwtTokenUtil.getUsernameFromToken(token);
UserDetails userDetails = userService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(token, userDetails)) {
String newToken = jwtTokenUtil.generateToken(userDetails);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "令牌刷新成功",
"token", newToken
));
} else {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "令牌已过期"
));
}
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "令牌刷新失败"
));
}
}
// 登出
@PostMapping("/logout")
public ResponseEntity<?> logout() {
// 由于使用JWT,服务端无法真正"登出"
// 客户端需要删除本地存储的令牌
return ResponseEntity.ok(Map.of(
"success", true,
"message", "登出成功"
));
}
// 获取当前用户信息
@GetMapping("/me")
public ResponseEntity<?> getCurrentUser(@RequestHeader("Authorization") String authHeader) {
try {
String token = jwtTokenUtil.extractTokenFromHeader(authHeader);
if (token == null) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "无效的令牌"
));
}
String username = jwtTokenUtil.getUsernameFromToken(token);
User user = userService.getUserByUsername(username);
return ResponseEntity.ok(Map.of(
"success", true,
"data", user
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", "获取用户信息失败"
));
}
}
}
// 登录请求DTO
class LoginRequest {
private String username;
private String password;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
}
// 注册请求DTO
class RegisterRequest {
private String username;
private String email;
private String password;
private String firstName;
private String lastName;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getPassword() { return password; }
public void setPassword(String password) { this.password = password; }
public String getFirstName() { return firstName; }
public void setFirstName(String firstName) { this.firstName = firstName; }
public String getLastName() { return lastName; }
public void setLastName(String lastName) { this.lastName = lastName; }
}
✅ 第五部分:受保护的API接口(60分钟)
1. 用户管理接口(需要认证)
SecureUserController.java
// SecureUserController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/user")
public class SecureUserController {
@Autowired
private UserService userService;
// 获取当前用户信息
@GetMapping("/profile")
public ResponseEntity<?> getProfile() {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userService.getUserByUsername(username);
return ResponseEntity.ok(Map.of(
"success", true,
"data", user
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 更新用户信息
@PutMapping("/profile")
public ResponseEntity<?> updateProfile(@RequestBody User updateUser) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User currentUser = userService.getUserByUsername(username);
User updatedUser = userService.updateUser(currentUser.getId(), updateUser);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "个人信息更新成功",
"data", updatedUser
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 修改密码
@PutMapping("/password")
public ResponseEntity<?> changePassword(@RequestBody ChangePasswordRequest request) {
try {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
String username = authentication.getName();
User user = userService.getUserByUsername(username);
userService.changePassword(user.getId(), request.getOldPassword(), request.getNewPassword());
return ResponseEntity.ok(Map.of(
"success", true,
"message", "密码修改成功"
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 获取用户列表(需要USER角色)
@PreAuthorize("hasRole('USER')")
@GetMapping("/list")
public ResponseEntity<?> getUserList() {
try {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(Map.of(
"success", true,
"data", users,
"total", users.size()
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
}
// 修改密码请求DTO
class ChangePasswordRequest {
private String oldPassword;
private String newPassword;
public String getOldPassword() { return oldPassword; }
public void setOldPassword(String oldPassword) { this.oldPassword = oldPassword; }
public String getNewPassword() { return newPassword; }
public void setNewPassword(String newPassword) { this.newPassword = newPassword; }
}
2. 管理员接口(需要ADMIN角色)
AdminController.java
// AdminController.java
package com.example.demo.controller;
import com.example.demo.model.User;
import com.example.demo.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/admin")
@PreAuthorize("hasRole('ADMIN')") // 类级别权限控制
public class AdminController {
@Autowired
private UserService userService;
// 获取所有用户
@GetMapping("/users")
public ResponseEntity<?> getAllUsers() {
try {
List<User> users = userService.getAllUsers();
return ResponseEntity.ok(Map.of(
"success", true,
"data", users,
"total", users.size()
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 根据ID获取用户
@GetMapping("/users/{id}")
public ResponseEntity<?> getUserById(@PathVariable Long id) {
try {
User user = userService.getUserById(id);
return ResponseEntity.ok(Map.of(
"success", true,
"data", user
));
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
// 创建用户
@PostMapping("/users")
public ResponseEntity<?> createUser(@RequestBody User user) {
try {
User createdUser = userService.createUser(user);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "用户创建成功",
"data", createdUser
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 更新用户
@PutMapping("/users/{id}")
public ResponseEntity<?> updateUser(@PathVariable Long id, @RequestBody User user) {
try {
User updatedUser = userService.updateUser(id, user);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "用户更新成功",
"data", updatedUser
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 删除用户
@DeleteMapping("/users/{id}")
public ResponseEntity<?> deleteUser(@PathVariable Long id) {
try {
userService.deleteUser(id);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "用户删除成功"
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 启用/禁用用户
@PatchMapping("/users/{id}/status")
public ResponseEntity<?> toggleUserStatus(@PathVariable Long id, @RequestParam boolean enabled) {
try {
userService.updateUserStatus(id, enabled);
return ResponseEntity.ok(Map.of(
"success", true,
"message", enabled ? "用户已启用" : "用户已禁用"
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 重置用户密码
@PatchMapping("/users/{id}/reset-password")
public ResponseEntity<?> resetUserPassword(@PathVariable Long id) {
try {
String newPassword = userService.resetUserPassword(id);
return ResponseEntity.ok(Map.of(
"success", true,
"message", "密码重置成功",
"newPassword", newPassword
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
// 获取系统统计信息
@GetMapping("/stats")
public ResponseEntity<?> getSystemStats() {
try {
Map<String, Object> stats = userService.getSystemStats();
return ResponseEntity.ok(Map.of(
"success", true,
"data", stats
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"success", false,
"message", e.getMessage()
));
}
}
}
✅ 第六部分:OAuth2基础概念(60分钟)
1. OAuth2核心概念
OAuth2是什么?
OAuth2 = Open Authorization 2.0
- 开放授权标准协议
- 允许第三方应用访问用户资源
- 无需提供用户名密码给第三方
- 广泛用于社交登录、API授权等场景
OAuth2角色定义
// OAuth2中的四个角色
public class OAuth2Roles {
/*
1. Resource Owner (资源所有者)
- 用户本人
- 拥有被保护资源的访问权限
2. Client (客户端)
- 第三方应用
- 请求访问用户资源
3. Authorization Server (授权服务器)
- 验证用户身份
- 颁发访问令牌
- 管理授权流程
4. Resource Server (资源服务器)
- 存储用户资源
- 验证访问令牌
- 提供受保护的API
*/
}
2. OAuth2四种授权模式
授权码模式(Authorization Code)
// 最安全、最常用的模式
// 适用于有后端的Web应用
// 1. 用户访问客户端
// 2. 客户端重定向到授权服务器
// 3. 用户授权
// 4. 授权服务器重定向回客户端(带授权码)
// 5. 客户端用授权码换取访问令牌
@RestController
@RequestMapping("/oauth2")
public class OAuth2Controller {
// 第一步:重定向到授权服务器
@GetMapping("/authorize")
public void authorize(HttpServletResponse response) throws IOException {
String authUrl = "https://oauth.provider.com/authorize" +
"?response_type=code" +
"&client_id=your_client_id" +
"&redirect_uri=http://localhost:8080/oauth2/callback" +
"&scope=read write" +
"&state=random_state_string";
response.sendRedirect(authUrl);
}
// 第二步:处理授权回调
@GetMapping("/callback")
public ResponseEntity<?> callback(@RequestParam String code,
@RequestParam String state) {
// 验证state参数防止CSRF攻击
if (!isValidState(state)) {
return ResponseEntity.badRequest().body("Invalid state");
}
// 用授权码换取访问令牌
String token = exchangeCodeForToken(code);
return ResponseEntity.ok(Map.of(
"success", true,
"token", token
));
}
private String exchangeCodeForToken(String code) {
// 实现授权码换令牌的逻辑
return "access_token_here";
}
private boolean isValidState(String state) {
// 验证state参数
return true;
}
}
简化模式(Implicit)
// 适用于纯前端应用(SPA)
// 直接在浏览器中获取访问令牌
// 1. 用户访问客户端
// 2. 客户端重定向到授权服务器
// 3. 用户授权
// 4. 授权服务器重定向回客户端(直接带访问令牌)
@GetMapping("/implicit")
public void implicitAuthorize(HttpServletResponse response) throws IOException {
String authUrl = "https://oauth.provider.com/authorize" +
"?response_type=token" + // 注意:这里是token不是code
"&client_id=your_client_id" +
"&redirect_uri=http://localhost:8080/oauth2/implicit-callback" +
"&scope=read write";
response.sendRedirect(authUrl);
}
@GetMapping("/implicit-callback")
public String implicitCallback(@RequestParam String access_token,
@RequestParam String token_type,
@RequestParam String expires_in) {
// 直接获取访问令牌
return "Access Token: " + access_token;
}
密码模式(Resource Owner Password Credentials)
// 适用于高度信任的客户端
// 直接使用用户名密码获取令牌
@PostMapping("/password")
public ResponseEntity<?> passwordGrant(@RequestBody PasswordGrantRequest request) {
try {
// 直接使用用户名密码获取令牌
String token = getTokenByPassword(
request.getUsername(),
request.getPassword()
);
return ResponseEntity.ok(Map.of(
"access_token", token,
"token_type", "Bearer",
"expires_in", 3600
));
} catch (Exception e) {
return ResponseEntity.badRequest().body("Authentication failed");
}
}
private String getTokenByPassword(String username, String password) {
// 实现密码模式获取令牌
return "access_token_from_password";
}
客户端凭证模式(Client Credentials)
// 适用于服务端到服务端的通信
// 不需要用户参与
@PostMapping("/client-credentials")
public ResponseEntity<?> clientCredentials() {
try {
String token = getTokenByClientCredentials();
return ResponseEntity.ok(Map.of(
"access_token", token,
"token_type", "Bearer",
"expires_in", 3600
));
} catch (Exception e) {
return ResponseEntity.badRequest().body("Client authentication failed");
}
}
private String getTokenByClientCredentials() {
// 使用客户端ID和密钥获取令牌
return "access_token_from_client_credentials";
}
✅ 第七部分:Spring Security OAuth2实现(90分钟)
1. OAuth2依赖配置
pom.xml依赖
<dependencies>
<!-- Spring Security OAuth2 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-authorization-server</artifactId>
<version>1.1.0</version>
</dependency>
<!-- Spring Security OAuth2 Client -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-client</artifactId>
</dependency>
<!-- Spring Security OAuth2 Resource Server -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-resource-server</artifactId>
</dependency>
<!-- Spring Security OAuth2 JOSE -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-oauth2-jose</artifactId>
</dependency>
</dependencies>
2. OAuth2授权服务器配置
AuthorizationServerConfig.java
// AuthorizationServerConfig.java
package com.example.demo.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.AuthorizationServerSettings;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.time.Duration;
import java.util.UUID;
@Configuration
@EnableWebSecurity
public class AuthorizationServerConfig {
@Bean
@Order(1)
public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http)
throws Exception {
OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
http.getConfigurer(OAuth2AuthorizationServerConfigurer.class)
.oidc(Customizer.withDefaults()); // Enable OpenID Connect 1.0
http
// Redirect to the login page when not authenticated from the
// authorization endpoint
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(
new LoginUrlAuthenticationEntryPoint("/login"))
);
return http.build();
}
@Bean
@Order(2)
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http)
throws Exception {
http
.authorizeHttpRequests((authorize) -> authorize
.anyRequest().authenticated()
)
// Form login handles the redirect to the login page from the
// authorization server filter chain
.formLogin(formLogin -> formLogin
.loginPage("/login")
.permitAll()
);
return http.build();
}
@Bean
public UserDetailsService userDetailsService() {
UserDetails userDetails = User.withDefaultPasswordEncoder()
.username("user")
.password("password")
.roles("USER")
.build();
return new InMemoryUserDetailsManager(userDetails);
}
@Bean
public RegisteredClientRepository registeredClientRepository() {
RegisteredClient oidcClient = RegisteredClient.withId(UUID.randomUUID().toString())
.clientId("oidc-client")
.clientSecret(passwordEncoder().encode("secret"))
.clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC)
.authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
.authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
.redirectUri("http://127.0.0.1:8080/login/oauth2/code/oidc-client")
.postLogoutRedirectUri("http://127.0.0.1:8080/")
.scope(OidcScopes.OPENID)
.scope(OidcScopes.PROFILE)
.scope("read")
.scope("write")
.clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build())
.tokenSettings(TokenSettings.builder()
.accessTokenTimeToLive(Duration.ofHours(1))
.refreshTokenTimeToLive(Duration.ofDays(7))
.build())
.build();
return new InMemoryRegisteredClientRepository(oidcClient);
}
@Bean
public JWKSource<SecurityContext> jwkSource() {
KeyPair keyPair = generateRsaKey();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
private static KeyPair generateRsaKey() {
KeyPair keyPair;
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
keyPair = keyPairGenerator.generateKeyPair();
}
catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
return keyPair;
}
@Bean
public JwtDecoder jwtDecoder(JWKSource<SecurityContext> jwkSource) {
return OAuth2AuthorizationServerConfiguration.jwtDecoder(jwkSource);
}
@Bean
public AuthorizationServerSettings authorizationServerSettings() {
return AuthorizationServerSettings.builder().build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
3. OAuth2客户端配置
OAuth2ClientConfig.java
// OAuth2ClientConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/oauth2/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(this.oidcUserService())
.userService(this.oauth2UserService())
)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.clearAuthentication(true)
);
return http.build();
}
@Bean
public OAuth2UserService<OidcUserRequest, OidcUser> oidcUserService() {
final OidcUserService delegate = new OidcUserService();
return (userRequest) -> {
// 委托给默认的OidcUserService
OidcUser oidcUser = delegate.loadUser(userRequest);
// 可以在这里添加自定义逻辑
// 比如保存用户信息到数据库
saveOidcUser(oidcUser);
return oidcUser;
};
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> oauth2UserService() {
return new CustomOAuth2UserService();
}
private void saveOidcUser(OidcUser oidcUser) {
// 保存OIDC用户信息到数据库
System.out.println("Saving OIDC user: " + oidcUser.getName());
}
}
CustomOAuth2UserService.java
// CustomOAuth2UserService.java
package com.example.demo.config;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User user = super.loadUser(userRequest);
// 获取客户端注册信息
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// 根据不同的OAuth2提供商处理用户信息
switch (registrationId) {
case "google":
return processGoogleUser(user);
case "github":
return processGithubUser(user);
case "facebook":
return processFacebookUser(user);
default:
return user;
}
}
private OAuth2User processGoogleUser(OAuth2User user) {
// 处理Google用户信息
System.out.println("Processing Google user: " + user.getName());
return user;
}
private OAuth2User processGithubUser(OAuth2User user) {
// 处理GitHub用户信息
System.out.println("Processing GitHub user: " + user.getName());
return user;
}
private OAuth2User processFacebookUser(OAuth2User user) {
// 处理Facebook用户信息
System.out.println("Processing Facebook user: " + user.getName());
return user;
}
}
✅ 第八部分:第三方OAuth2提供商集成(60分钟)
1. Google OAuth2集成
application.yml配置
spring:
security:
oauth2:
client:
registration:
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope:
- openid
- profile
- email
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
github:
client-id: your-github-client-id
client-secret: your-github-client-secret
scope:
- user:email
- read:user
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
facebook:
client-id: your-facebook-app-id
client-secret: your-facebook-app-secret
scope:
- email
- public_profile
redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
2. OAuth2登录控制器
OAuth2LoginController.java
// OAuth2LoginController.java
package com.example.demo.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class OAuth2LoginController {
@GetMapping("/user")
public Map<String, Object> user() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
return Map.of(
"name", oidcUser.getName(),
"email", oidcUser.getEmail(),
"picture", oidcUser.getPicture(),
"provider", "OIDC"
);
} else if (authentication.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
return Map.of(
"name", oauth2User.getName(),
"attributes", oauth2User.getAttributes(),
"provider", "OAuth2"
);
}
return Map.of("error", "No OAuth2 user found");
}
@GetMapping("/login-success")
public Map<String, Object> loginSuccess() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return Map.of(
"message", "登录成功",
"user", authentication.getName(),
"authorities", authentication.getAuthorities()
);
}
}
3. OAuth2资源服务器配置
ResourceServerConfig.java
// ResourceServerConfig.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class ResourceServerConfig {
@Bean
public SecurityFilterChain resourceServerFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/api/public/**").permitAll()
.requestMatchers("/api/protected/**").authenticated()
.anyRequest().authenticated()
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.decoder(jwtDecoder())
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
);
return http.build();
}
@Bean
public JwtDecoder jwtDecoder() {
// 配置JWT解码器
// 这里需要配置JWT的签名密钥或JWK Set URI
return NimbusJwtDecoder.withJwkSetUri("http://localhost:8080/.well-known/jwks.json").build();
}
@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
JwtGrantedAuthoritiesConverter authoritiesConverter = new JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("authorities");
JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
}
✅ 第九部分:OAuth2与JWT集成(60分钟)
1. OAuth2 JWT令牌配置
JwtTokenConfig.java
// JwtTokenConfig.java
package com.example.demo.config;
import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.oauth2.jwt.JwtDecoder;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.NimbusJwtDecoder;
import org.springframework.security.oauth2.jwt.NimbusJwtEncoder;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
import java.security.interfaces.RSAPrivateKey;
import java.security.interfaces.RSAPublicKey;
import java.util.UUID;
@Configuration
public class JwtTokenConfig {
@Bean
public KeyPair keyPair() {
try {
KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
return keyPairGenerator.generateKeyPair();
} catch (NoSuchAlgorithmException ex) {
throw new IllegalStateException(ex);
}
}
@Bean
public JWKSource<SecurityContext> jwkSource(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
RSAKey rsaKey = new RSAKey.Builder(publicKey)
.privateKey(privateKey)
.keyID(UUID.randomUUID().toString())
.build();
JWKSet jwkSet = new JWKSet(rsaKey);
return new ImmutableJWKSet<>(jwkSet);
}
@Bean
public JwtEncoder jwtEncoder(JWKSource<SecurityContext> jwkSource) {
return new NimbusJwtEncoder(jwkSource);
}
@Bean
public JwtDecoder jwtDecoder(KeyPair keyPair) {
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
return NimbusJwtDecoder.withPublicKey(publicKey).build();
}
}
2. OAuth2 JWT服务
OAuth2JwtService.java
// OAuth2JwtService.java
package com.example.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.oauth2.jwt.JwtEncoder;
import org.springframework.security.oauth2.jwt.JwtClaimsSet;
import org.springframework.stereotype.Service;
import java.time.Instant;
import java.time.temporal.ChronoUnit;
import java.util.List;
@Service
public class OAuth2JwtService {
@Autowired
private JwtEncoder jwtEncoder;
public String generateToken(String username, List<String> authorities) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(1, ChronoUnit.HOURS))
.subject(username)
.claim("authorities", authorities)
.claim("scope", "read write")
.build();
return jwtEncoder.encode(org.springframework.security.oauth2.jwt.JwtEncoderParameters.from(claims)).getTokenValue();
}
public String generateRefreshToken(String username) {
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.issuer("self")
.issuedAt(now)
.expiresAt(now.plus(7, ChronoUnit.DAYS))
.subject(username)
.claim("type", "refresh")
.build();
return jwtEncoder.encode(org.springframework.security.oauth2.jwt.JwtEncoderParameters.from(claims)).getTokenValue();
}
}
3. OAuth2 API控制器
OAuth2ApiController.java
// OAuth2ApiController.java
package com.example.demo.controller;
import com.example.demo.service.OAuth2JwtService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
@RestController
@RequestMapping("/api/oauth2")
public class OAuth2ApiController {
@Autowired
private OAuth2JwtService jwtService;
// 受保护的API端点
@GetMapping("/protected")
public ResponseEntity<?> protectedEndpoint() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(Map.of(
"message", "这是一个受保护的API端点",
"user", authentication.getName(),
"authorities", authentication.getAuthorities()
));
}
// 需要特定权限的API端点
@GetMapping("/admin")
public ResponseEntity<?> adminEndpoint() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return ResponseEntity.ok(Map.of(
"message", "这是管理员专用API端点",
"user", authentication.getName()
));
}
// 生成JWT令牌
@PostMapping("/token")
public ResponseEntity<?> generateToken(@RequestBody TokenRequest request) {
try {
String token = jwtService.generateToken(
request.getUsername(),
request.getAuthorities()
);
String refreshToken = jwtService.generateRefreshToken(request.getUsername());
return ResponseEntity.ok(Map.of(
"access_token", token,
"refresh_token", refreshToken,
"token_type", "Bearer",
"expires_in", 3600
));
} catch (Exception e) {
return ResponseEntity.badRequest().body(Map.of(
"error", "Token generation failed",
"message", e.getMessage()
));
}
}
}
// 令牌请求DTO
class TokenRequest {
private String username;
private java.util.List<String> authorities;
public String getUsername() { return username; }
public void setUsername(String username) { this.username = username; }
public java.util.List<String> getAuthorities() { return authorities; }
public void setAuthorities(java.util.List<String> authorities) { this.authorities = authorities; }
}
✅ 第十部分:OAuth2安全配置与测试(60分钟)
1. 完整的OAuth2安全配置
CompleteOAuth2Config.java
// CompleteOAuth2Config.java
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserRequest;
import org.springframework.security.oauth2.client.oidc.userinfo.OidcUserService;
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
import org.springframework.security.oauth2.client.web.OAuth2LoginAuthenticationFilter;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
@Configuration
@EnableWebSecurity
public class CompleteOAuth2Config {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/", "/login", "/oauth2/**", "/api/public/**").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN")
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/dashboard", true)
.successHandler(authenticationSuccessHandler())
.userInfoEndpoint(userInfo -> userInfo
.oidcUserService(oidcUserService())
)
)
.oauth2ResourceServer(oauth2 -> oauth2
.jwt(jwt -> jwt
.jwtAuthenticationConverter(jwtAuthenticationConverter())
)
)
.logout(logout -> logout
.logoutSuccessUrl("/")
.invalidateHttpSession(true)
.clearAuthentication(true)
.deleteCookies("JSESSIONID")
);
return http.build();
}
@Bean
public AuthenticationSuccessHandler authenticationSuccessHandler() {
return new SimpleUrlAuthenticationSuccessHandler() {
@Override
public void onAuthenticationSuccess(HttpServletRequest request,
HttpServletResponse response,
org.springframework.security.core.Authentication authentication)
throws IOException, ServletException {
// 记录登录成功日志
System.out.println("OAuth2 login successful for user: " + authentication.getName());
// 可以在这里添加自定义逻辑
// 比如保存用户信息到数据库
super.onAuthenticationSuccess(request, response, authentication);
}
};
}
@Bean
public OidcUserService oidcUserService() {
return new OidcUserService() {
@Override
public OidcUser loadUser(OidcUserRequest userRequest) {
OidcUser oidcUser = super.loadUser(userRequest);
// 处理OIDC用户信息
processOidcUser(oidcUser);
return oidcUser;
}
};
}
@Bean
public org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter jwtAuthenticationConverter() {
org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter authoritiesConverter =
new org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter();
authoritiesConverter.setAuthorityPrefix("ROLE_");
authoritiesConverter.setAuthoritiesClaimName("authorities");
org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter converter =
new org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter();
converter.setJwtGrantedAuthoritiesConverter(authoritiesConverter);
return converter;
}
private void processOidcUser(OidcUser oidcUser) {
// 处理OIDC用户信息
System.out.println("Processing OIDC user: " + oidcUser.getName());
System.out.println("Email: " + oidcUser.getEmail());
System.out.println("Picture: " + oidcUser.getPicture());
}
}
2. OAuth2测试控制器
OAuth2TestController.java
// OAuth2TestController.java
package com.example.demo.controller;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
@RestController
public class OAuth2TestController {
@GetMapping("/")
public Map<String, Object> home() {
return Map.of(
"message", "欢迎来到OAuth2演示应用",
"endpoints", Map.of(
"login", "/login",
"user", "/user",
"protected", "/api/oauth2/protected",
"admin", "/api/oauth2/admin"
)
);
}
@GetMapping("/dashboard")
public Map<String, Object> dashboard() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
return Map.of(
"message", "欢迎来到仪表板",
"user", authentication.getName(),
"authorities", authentication.getAuthorities(),
"authenticated", authentication.isAuthenticated()
);
}
@GetMapping("/user")
public Map<String, Object> user() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication.getPrincipal() instanceof OidcUser) {
OidcUser oidcUser = (OidcUser) authentication.getPrincipal();
return Map.of(
"type", "OIDC",
"name", oidcUser.getName(),
"email", oidcUser.getEmail(),
"picture", oidcUser.getPicture(),
"claims", oidcUser.getClaims()
);
} else if (authentication.getPrincipal() instanceof OAuth2User) {
OAuth2User oauth2User = (OAuth2User) authentication.getPrincipal();
return Map.of(
"type", "OAuth2",
"name", oauth2User.getName(),
"attributes", oauth2User.getAttributes()
);
}
return Map.of("error", "No OAuth2 user found");
}
}
3. OAuth2测试用例
OAuth2Test.java
// OAuth2Test.java
package com.example.demo.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.security.test.context.support.WithMockUser;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.security.test.web.servlet.setup.SecurityMockMvcConfigurers.springSecurity;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureWebMvc
public class OAuth2Test {
@Autowired
private WebApplicationContext context;
private MockMvc mockMvc;
@Test
public void testHomePage() throws Exception {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
mockMvc.perform(get("/"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("欢迎来到OAuth2演示应用"));
}
@Test
@WithMockUser(roles = "USER")
public void testProtectedEndpoint() throws Exception {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
mockMvc.perform(get("/api/oauth2/protected"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("这是一个受保护的API端点"));
}
@Test
@WithMockUser(roles = "ADMIN")
public void testAdminEndpoint() throws Exception {
mockMvc = MockMvcBuilders
.webAppContextSetup(context)
.apply(springSecurity())
.build();
mockMvc.perform(get("/api/oauth2/admin"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.message").value("这是管理员专用API端点"));
}
}
OAuth2学习总结
1. OAuth2核心优势
- ✅ 安全性:无需共享用户密码
- ✅ 标准化:统一的授权协议
- ✅ 灵活性:支持多种授权模式
- ✅ 可扩展性:支持各种客户端类型
2. 四种授权模式对比
授权码模式:最安全,适用于Web应用
简化模式:适用于SPA应用
密码模式:适用于高度信任的客户端
客户端凭证:适用于服务端通信
3. Spring Security OAuth2特性
- 授权服务器:内置OAuth2授权服务器
- 资源服务器:JWT令牌验证
- 客户端支持:多种OAuth2提供商
- 安全配置:灵活的安全规则
4. 实际应用场景
- 社交登录(Google、GitHub、Facebook)
- API授权访问
- 微服务间认证
- 第三方应用集成
5. 安全最佳实践
- 使用HTTPS传输
- 验证state参数防CSRF
- 设置合理的令牌过期时间
- 定期轮换客户端密钥
- 记录安全审计日志
🎯 今日学习总结
1. 掌握的核心技能
- ✅ Spring Security安全框架基础
- ✅ JWT令牌生成与验证
- ✅ 用户认证与授权机制
- ✅ 基于角色的访问控制
- ✅ 安全配置与过滤器
2. 安全体系特点
- JWT认证:无状态、可扩展的认证方式
- 角色授权:基于角色的细粒度权限控制
- 安全配置:灵活的安全规则配置
- CORS支持:跨域请求安全处理
- 密码加密:BCrypt安全哈希算法
3. API安全层次
公开接口:/api/auth/**, /api/public/**
用户接口:/api/user/** (需要USER角色)
管理接口:/api/admin/** (需要ADMIN角色)
4. 下一步学习方向
- 数据库事务管理
- 微服务安全架构
- OAuth2认证授权
- API网关安全
- 安全审计与日志
学习建议
- 安全测试:使用Postman测试不同角色的接口访问
- 令牌管理:理解JWT的生命周期管理
- 权限设计:学会设计合理的角色权限体系
- 安全配置:掌握Spring Security的配置选项
- 问题排查:学会使用日志排查安全相关问题