前言:为什么你需要读这篇文章
还记得那个深夜吗?你的系统被人用 admin/123456 登录了,然后删库跑路。老板问你:"我们的安全措施呢?"你支支吾吾地说:"我以为用户会自觉的..."
朋友,醒醒吧。
本文将带你深入了解Java世界的三大安全框架:Spring Security(官方嫡长子)、Apache Shiro(轻量级老将)、Sa-Token(国产新秀)。看完之后,你至少能在老板面前假装很懂安全。
一、安全框架核心概念
在开始之前,先搞清楚几个灵魂拷问:
1.1 认证 vs 授权
认证(Authentication):你是谁?—— 验明正身
授权(Authorization):你能干啥?—— 权限控制打个比方:
- 认证:门卫检查你的工牌,确认你是公司员工
- 授权:你虽然是员工,但你不能进财务室拿钱
1.2 安全框架核心架构
flowchart TB
subgraph 客户端
A[用户请求]
end
subgraph 安全框架
B[认证过滤器链]
C[身份认证模块]
D[授权决策模块]
E[会话管理]
F[密码加密]
end
subgraph 业务系统
G[Controller]
H[Service]
I[数据库]
end
A --> B
B --> C
C -->|认证成功| D
C -->|认证失败| J[返回401]
D -->|授权通过| G
D -->|授权失败| K[返回403]
G --> H --> I
C <--> E
C <--> F
1.3 主流安全框架对比
| 特性 | Spring Security | Apache Shiro | Sa-Token |
|---|---|---|---|
| 学习曲线 | 🔥🔥🔥🔥🔥 陡峭如珠峰 | 🔥🔥🔥 相对平缓 | 🔥🔥 新手友好 |
| 功能完整度 | ⭐⭐⭐⭐⭐ | ⭐⭐⭐⭐ | ⭐⭐⭐⭐ |
| Spring集成 | 原生支持 | 需要适配 | 良好支持 |
| 社区活跃度 | 非常活跃 | 一般 | 国内活跃 |
| OAuth2支持 | 原生支持 | 需要扩展 | 内置支持 |
| 文档质量 | 英文为主,较全 | 一般 | 中文友好 |
| 适用场景 | 企业级复杂系统 | 中小型项目 | 快速开发 |
二、Spring Security:官方嫡长子
2.1 简介
Spring Security 是 Spring 家族的亲儿子,功能强大到令人发指,配置复杂到令人放弃。但没办法,谁让人家是正统呢?
"Spring Security 的学习曲线不是曲线,是悬崖。" —— 某不愿透露姓名的秃头程序员
2.2 核心架构
flowchart TB
subgraph FilterChain[安全过滤器链]
F1[SecurityContextPersistenceFilter
安全上下文持久化] F2[UsernamePasswordAuthenticationFilter
用户名密码认证] F3[BasicAuthenticationFilter
Basic认证] F4[ExceptionTranslationFilter
异常转换] F5[FilterSecurityInterceptor
授权拦截器] end subgraph Core[核心组件] A[AuthenticationManager
认证管理器] P[AuthenticationProvider
认证提供者] U[UserDetailsService
用户详情服务] E[PasswordEncoder
密码编码器] end subgraph Authorization[授权组件] AD[AccessDecisionManager
访问决策管理器] V1[AffirmativeBased
一票通过] V2[ConsensusBased
少数服从多数] V3[UnanimousBased
一票否决] end F1 --> F2 --> F3 --> F4 --> F5 F2 --> A A --> P P --> U P --> E F5 --> AD AD --> V1 AD --> V2 AD --> V3
安全上下文持久化] F2[UsernamePasswordAuthenticationFilter
用户名密码认证] F3[BasicAuthenticationFilter
Basic认证] F4[ExceptionTranslationFilter
异常转换] F5[FilterSecurityInterceptor
授权拦截器] end subgraph Core[核心组件] A[AuthenticationManager
认证管理器] P[AuthenticationProvider
认证提供者] U[UserDetailsService
用户详情服务] E[PasswordEncoder
密码编码器] end subgraph Authorization[授权组件] AD[AccessDecisionManager
访问决策管理器] V1[AffirmativeBased
一票通过] V2[ConsensusBased
少数服从多数] V3[UnanimousBased
一票否决] end F1 --> F2 --> F3 --> F4 --> F5 F2 --> A A --> P P --> U P --> E F5 --> AD AD --> V1 AD --> V2 AD --> V3
2.3 快速入门
2.3.1 添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>恭喜你!加完这一行,你的所有接口都需要登录了。是不是很惊喜?😏
2.3.2 基础配置(Spring Boot 3.x 风格)
@Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启方法级别安全
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 关闭CSRF(前后端分离项目通常关闭)
.csrf(csrf -> csrf.disable())
// 配置请求授权规则
.authorizeHttpRequests(auth -> auth
// 放行登录、注册接口
.requestMatchers("/auth/**", "/public/**").permitAll()
// Swagger放行
.requestMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()
// 管理员接口
.requestMatchers("/admin/**").hasRole("ADMIN")
// 其他请求需要认证
.anyRequest().authenticated()
)
// 配置表单登录
.formLogin(form -> form
.loginProcessingUrl("/auth/login")
.successHandler(authenticationSuccessHandler())
.failureHandler(authenticationFailureHandler())
)
// 配置登出
.logout(logout -> logout
.logoutUrl("/auth/logout")
.logoutSuccessHandler(logoutSuccessHandler())
)
// 异常处理
.exceptionHandling(ex -> ex
.authenticationEntryPoint(authenticationEntryPoint())
.accessDeniedHandler(accessDeniedHandler())
)
// Session配置(无状态)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 添加JWT过滤器
http.addFilterBefore(jwtAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration config) throws Exception {
return config.getAuthenticationManager();
}
}2.3.3 自定义 UserDetailsService
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
// 从数据库查询用户
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException(
"用户不存在: " + username));
// 构建Spring Security的UserDetails
return org.springframework.security.core.userdetails.User
.withUsername(user.getUsername())
.password(user.getPassword())
.authorities(user.getRoles().stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role.getName()))
.collect(Collectors.toList()))
.accountExpired(false)
.accountLocked(user.isLocked())
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
}2.3.4 JWT 工具类
@Component
public class JwtTokenProvider {
@Value("${jwt.secret}")
private String jwtSecret;
@Value("${jwt.expiration}")
private long jwtExpiration;
/**
* 生成JWT Token
*/
public String generateToken(Authentication authentication) {
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
Date now = new Date();
Date expiryDate = new Date(now.getTime() + jwtExpiration);
return Jwts.builder()
.setSubject(userDetails.getUsername())
.claim("roles", userDetails.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.toList()))
.setIssuedAt(now)
.setExpiration(expiryDate)
.signWith(getSigningKey(), SignatureAlgorithm.HS512)
.compact();
}
/**
* 从Token中获取用户名
*/
public String getUsernameFromToken(String token) {
Claims claims = Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token)
.getBody();
return claims.getSubject();
}
/**
* 验证Token
*/
public boolean validateToken(String token) {
try {
Jwts.parserBuilder()
.setSigningKey(getSigningKey())
.build()
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
log.error("Invalid JWT token: {}", e.getMessage());
return false;
}
}
private Key getSigningKey() {
byte[] keyBytes = Decoders.BASE64.decode(jwtSecret);
return Keys.hmacShaKeyFor(keyBytes);
}
}2.3.5 JWT 认证过滤器
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {
private final JwtTokenProvider tokenProvider;
private final CustomUserDetailsService userDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain)
throws ServletException, IOException {
try {
String jwt = getJwtFromRequest(request);
if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
String username = tokenProvider.getUsernameFromToken(jwt);
UserDetails userDetails = userDetailsService
.loadUserByUsername(username);
UsernamePasswordAuthenticationToken authentication =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
authentication.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
// 将认证信息放入Security上下文
SecurityContextHolder.getContext()
.setAuthentication(authentication);
}
} catch (Exception e) {
log.error("无法设置用户认证: {}", e.getMessage());
}
filterChain.doFilter(request, response);
}
private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring(7);
}
return null;
}
}2.4 方法级别权限控制
@RestController
@RequestMapping("/api/users")
@RequiredArgsConstructor
public class UserController {
private final UserService userService;
// 需要ADMIN角色
@PreAuthorize("hasRole('ADMIN')")
@GetMapping
public List<UserDTO> getAllUsers() {
return userService.findAll();
}
// 需要USER或ADMIN角色
@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
@GetMapping("/{id}")
public UserDTO getUser(@PathVariable Long id) {
return userService.findById(id);
}
// 只能访问自己的信息,或者是管理员
@PreAuthorize("#id == authentication.principal.id or hasRole('ADMIN')")
@PutMapping("/{id}")
public UserDTO updateUser(@PathVariable Long id, @RequestBody UserDTO dto) {
return userService.update(id, dto);
}
// 基于权限(Permission)的控制
@PreAuthorize("hasAuthority('user:delete')")
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable Long id) {
userService.delete(id);
}
// 方法执行后校验返回值
@PostAuthorize("returnObject.createdBy == authentication.name")
@GetMapping("/my-data/{id}")
public DataDTO getMyData(@PathVariable Long id) {
return dataService.findById(id);
}
}2.5 Spring Security 认证流程详解
sequenceDiagram
participant C as 客户端
participant F as JwtAuthFilter
participant SC as SecurityContext
participant AM as AuthenticationManager
participant AP as AuthenticationProvider
participant UDS as UserDetailsService
participant DB as 数据库
C->>F: 1. 携带Token请求
F->>F: 2. 解析Token获取username
F->>UDS: 3. loadUserByUsername()
UDS->>DB: 4. 查询用户
DB-->>UDS: 5. 返回用户信息
UDS-->>F: 6. 返回UserDetails
F->>SC: 7. 设置Authentication
SC-->>C: 8. 请求继续执行
Note over C,DB: 登录流程
C->>AM: 9. authenticate(用户名,密码)
AM->>AP: 10. 委托认证
AP->>UDS: 11. 加载用户
UDS->>DB: 12. 查询
DB-->>UDS: 13. 用户数据
UDS-->>AP: 14. UserDetails
AP->>AP: 15. 密码比对
AP-->>AM: 16. Authentication
AM-->>C: 17. 认证成功,返回Token
三、Apache Shiro:轻量级老将
3.1 简介
Apache Shiro 是一个老牌安全框架,比 Spring Security 更轻量,学习曲线更平缓。如果你觉得 Spring Security 太重了,Shiro 是个不错的选择。
"Shiro 的 API 设计得像人话。" —— 被 Spring Security 虐过的程序员
3.2 核心架构
flowchart TB
subgraph Subject[Subject 当前用户]
S[Subject]
end
subgraph SecurityManager[SecurityManager 安全管理器]
SM[SecurityManager]
AU[Authenticator
认证器] AZ[Authorizer
授权器] SEM[SessionManager
会话管理器] end subgraph Realms[Realm 数据源] R1[JdbcRealm] R2[LdapRealm] R3[CustomRealm] end subgraph Support[支撑组件] CC[CacheManager
缓存管理] CR[Cryptography
加密] end S --> SM SM --> AU SM --> AZ SM --> SEM AU --> R1 AU --> R2 AU --> R3 AZ --> R1 AZ --> R2 AZ --> R3 SM --> CC SM --> CR
认证器] AZ[Authorizer
授权器] SEM[SessionManager
会话管理器] end subgraph Realms[Realm 数据源] R1[JdbcRealm] R2[LdapRealm] R3[CustomRealm] end subgraph Support[支撑组件] CC[CacheManager
缓存管理] CR[Cryptography
加密] end S --> SM SM --> AU SM --> AZ SM --> SEM AU --> R1 AU --> R2 AU --> R3 AZ --> R1 AZ --> R2 AZ --> R3 SM --> CC SM --> CR
3.3 快速入门
3.3.1 添加依赖
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-starter</artifactId>
<version>1.13.0</version>
</dependency>3.3.2 自定义 Realm
public class CustomRealm extends AuthorizingRealm {
@Autowired
private UserService userService;
/**
* 授权(权限验证)
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(
PrincipalCollection principals) {
String username = (String) principals.getPrimaryPrincipal();
User user = userService.findByUsername(username);
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
// 设置角色
Set<String> roles = user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toSet());
info.setRoles(roles);
// 设置权限
Set<String> permissions = user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getCode)
.collect(Collectors.toSet());
info.setStringPermissions(permissions);
return info;
}
/**
* 认证(身份验证)
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(
AuthenticationToken token) throws AuthenticationException {
String username = (String) token.getPrincipal();
User user = userService.findByUsername(username);
if (user == null) {
throw new UnknownAccountException("用户不存在");
}
if (user.isLocked()) {
throw new LockedAccountException("账户已锁定");
}
// 返回认证信息,Shiro会自动比对密码
return new SimpleAuthenticationInfo(
user.getUsername(),
user.getPassword(),
ByteSource.Util.bytes(user.getSalt()), // 盐值
getName()
);
}
}3.3.3 Shiro 配置
@Configuration
public class ShiroConfig {
@Bean
public CustomRealm customRealm() {
CustomRealm realm = new CustomRealm();
// 设置密码匹配器
HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();
matcher.setHashAlgorithmName("SHA-256");
matcher.setHashIterations(1024);
realm.setCredentialsMatcher(matcher);
return realm;
}
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setRealm(customRealm());
manager.setSessionManager(sessionManager());
manager.setCacheManager(cacheManager());
return manager;
}
@Bean
public ShiroFilterFactoryBean shiroFilter() {
ShiroFilterFactoryBean filter = new ShiroFilterFactoryBean();
filter.setSecurityManager(securityManager());
// 登录页面
filter.setLoginUrl("/login");
// 未授权页面
filter.setUnauthorizedUrl("/403");
// 过滤器链配置
Map<String, String> filterChainMap = new LinkedHashMap<>();
filterChainMap.put("/static/**", "anon"); // 静态资源放行
filterChainMap.put("/login", "anon"); // 登录放行
filterChainMap.put("/logout", "logout"); // 登出
filterChainMap.put("/admin/**", "roles[admin]"); // admin角色
filterChainMap.put("/**", "authc"); // 其他需要认证
filter.setFilterChainDefinitionMap(filterChainMap);
return filter;
}
@Bean
public SessionManager sessionManager() {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
manager.setSessionIdUrlRewritingEnabled(false);
manager.setGlobalSessionTimeout(1800000); // 30分钟
return manager;
}
@Bean
public CacheManager cacheManager() {
return new MemoryConstrainedCacheManager();
}
// 开启注解支持
@Bean
public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() {
AuthorizationAttributeSourceAdvisor advisor =
new AuthorizationAttributeSourceAdvisor();
advisor.setSecurityManager(securityManager());
return advisor;
}
}3.3.4 使用示例
@RestController
@RequestMapping("/api")
public class ApiController {
/**
* 登录
*/
@PostMapping("/login")
public Result login(@RequestBody LoginRequest request) {
Subject subject = SecurityUtils.getSubject();
UsernamePasswordToken token = new UsernamePasswordToken(
request.getUsername(),
request.getPassword(),
request.isRememberMe()
);
try {
subject.login(token);
return Result.success("登录成功");
} catch (UnknownAccountException e) {
return Result.fail("用户不存在");
} catch (IncorrectCredentialsException e) {
return Result.fail("密码错误");
} catch (LockedAccountException e) {
return Result.fail("账户已锁定");
}
}
/**
* 需要认证
*/
@RequiresAuthentication
@GetMapping("/user/info")
public Result getUserInfo() {
Subject subject = SecurityUtils.getSubject();
String username = (String) subject.getPrincipal();
return Result.success(userService.findByUsername(username));
}
/**
* 需要角色
*/
@RequiresRoles("admin")
@GetMapping("/admin/users")
public Result listUsers() {
return Result.success(userService.findAll());
}
/**
* 需要权限
*/
@RequiresPermissions("user:delete")
@DeleteMapping("/user/{id}")
public Result deleteUser(@PathVariable Long id) {
userService.delete(id);
return Result.success();
}
/**
* 多个角色(满足其一)
*/
@RequiresRoles(value = {"admin", "manager"}, logical = Logical.OR)
@GetMapping("/reports")
public Result getReports() {
return Result.success(reportService.findAll());
}
}3.4 Shiro 过滤器说明
| 过滤器 | 说明 | 示例 |
|---|---|---|
anon | 匿名访问,不需要认证 | /public/** |
authc | 需要认证 | /** |
logout | 登出过滤器 | /logout |
roles | 需要指定角色 | roles[admin] |
perms | 需要指定权限 | perms[user:read] |
user | 已认证或RememberMe | /user/** |
ssl | 需要HTTPS | /secure/** |
四、Sa-Token:国产新秀
4.1 简介
Sa-Token 是一个轻量级 Java 权限认证框架,主打的就是一个简单。如果你厌倦了 Spring Security 的繁琐配置,Sa-Token 绝对能让你眼前一亮。
"终于有一个看得懂的安全框架了!" —— 刚从 Spring Security 逃出来的程序员
4.2 核心特性
mindmap
root((Sa-Token))
登录认证
多端登录
单点登录SSO
OAuth2.0
记住我
权限认证
角色认证
权限认证
会话查询
踢人下线
Session会话
Token风格
自动续签
会话治理
其他功能
微服务鉴权
临时Token
二级认证
密码加密
4.3 快速入门
4.3.1 添加依赖
<!-- Sa-Token 权限认证 -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-spring-boot3-starter</artifactId>
<version>1.38.0</version>
</dependency>
<!-- Sa-Token 整合 Redis(可选) -->
<dependency>
<groupId>cn.dev33</groupId>
<artifactId>sa-token-redis-jackson</artifactId>
<version>1.38.0</version>
</dependency>4.3.2 配置文件
sa-token:
# Token名称(同时也是Cookie名称)
token-name: Authorization
# Token有效期(秒),-1代表永不过期
timeout: 86400
# Token临时有效期(指定时间内无操作则过期)
active-timeout: 1800
# 是否允许同一账号并发登录
is-concurrent: true
# 同一账号最大登录数量
max-login-count: 5
# Token风格
token-style: uuid
# 是否输出操作日志
is-log: true4.3.3 登录认证
@RestController
@RequestMapping("/auth")
public class AuthController {
@Autowired
private UserService userService;
/**
* 登录 —— 就这么简单!
*/
@PostMapping("/login")
public SaResult login(@RequestBody LoginRequest req) {
User user = userService.findByUsername(req.getUsername());
if (user == null) {
return SaResult.error("用户不存在");
}
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
return SaResult.error("密码错误");
}
// 登录:一行代码搞定!
StpUtil.login(user.getId());
// 返回Token
return SaResult.data(StpUtil.getTokenValue());
}
/**
* 登出
*/
@PostMapping("/logout")
public SaResult logout() {
StpUtil.logout();
return SaResult.ok("登出成功");
}
/**
* 查询登录状态
*/
@GetMapping("/is-login")
public SaResult isLogin() {
return SaResult.ok("是否登录: " + StpUtil.isLogin());
}
/**
* 获取当前登录用户信息
*/
@GetMapping("/info")
public SaResult info() {
// 获取当前登录用户ID
long userId = StpUtil.getLoginIdAsLong();
User user = userService.findById(userId);
return SaResult.data(user);
}
}4.3.4 权限认证
首先,实现权限接口:
@Component
public class StpInterfaceImpl implements StpInterface {
@Autowired
private UserService userService;
/**
* 返回用户的权限列表
*/
@Override
public List<String> getPermissionList(Object loginId, String loginType) {
Long userId = Long.parseLong(loginId.toString());
User user = userService.findById(userId);
return user.getRoles().stream()
.flatMap(role -> role.getPermissions().stream())
.map(Permission::getCode)
.distinct()
.collect(Collectors.toList());
}
/**
* 返回用户的角色列表
*/
@Override
public List<String> getRoleList(Object loginId, String loginType) {
Long userId = Long.parseLong(loginId.toString());
User user = userService.findById(userId);
return user.getRoles().stream()
.map(Role::getName)
.collect(Collectors.toList());
}
}然后愉快地使用:
@RestController
@RequestMapping("/api")
public class ApiController {
/**
* 角色校验
*/
@GetMapping("/admin/data")
public SaResult adminData() {
// 校验当前用户是否有admin角色
StpUtil.checkRole("admin");
return SaResult.data("管理员数据");
}
/**
* 权限校验
*/
@GetMapping("/user/delete")
public SaResult deleteUser() {
// 校验当前用户是否有user:delete权限
StpUtil.checkPermission("user:delete");
return SaResult.ok("删除成功");
}
/**
* 多角色校验(满足其一)
*/
@GetMapping("/reports")
public SaResult reports() {
StpUtil.checkRoleOr("admin", "manager", "analyst");
return SaResult.data("报表数据");
}
/**
* 多权限校验(全部满足)
*/
@GetMapping("/sensitive/operation")
public SaResult sensitiveOp() {
StpUtil.checkPermissionAnd("data:read", "data:write", "data:export");
return SaResult.ok("敏感操作执行成功");
}
}4.3.5 注解鉴权
@RestController
@RequestMapping("/api")
public class AnnotationController {
// 登录校验
@SaCheckLogin
@GetMapping("/user/info")
public SaResult userInfo() {
return SaResult.data("用户信息");
}
// 角色校验
@SaCheckRole("admin")
@GetMapping("/admin/settings")
public SaResult adminSettings() {
return SaResult.data("管理员设置");
}
// 权限校验
@SaCheckPermission("user:add")
@PostMapping("/user")
public SaResult addUser() {
return SaResult.ok("添加成功");
}
// 多角色校验(mode = OR 表示满足其一)
@SaCheckRole(value = {"admin", "manager"}, mode = SaMode.OR)
@GetMapping("/dashboard")
public SaResult dashboard() {
return SaResult.data("仪表盘数据");
}
// 二级认证(敏感操作前需要再次验证)
@SaCheckSafe
@PostMapping("/transfer")
public SaResult transfer() {
return SaResult.ok("转账成功");
}
}4.4 高级功能
4.4.1 多端登录
// 在不同设备登录时,指定设备类型
StpUtil.login(10001, "PC"); // PC端登录
StpUtil.login(10001, "APP"); // APP端登录
StpUtil.login(10001, "WeChat"); // 微信小程序登录
// 踢掉指定设备
StpUtil.logout(10001, "PC");
// 踢掉所有设备
StpUtil.logout(10001);4.4.2 踢人下线
// 踢人下线(根据用户ID)
StpUtil.kickout(10001);
// 踢人下线(根据Token)
StpUtil.kickoutByTokenValue("xxxx-xxxx-xxxx");
// 获取某用户的所有Token
List<String> tokens = StpUtil.getTokenValueListByLoginId(10001);4.4.3 同端互斥登录
sa-token:
# 同端互斥:同一设备类型只能登录一个
is-concurrent: false
# 当登录数量超出时,是否踢出旧Token
is-share: false五、OAuth 2.0 集成
5.1 OAuth 2.0 简介
OAuth 2.0 是一个开放授权标准,允许用户授权第三方应用访问其资源,而无需暴露密码。
sequenceDiagram
participant U as 用户
participant C as 客户端应用
participant A as 授权服务器
participant R as 资源服务器
U->>C: 1. 点击"使用GitHub登录"
C->>A: 2. 重定向到授权页面
A->>U: 3. 显示授权页面
U->>A: 4. 用户同意授权
A->>C: 5. 返回授权码(code)
C->>A: 6. 用授权码换取Token
A->>C: 7. 返回Access Token
C->>R: 8. 携带Token请求资源
R->>C: 9. 返回受保护资源
C->>U: 10. 显示资源
5.2 Spring Security OAuth2 客户端
@Configuration
@EnableWebSecurity
public class OAuth2ClientConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login/**").permitAll()
.anyRequest().authenticated()
)
.oauth2Login(oauth2 -> oauth2
.loginPage("/login")
.defaultSuccessUrl("/home")
.userInfoEndpoint(userInfo -> userInfo
.userService(customOAuth2UserService())
)
);
return http.build();
}
@Bean
public OAuth2UserService<OAuth2UserRequest, OAuth2User> customOAuth2UserService() {
return new CustomOAuth2UserService();
}
}配置文件:
spring:
security:
oauth2:
client:
registration:
github:
client-id: your-client-id
client-secret: your-client-secret
scope: read:user,user:email
google:
client-id: your-google-client-id
client-secret: your-google-client-secret
scope: openid,profile,email5.3 Sa-Token OAuth2 服务端
@Configuration
public class SaOAuth2Config {
@Autowired
public void configOAuth2Server(SaOAuth2Config oauth2Config) {
// 配置OAuth2模式
oauth2Config
// 授权码模式
.setEnableAuthorizationCode(true)
// 隐式模式
.setEnableImplicit(true)
// 密码模式
.setEnablePassword(true)
// 客户端模式
.setEnableClientCredentials(true);
}
}
@RestController
@RequestMapping("/oauth2")
public class OAuth2Controller {
// 授权端点
@RequestMapping("/authorize")
public Object authorize() {
return SaOAuth2Handle.serverRequest();
}
// Token端点
@RequestMapping("/token")
public Object token() {
return SaOAuth2Handle.serverRequest();
}
}六、安全最佳实践
6.1 密码安全
@Component
public class PasswordHelper {
// 使用BCrypt(推荐)
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
/**
* 加密密码
*/
public String encode(String rawPassword) {
return encoder.encode(rawPassword);
}
/**
* 验证密码
*/
public boolean matches(String rawPassword, String encodedPassword) {
return encoder.matches(rawPassword, encodedPassword);
}
/**
* 密码强度检查
*/
public boolean isStrongPassword(String password) {
// 至少8位,包含大小写字母、数字、特殊字符
String pattern = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,}$";
return password.matches(pattern);
}
}6.2 防止常见攻击
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF保护(前后端分离可以关闭,但要使用其他方式)
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// 安全响应头
.headers(headers -> headers
// 防止点击劫持
.frameOptions(frame -> frame.deny())
// XSS保护
.xssProtection(xss -> xss.enable())
// 内容类型嗅探
.contentTypeOptions(content -> {})
// HSTS
.httpStrictTransportSecurity(hsts -> hsts
.includeSubDomains(true)
.maxAgeInSeconds(31536000)
)
)
// 其他配置...
;
return http.build();
}
}6.3 登录安全策略
@Service
@RequiredArgsConstructor
public class LoginSecurityService {
private final StringRedisTemplate redisTemplate;
private static final int MAX_ATTEMPTS = 5;
private static final int LOCK_MINUTES = 30;
/**
* 检查是否被锁定
*/
public boolean isLocked(String username) {
String key = "login:locked:" + username;
return Boolean.TRUE.equals(redisTemplate.hasKey(key));
}
/**
* 记录失败次数
*/
public void recordFailure(String username) {
String key = "login:attempts:" + username;
Long attempts = redisTemplate.opsForValue().increment(key);
redisTemplate.expire(key, LOCK_MINUTES, TimeUnit.MINUTES);
if (attempts != null && attempts >= MAX_ATTEMPTS) {
lock(username);
}
}
/**
* 锁定账户
*/
public void lock(String username) {
String key = "login:locked:" + username;
redisTemplate.opsForValue().set(key, "1", LOCK_MINUTES, TimeUnit.MINUTES);
}
/**
* 清除失败记录(登录成功后调用)
*/
public void clearFailures(String username) {
redisTemplate.delete("login:attempts:" + username);
redisTemplate.delete("login:locked:" + username);
}
}6.4 敏感数据脱敏
public class DataMasker {
/**
* 手机号脱敏:138****8888
*/
public static String maskPhone(String phone) {
if (phone == null || phone.length() != 11) {
return phone;
}
return phone.substring(0, 3) + "****" + phone.substring(7);
}
/**
* 身份证脱敏:110***********1234
*/
public static String maskIdCard(String idCard) {
if (idCard == null || idCard.length() < 8) {
return idCard;
}
return idCard.substring(0, 3) + "***********" + idCard.substring(idCard.length() - 4);
}
/**
* 邮箱脱敏:t***@example.com
*/
public static String maskEmail(String email) {
if (email == null || !email.contains("@")) {
return email;
}
int atIndex = email.indexOf("@");
if (atIndex <= 1) {
return email;
}
return email.charAt(0) + "***" + email.substring(atIndex);
}
/**
* 银行卡脱敏:**** **** **** 1234
*/
public static String maskBankCard(String cardNo) {
if (cardNo == null || cardNo.length() < 4) {
return cardNo;
}
return "**** **** **** " + cardNo.substring(cardNo.length() - 4);
}
}七、框架选型建议
flowchart TD
A[项目类型?] --> B{企业级复杂系统?}
B -->|是| C[Spring Security]
B -->|否| D{需要快速开发?}
D -->|是| E[Sa-Token]
D -->|否| F{团队熟悉Shiro?}
F -->|是| G[Apache Shiro]
F -->|否| H{有Spring环境?}
H -->|是| I[Spring Security
或 Sa-Token] H -->|否| J[Apache Shiro] C --> K[功能最全,生态最好
学习成本高] E --> L[简单易用,国内友好
功能够用] G --> M[轻量灵活
社区一般]
或 Sa-Token] H -->|否| J[Apache Shiro] C --> K[功能最全,生态最好
学习成本高] E --> L[简单易用,国内友好
功能够用] G --> M[轻量灵活
社区一般]
选型总结
| 场景 | 推荐框架 | 理由 |
|---|---|---|
| Spring Boot企业项目 | Spring Security | 官方支持,功能全面 |
| 快速原型/中小项目 | Sa-Token | 上手快,中文文档友好 |
| 非Spring项目 | Apache Shiro | 框架无关,轻量灵活 |
| 需要OAuth2/OIDC | Spring Security | 原生支持最完善 |
| 微服务架构 | Sa-Token / Spring Security | 都有微服务方案 |
| 学习入门 | Sa-Token | 门槛最低,概念清晰 |
八、总结
安全框架就像保险,平时觉得没用,出事了追悔莫及。
核心要点:
- 认证和授权是两码事:先确认你是谁,再决定你能干啥
- 选择适合的框架:别用牛刀杀鸡,也别用水果刀砍牛
- 安全是系统工程:框架只是一部分,还需要配合其他安全措施
- 持续学习:安全领域日新月异,今天的最佳实践可能明天就过时了
最后,无论选择哪个框架,记住这句话:
"最大的安全漏洞永远是人。" —— 鲁迅(可能没说过)