1. 项目概述:为什么我们需要盘点登录与鉴权框架?
在任何一个需要区分用户身份、控制资源访问的应用里,登录和鉴权都是绕不开的基石。简单来说,登录解决“你是谁”的问题,而鉴权则回答“你能做什么”。我见过太多项目,初期为了快速上线,用几行简单的用户名密码校验就草草了事,结果随着业务扩张,权限体系变得一团乱麻,代码里到处是if-else判断,维护起来苦不堪言。更别提安全漏洞了,一个不小心,用户数据就可能暴露。
所以,今天我们不聊具体的业务代码,而是把目光投向那些经过社区千锤百炼、能帮我们系统化解决这些问题的“轮子”——开源登录及权限认证框架。无论是单体应用还是微服务架构,无论是传统的网页表单登录还是时髦的微信扫码、JWT令牌,选对一个合适的框架,就等于为你的应用安全性和可维护性打下了坚实的地基。这次盘点,我会结合自己这些年踩过的坑和实战经验,带你梳理几个主流框架的核心思想、适用场景和那些“坑爹”的细节,目标是让你看完后,能根据自己项目的实际情况,做出最合适的技术选型。
2. 核心概念辨析:认证、授权、鉴权与权限
在深入框架之前,我们必须先把几个容易混淆的概念理清楚。很多开发者,甚至一些文档,都会混用这些术语,但这会导致我们在设计和沟通时出现偏差。
2.1 认证:证明你是你
认证,英文是Authentication,简称AuthN。它的核心任务是验证主体的身份。这个“主体”通常就是用户。最常见的例子就是输入用户名和密码。系统核对密码是否正确,就是在完成认证。除了密码,还有手机验证码、指纹、人脸识别、第三方登录(微信、GitHub)等,这些都是认证的手段。认证成功后,系统会建立一个会话(Session)或颁发一个令牌(Token),用来在后续请求中标识这个已认证的用户。所以,认证回答的问题是:“你是否是系统所声称的那个用户?”
2.2 授权与权限:你能做什么
授权,英文是Authorization,简称AuthZ。它发生在认证之后。当系统知道“你是谁”之后,授权要决定“你被允许做什么”。权限则是授权的具体体现,是访问特定资源或执行特定操作的许可。
这里通常分为两个层次:
- 访问控制:粗粒度地控制用户能否进入某个功能模块或页面。例如,普通用户不能访问后台管理页面。
- 权限控制:细粒度地控制用户对具体数据或操作的权利。例如,部门经理只能审批本部门的报销单,而不能审批其他部门的。
2.3 鉴权:检查的过程
鉴权这个词在中文语境里有点特殊,它有时是认证和授权的统称,有时特指授权检查的过程。在技术框架中,我们通常把它理解为“权限鉴定”的过程,即系统在用户试图执行某个操作或访问某个资源时,根据用户的身份和角色,去检查他是否拥有相应的权限。这个过程就是鉴权。所以,一个完整的流程是:用户先通过认证登录,然后在其发起请求时,系统进行鉴权,依据授权规则判断是否放行。
注意:在实际开发中,尤其是在Spring Security这类框架的语境下,我们常说“配置权限”,其实就是在定义授权规则,而框架在过滤器链中自动执行的过程就是鉴权。
理清这些概念后,我们就能明白,一个完整的登录及权限认证框架,需要提供一套机制来处理从用户声明身份(登录)到系统验证身份(认证),再到根据规则判断访问资格(授权与鉴权)的全流程。
3. 主流开源登录及权限认证框架深度解析(上)
市面上框架众多,各有侧重。有的重,功能全但学习曲线陡;有的轻,灵活但需要自己组装更多部件。我选取了几个在Java和泛Web开发领域最具代表性、生态最成熟的框架进行拆解。
3.1 Spring Security:Java企业级安全的“定海神针”
提到Java领域的权限框架,Spring Security是绝对无法绕开的名字。它与其说是一个框架,不如说是一个高度可定制、功能全面的安全解决方案。
3.1.1 核心设计思想:过滤器链与委托
Spring Security的核心是一系列串联的Servlet Filter。一个HTTP请求到达应用后,会经过这条安全过滤器链。链上的每个过滤器负责一项特定的安全任务,例如:
UsernamePasswordAuthenticationFilter: 处理表单登录。BasicAuthenticationFilter: 处理HTTP Basic认证。FilterSecurityInterceptor: 进行最终的访问决策鉴权(判断某个URL是否需要特定权限)。
这种设计的好处是职责清晰,你可以轻松地添加、移除或替换过滤器来定制安全流程。它的授权模型主要基于“角色”和“权限”,通过注解(如@PreAuthorize(“hasRole(‘ADMIN’)”))或配置HttpSecurity来声明访问规则。
3.1.2 核心优势与适用场景
- 深度集成Spring生态:与Spring Boot无缝结合,几乎成为Spring技术栈项目的默认安全选择。
- 功能极其全面:从经典的Session管理、Remember-Me、防CSRF、防点击劫持,到OAuth2客户端/资源服务器、LDAP、SAML等企业级协议,应有尽有。
- 高度可配置与可扩展:几乎每一个组件都可以被覆盖或扩展,能满足最复杂的安全需求。
3.1.3 典型配置与实操要点一个最基本的Spring Security配置可能长这样:
@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/", "/home").permitAll() // 首页允许所有人访问 .antMatchers("/admin/**").hasRole("ADMIN") // /admin/ 下的路径需要ADMIN角色 .antMatchers("/user/**").hasRole("USER") // /user/ 下的路径需要USER角色 .anyRequest().authenticated() // 其他所有请求都需要认证 .and() .formLogin() .loginPage("/login") // 自定义登录页 .permitAll() .and() .logout() .permitAll(); } @Bean @Override public UserDetailsService userDetailsService() { // 这里简单使用内存用户,生产环境需从数据库加载 UserDetails user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); UserDetails admin = User.withDefaultPasswordEncoder() .username("admin") .password("admin") .roles("ADMIN", "USER") .build(); return new InMemoryUserDetailsManager(user, admin); } }3.1.4 避坑指南与心得
- 配置顺序很重要:在
authorizeRequests()中,规则的声明顺序就是匹配顺序。更具体的规则要放在更通用的规则前面。如果把.anyRequest().authenticated()放在最前面,后面的所有规则都会失效。 - 小心密码编码器:上面的例子用了
withDefaultPasswordEncoder(),这仅用于演示,绝对禁止在生产环境使用。生产环境必须使用BCryptPasswordEncoder、Pbkdf2PasswordEncoder等安全的、带随机盐的编码器。 - 理解“角色”与“权限”:Spring Security中,角色本质上是一种带有
ROLE_前缀的特殊权限。hasRole(‘ADMIN’)会自动检查ROLE_ADMIN。如果你直接使用权限字符串,则用hasAuthority(‘WRITE_PRIVILEGE’)。 - 微服务下的挑战:在纯粹的微服务架构中,每个服务都引入完整的Spring Security会显得笨重,且Session状态难以共享。此时,通常会将认证网关化,服务本身采用无状态的Token(如JWT)鉴权,Spring Security可以配置为OAuth2资源服务器来适配这种模式。
3.2 Apache Shiro:力求简单灵活的“轻骑兵”
与Spring Security的“重”相比,Shiro的设计哲学是“简单和灵活”。它不依赖任何容器或框架,可以运行在任何环境,从简单的命令行应用到大型的Web应用。
3.2.1 核心设计思想:Subject、SecurityManager与RealmShiro的架构非常清晰,核心是三个概念:
- Subject:代表当前执行操作的用户(或第三方服务、定时任务等)。所有权限判断都围绕Subject进行。
- SecurityManager:Shiro的核心,管理所有Subject,负责认证、授权、会话管理等。它是Shiro的“大脑”。
- Realm:充当Shiro与应用安全数据(如用户数据库、LDAP服务器)之间的“桥梁”。开发者需要自己实现Realm,告诉Shiro如何根据用户名获取用户信息及权限。
这种设计让Shiro的学习曲线相对平缓,概念直观。
3.2.2 核心优势与适用场景
- API直观易用:
subject.login(token),subject.hasRole(“admin”),subject.isPermitted(“user:delete”),代码读起来就像自然语言。 - 轻量级,无侵入:不强制依赖Spring等框架,可以轻松集成到任何项目中。
- 功能模块化:虽然核心功能是认证授权,但也提供了会话管理、缓存、加密、Remember-Me等可选模块,按需取用。
- 易于理解:对于中小型项目或团队中Spring背景不深的成员,Shiro更容易上手。
3.2.3 典型配置与实操要点在Spring Boot中集成Shiro的一个简单示例:
- 引入依赖(Maven):
<dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring-boot-starter</artifactId> <version>1.11.0</version> </dependency> - 自定义Realm:
public class MyShiroRealm extends AuthorizingRealm { @Autowired private UserService userService; // 授权:获取用户的角色和权限信息 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo(); // 从数据库查询角色和权限 Set<String> roles = userService.findRolesByUsername(username); Set<String> permissions = userService.findPermissionsByUsername(username); authorizationInfo.setRoles(roles); authorizationInfo.setStringPermissions(permissions); return authorizationInfo; } // 认证:验证用户身份 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String username = (String) token.getPrincipal(); User user = userService.findByUsername(username); if (user == null) { throw new UnknownAccountException(); // 用户不存在 } // 参数:用户名,数据库中的密码,当前Realm名 return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(), getName()); } } - Shiro配置类:
@Configuration public class ShiroConfig { @Bean public Realm myRealm() { return new MyShiroRealm(); } @Bean public DefaultWebSecurityManager securityManager(Realm realm) { DefaultWebSecurityManager manager = new DefaultWebSecurityManager(); manager.setRealm(realm); return manager; } @Bean public ShiroFilterChainDefinition shiroFilterChainDefinition() { DefaultShiroFilterChainDefinition chain = new DefaultShiroFilterChainDefinition(); // 静态资源允许匿名访问 chain.addPathDefinition("/static/**", "anon"); // 登录接口允许匿名访问 chain.addPathDefinition("/login", "anon"); // 退出登录 chain.addPathDefinition("/logout", "logout"); // 管理员路径需要`admin`角色 chain.addPathDefinition("/admin/**", "authc, roles[admin]"); // 其他所有路径都需要认证 chain.addPathDefinition("/**", "authc"); return chain; } }
3.2.4 避坑指南与心得
- 密码比较:在Realm的
doGetAuthenticationInfo方法中,我们返回的SimpleAuthenticationInfo包含了从数据库查出的正确密码。Shiro会自动用它来比较用户登录时输入的密码。你只需要确保数据库存储的是加密后的密码,并在Realm中配置相应的CredentialsMatcher(如HashedCredentialsMatcher)来指定加密算法。 - Session管理:Shiro提供了自己的Session API,可以独立于Servlet容器的HttpSession工作。这在分布式环境下很有用,但需要注意,如果你同时使用了Shiro Session和HttpSession,要理清它们的关系,避免混淆。
- 过滤器链:Shiro的过滤器(
authc,anon,roles等)是静态配置的,不如Spring Security的DSL动态灵活。对于非常复杂的、动态的URL权限规则,配置起来可能会有些繁琐。 - 注解支持:Shiro也支持
@RequiresRoles,@RequiresPermissions等注解,但需要借助AOP(如Spring AOP)来生效,集成步骤比Spring Security原生注解稍多一步。
3.3 JSON Web Tokens:无状态时代的“通行证”
JWT本身不是一个框架,而是一种开放标准。但在现代前后端分离、微服务架构中,它已经成为实现无状态认证/授权事实上的标准协议,几乎所有主流的安全框架都支持或基于它构建。
3.3.1 核心设计思想:自包含的令牌JWT的核心思想是,将认证和授权信息直接编码到一个令牌里,这个令牌由服务端签发,客户端保存,并在每次请求时携带。服务端只需验证令牌的签名即可信任其中的内容,无需再去查询数据库或共享Session。一个JWT由三部分组成,用点分隔:Header.Payload.Signature。
- Header:声明令牌类型和签名算法,如
{“alg”: “HS256”, “typ”: “JWT”}。 - Payload:存放实际需要传递的数据,比如用户ID、角色、过期时间等。这部分信息是Base64编码的,任何人都可以解码看到,所以绝不能存放密码等敏感信息。
- Signature:对前两部分的签名,用于验证消息在传输过程中未被篡改,以及确认发送者的身份。
3.3.2 核心优势与适用场景
- 无状态,可扩展:服务端不需要存储会话信息,天生适合分布式系统和微服务。
- 跨域友好:可以轻松在移动端、浏览器、API网关之间传递。
- 自包含:减少了查询用户信息的数据库开销。
- 标准化:有严格的RFC标准,各种语言都有成熟库支持。
3.3.3 典型工作流程
- 用户使用凭证(如密码)登录。
- 认证服务验证凭证,生成一个包含用户身份和权限的JWT,并返回给客户端(通常放在
Authorization: Bearer <token>头中)。 - 客户端将JWT存储在本地(如LocalStorage或Cookie)。
- 客户端在后续请求的Header中携带此JWT。
- 资源服务(或API网关)验证JWT的签名和有效期。如果有效,则从Payload中提取用户信息进行鉴权。
3.3.4 实操要点与避坑指南
- 令牌存储与传输安全:
- 不要将JWT存储在
localStorage中,如果网站存在XSS漏洞,令牌可能被窃取。相对更安全的方式是使用HttpOnly、Secure的Cookie。 - 必须使用HTTPS来传输JWT,防止令牌在传输中被窃听。
- 不要将JWT存储在
- 令牌过期与刷新:JWT一旦签发,在过期前无法被服务端主动废止。这是双刃剑。通常策略是设置一个较短的过期时间(如15分钟),并提供一个刷新令牌来获取新的访问令牌。刷新令牌需要持久化存储,并可以主动撤销。
- Payload不要过大:JWT在每次请求中都会携带,过大的Payload会增加网络开销。只存放必要的最小信息集。
- 签名算法选择:
- HS256:对称加密,用同一个密钥进行签名和验证。简单高效,但密钥需要在签发方和验证方之间安全共享。适合单体或服务端完全受控的环境。
- RS256:非对称加密,用私钥签名,公钥验证。公钥可以公开分发,验证方无需知道私钥。这是更安全、更推荐用于分布式环境的方式。
- “注销”难题:由于无状态,服务端无法直接让一个未过期的JWT失效。常见的解决方案有:
- 使用短有效期令牌,降低风险窗口。
- 维护一个很小的令牌黑名单(用于注销近期令牌),但这又引入了状态。
- 改变密钥(极端情况,会使所有令牌失效)。
3.4 OAuth 2.0 与 OpenID Connect:第三方授权与身份认证的“黄金标准”
当你的应用需要允许用户使用微信、GitHub、Google等第三方账号登录,或者你需要开放API给第三方应用调用时,OAuth 2.0和OpenID Connect就是你必须掌握的标准。
3.4.1 OAuth 2.0:专注授权OAuth 2.0是一个授权框架,核心是解决“第三方应用在用户授权下,访问用户在资源服务器上的受保护资源”的问题。它不处理身份认证。例如,一个第三方天气应用想获取你在微信的头像,它引导你到微信授权页面,你同意后,微信给天气应用一个访问令牌,天气应用凭此令牌去微信获取你的头像。在这个过程中,天气应用并不知道你的微信密码,也不知道你到底是不是你,它只知道“微信说这个令牌可以访问某个用户的头像”。
3.4.2 OpenID Connect:在OAuth 2.0之上构建身份层OIDC是建立在OAuth 2.0之上的一个身份认证协议。它在授权流程中,额外返回一个ID Token(一个特殊的JWT),这个令牌里包含了用户的身份信息(如用户唯一标识sub)。这样,第三方应用不仅能拿到访问资源的令牌,还能确切地知道登录的用户是谁。“使用微信登录”这个场景,本质上用的就是OIDC协议。
3.4.3 核心角色与流程以授权码模式(最安全、最常用的模式)为例:
- 资源所有者:用户。
- 客户端:你的Web应用或移动应用。
- 授权服务器:提供登录和授权页面的服务(如微信开放平台、GitHub)。
- 资源服务器:存放用户受保护资源的服务(如微信存储用户头像的服务器)。 流程:
- 客户端将用户重定向到授权服务器的登录/授权页面。
- 用户登录并授权。
- 授权服务器将用户重定向回客户端,并附上一个授权码。
- 客户端用授权码和自己的密钥,向授权服务器换取访问令牌(和ID Token)。
- 客户端使用访问令牌向资源服务器请求资源。
3.4.4 在Spring Security中实现OAuth 2.0客户端Spring Security提供了强大的OAuth 2.0客户端支持,配置非常简单(以GitHub登录为例):
- 引入依赖:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> - 配置文件
application.yml:spring: security: oauth2: client: registration: github: client-id: your-github-client-id client-secret: your-github-client-secret scope: user:email, read:user - 配置安全规则:
完成!Spring Security会自动处理所有重定向、换令牌的流程,并将认证成功的用户信息注入到SecurityContext中。@Configuration public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .anyRequest().authenticated() .and() .oauth2Login(); // 启用OAuth2登录 } }
3.4.5 避坑指南与心得
- 授权码模式是王道:对于Web服务器端应用,务必使用授权码模式。隐式模式等已被标记为不安全,不应再使用。
- 妥善保管
client-secret:这是你应用的身份凭证,必须像保护密码一样保护它,绝不能泄露到前端。 - 正确配置回调地址:在第三方平台注册应用时,回调地址必须完全匹配,包括协议、域名、端口和路径。
state参数防CSRF:在发起OAuth请求时,必须生成一个随机的state参数并保存在会话中,在回调时验证其一致性,防止CSRF攻击。- 区分OAuth2资源服务器与客户端:如果你的应用是提供API的一方(资源服务器),需要配置
@EnableResourceServer;如果是调用第三方API的一方(客户端),则配置@EnableOAuth2Client或使用oauth2Client()。概念别搞混。
4. 框架选型核心考量因素
看了这么多框架和协议,到底该怎么选?没有银弹,只有最适合你当前场景的选择。你可以从下面几个维度来评估:
4.1 项目架构与规模
- 单体/简单Web应用:Spring Security或Apache Shiro都是不错的选择。如果项目本身就是Spring Boot技术栈,用Spring Security更省心;如果追求轻量、简单,或者项目非Spring体系,Shiro是很好的选择。
- 前后端分离/SPA应用:无状态的JWT是天然搭档。可以选择Spring Security + JWT,或者使用专门针对现代应用的安全库。
- 微服务/分布式系统:需要将认证网关化(如使用Spring Cloud Gateway + OAuth2)。各个微服务作为资源服务器,使用JWT进行无状态鉴权。OAuth2和OIDC是解决服务间授权和外部身份联合的标准方案。
4.2 团队技术栈与熟悉度
- 如果团队对Spring生态非常熟悉,Spring Security的学习成本相对较低,集成也更顺畅。
- 如果团队背景多样,或者项目技术栈较老,Shiro的简单API和低侵入性可能更容易被接受。
- 如果团队正在构建全新的云原生或微服务应用,那么从设计之初就采用基于JWT和OAuth2的现代化安全架构是更面向未来的选择。
4.3 安全需求与合规性
- 对于金融、政务等对安全要求极高的场景,需要框架支持细粒度的权限模型(如RBAC、ABAC)、完整的审计日志、多因素认证等。Spring Security的生态和扩展性能更好地满足这些复杂需求。
- 如果需要对接已有的企业身份提供商(如LDAP、Active Directory、SAML IdP),Spring Security对企业协议的支持通常更全面。
- 如果应用需要面向公众,提供社交账号登录,那么支持OAuth2/OIDC是必须的。
4.4 性能与可维护性
- JWT的无状态特性在性能上有优势,但需要仔细设计令牌刷新和注销机制。
- Session方案更成熟,服务端可控性强,但在分布式环境下需要解决Session共享问题(如用Redis),引入了状态和复杂度。
- 权限规则的维护方式:是硬编码在配置/注解里,还是需要动态从数据库加载?Spring Security和Shiro都支持动态权限,但实现方式不同,需要评估哪种更符合你的运维习惯。
5. 常见问题与排查技巧实录
在实际集成和使用这些框架时,你几乎一定会遇到下面这些问题。我把它们和排查思路整理出来,希望能帮你快速定位。
5.1 Spring Security 相关
问题:登录成功后无限重定向到登录页。
排查:这是最常见的问题之一。首先检查你的登录页面URL是否被
permitAll()放行。然后,检查登录成功后的默认跳转路径(defaultSuccessUrl)是否也是一个需要认证的路径,导致循环。使用浏览器开发者工具的网络面板,查看重定向链,能清晰看到跳转过程。心得:建议显式配置
successHandler来处理登录成功后的逻辑,而不是依赖默认跳转。问题:
@PreAuthorize注解不生效。排查:确保在配置类上添加了
@EnableGlobalMethodSecurity(prePostEnabled = true)注解。同时,确保方法调用是通过Spring代理的(例如,在同一个类内部调用带注解的方法会绕过代理,导致注解失效)。问题:CSRF防护导致POST请求被拒绝(403)。
排查:Spring Security默认启用CSRF防护。对于传统的表单提交,你需要在前端页面(如Thymeleaf)的表单中插入
<input type=”hidden” name=”_csrf” th:value=”${_csrf.token}”/>。对于纯API(如前后端分离),如果不需要CSRF,可以在配置中禁用:.csrf().disable(),但务必确保你的API有其他方式防止CSRF(如使用JWT并在Header中传递)。
5.2 Shiro 相关
问题:自定义Realm的授权方法
doGetAuthorizationInfo被多次调用。排查:Shiro默认每次权限检查都会调用授权方法。这可能导致数据库查询压力。解决方案是启用缓存。可以集成Redis或Ehcache,并在Realm中配置缓存管理。
心得:在生产环境中,必须为授权信息配置缓存,并合理设置缓存过期时间。
问题:Shiro的注解(如
@RequiresRoles)无效。排查:Shiro的注解需要AOP支持。在Spring中,你需要确保配置了
@EnableAspectJAutoProxy,并且将Shiro的AuthorizationAttributeSourceAdvisorBean加入到Spring容器中。
5.3 JWT 相关
问题:JWT令牌过期后,如何实现无感刷新?
方案:采用双令牌机制。访问令牌(Access Token)有效期短(如15分钟),刷新令牌(Refresh Token)有效期长(如7天),且存储于服务端(如数据库)。当访问令牌过期,客户端用刷新令牌去获取新的访问令牌。刷新令牌只能使用一次,获取新令牌后旧刷新令牌失效,并颁发一个新的刷新令牌(滚动刷新)。
注意:刷新令牌的端点必须严格防护,通常需要验证客户端身份和令牌有效性。
问题:如何防止JWT令牌被盗用?
措施: 1.强制HTTPS。 2.使用
HttpOnlyCookie存储(尽管对SPA不友好,但更安全)。 3.设置较短的过期时间。 4. 对于敏感操作(如修改密码、支付),要求二次认证。 5. 监控异常,如同一个令牌在短时间内从地理位置上相距甚远的两个IP地址使用。
5.4 OAuth 2.0 相关
问题:获取授权码后,用
/oauth/token换令牌时返回invalid_grant。排查:这是OAuth集成中最常见的错误。请按顺序检查: 1.授权码是否已使用过?授权码是一次性的。 2.回调地址是否完全一致?包括
http和https,末尾的/。 3.client_id和client_secret是否正确? 4.grant_type参数是否为authorization_code?问题:在资源服务器中如何获取当前OAuth2用户的详细信息?
方案:如果使用JWT作为访问令牌,资源服务器可以直接解析JWT的Payload。如果是不透明令牌,则需要调用授权服务器的
/userinfo端点(OIDC)或自定义的用户信息端点。在Spring Security中,可以通过@AuthenticationPrincipal注解注入一个OAuth2User对象来获取。
6. 安全最佳实践与深度思考
无论选择哪个框架,一些安全原则是共通的,必须在设计和开发中贯彻始终。
6.1 密码安全是底线
- 永远不要明文存储密码。使用强哈希算法,如BCrypt(Spring Security默认)、Argon2、PBKDF2。
- 加盐:现代密码哈希函数(如BCrypt)会自动处理盐值,无需手动管理。
- 前端传输也应加密:虽然HTTPS是必须的,但对于极高安全要求的场景,可以考虑在前端对密码进行非对称加密(如RSA),服务端用私钥解密后再哈希存储。但这会增加复杂度,需权衡。
6.2 最小权限原则
- 给用户或服务分配完成任务所必需的最小权限。不要图省事给所有用户
admin角色。 - 在代码审查时,要特别关注权限配置和注解,检查是否有过度授权的情况。
6.3 防御常见攻击
- SQL注入:使用预编译语句(如MyBatis的
#{},JPA的参数化查询),框架本身不直接解决此问题,但错误的权限查询可能引入漏洞。 - XSS:确保所有用户输入在输出到页面时都经过正确的转义或过滤。框架的CSRF防护可以抵御一部分利用XSS发起的攻击。
- CSRF:确保对状态修改操作(POST, PUT, DELETE)启用CSRF防护,或使用JWT等无状态方案时,确保令牌不通过容易被CSRF利用的方式(如Cookie)传输。
- 会话固定/劫持:使用安全的Cookie属性(
HttpOnly,Secure,SameSite),用户登录成功后使旧会话失效(Spring Security默认行为)。
6.4 监控与审计
- 记录所有认证和授权事件:谁、在什么时候、尝试访问什么资源、是否成功。这对于安全事件追溯至关重要。
- 监控异常登录行为:如频繁失败登录、来自异常地理位置的登录、同一账号多地同时登录等。
- 定期审查和更新依赖:安全框架本身也可能出现漏洞。保持Spring Security、Shiro等依赖库的版本更新。
6.5 关于“自己造轮子”我强烈建议,除非有极其特殊、现有框架完全无法满足的需求,否则不要从头自己实现一套认证授权系统。安全是一个深度专业领域,自己实现很容易在细节上埋下难以察觉的漏洞。使用成熟的开源框架,不仅是利用其功能,更是站在巨人的肩膀上,继承了社区无数开发者共同审视和修补的安全经验。你的精力,应该更多地放在业务逻辑和如何正确配置、使用这些框架上。