搜 索

java系统安全框架

  • 212阅读
  • 2023年01月01日
  • 0评论
首页 / 编程 / 正文

前言:为什么你需要读这篇文章

还记得那个深夜吗?你的系统被人用 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 SecurityApache ShiroSa-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

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

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: true

4.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,email

5.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[轻量灵活
社区一般]

选型总结

场景推荐框架理由
Spring Boot企业项目Spring Security官方支持,功能全面
快速原型/中小项目Sa-Token上手快,中文文档友好
非Spring项目Apache Shiro框架无关,轻量灵活
需要OAuth2/OIDCSpring Security原生支持最完善
微服务架构Sa-Token / Spring Security都有微服务方案
学习入门Sa-Token门槛最低,概念清晰

八、总结

安全框架就像保险,平时觉得没用,出事了追悔莫及。

核心要点:

  1. 认证和授权是两码事:先确认你是谁,再决定你能干啥
  2. 选择适合的框架:别用牛刀杀鸡,也别用水果刀砍牛
  3. 安全是系统工程:框架只是一部分,还需要配合其他安全措施
  4. 持续学习:安全领域日新月异,今天的最佳实践可能明天就过时了

最后,无论选择哪个框架,记住这句话:

"最大的安全漏洞永远是人。" —— 鲁迅(可能没说过)

参考资料

评论区
暂无评论
avatar