SpringBoot 多人协作平台实战(7):完善登录模块 ------ Spring 注解体系与密码加密实践
本文是《Java SpringBoot 多人协作平台》系列实战教程的第七篇,在前几课完成基础项目搭建之后,本节重点梳理 Spring 注解体系的核心概念,并在此基础上完善用户登录模块,引入 BCrypt 密码加密机制。
一、Spring 注解体系:项目中的两类对象
在 Spring Boot 项目中,类大体分为两类:
- 容器对象(Entity) :仅用于承载数据,例如
User,不提供任何业务功能,通常放在entity包下,不需要交给 Spring 管理。 - 服务对象(Bean) :负责处理业务逻辑、持久化、接口等功能,由 Spring 容器统一管理和注入。
理解这一区别,是正确使用 Spring 注解的前提。
二、用菜市场理解 Spring 注解
Spring 容器可以理解为整个菜市场的管理处 ,Bean 就是摊位上被统一管理的员工、工具或设备 。把一个类交给 Spring 管理,相当于给摊位办理营业执照,让市场统一调配。
2.1 @Component ------ 普通摊位
kotlin
@Component
public class Scale { // 电子秤
}
- 含义:这是一个普通摊位,由市场统一登记管理。
- 适用场景:通用工具类、辅助组件等没有明确分层职责的类。
2.2 @Service ------ 业务服务摊位
kotlin
@Service
public class VegetableService {
// 卖菜、算账、称重
}
- 含义:专门负责处理业务逻辑的摊位。
- 本质 :是
@Component的语义化扩展,功能完全相同,但命名更清晰。 - 适用场景:Service 层,处理核心业务逻辑。
2.3 @Repository ------ 仓库摊位
kotlin
@Repository
public class VegetableDao {
// 查菜还有多少
}
- 含义:专门负责与数据库交互的仓库摊位。
- 本质 :同样是
@Component的扩展,额外提供持久层异常转译功能。 - 适用场景:DAO 层,负责数据的增删改查。
2.4 @Controller ------ 门口接待窗口
kotlin
@Controller
public class BuyController {
// 顾客说要买啥,我来安排
}
- 含义:市场门口的接待窗口,负责接收外部请求并分发处理。
- 本质 :
@Component的扩展,结合视图解析使用;若接口直接返回 JSON,通常使用@RestController。 - 适用场景:Controller 层,处理 HTTP 请求。
2.5 @Bean ------ 外购设备,手动登记(重点)
以上四个注解都是在自己的类上标注 ,让 Spring 自动扫描注册。但如果要使用的是第三方库的类 (无法修改其源码),就需要用 @Bean 手动注册:
typescript
@Configuration
public class MarketConfig {
@Bean
public CashierMachine cashierMachine() {
// 从外部引入,手动放入 Spring 容器
return new CashierMachine();
}
}
一句话区分:
@Component家族:自己盖的摊位,让市场自动收编。@Bean:外面买来的设备,搬进来手动登记。
2.6 @Configuration ------ 市场规划办公室
ruby
@Configuration
public class MarketConfig {
// 在这里统一声明 @Bean
}
- 含义 :专门存放
@Bean定义的配置类,相当于市场的规划办公室。 - 本质 :本身也是一个
@Component,只是语义上专用于配置。
注解总结对照表
| 注解 | 类比 | 本质 | 适用层 |
|---|---|---|---|
@Component |
普通小摊位 | 基础注解 | 通用 |
@Service |
卖菜做生意摊位 | @Component 扩展 |
业务层 |
@Repository |
仓库摊位 | @Component 扩展 |
数据层 |
@Controller |
门口接待窗口 | @Component 扩展 |
控制层 |
@Bean |
外购设备手动登记 | 方法级注解 | 配置类中 |
@Configuration |
市场规划办公室 | @Component 扩展 |
配置类 |
三、查看接口实现类的快捷键
在 IntelliJ IDEA 中,当你看到一个接口或抽象类,想快速跳转到其实现或子类时:
css
Ctrl + Alt + B
这个快捷键在阅读 Spring 框架源码、理解 UserDetailsService 等接口时非常实用。
四、完善登录模块:引入密码加密
4.1 密码安全的三条铁律
在实现登录模块时,密码处理必须严格遵守以下原则:
- 绝对不能用明文存储密码 ------ 一旦数据库泄露,用户密码将全部暴露。
- 加密是不可逆的 ------ 只能比对,不能还原原始密码。
- 加密必须一致 ------ 使用成熟的标准算法(如 BCrypt),不要自己设计加密方式。
BCrypt 每次加密相同的字符串会产生不同的哈希值(因为内置了随机 salt),但 matches() 方法能正确验证,这是它相比 MD5 等算法的重要优势。
4.2 配置加密服务:WebSecurityConfig
首先引入 Spring Security,在配置类中注册 BCryptPasswordEncoder Bean:
scala
package hello.configuration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
这里使用的正是 @Bean:BCryptPasswordEncoder 是 Spring Security 提供的第三方类,无法在其上添加 @Component,因此通过 @Bean 方法手动注册到容器中。
4.3 实现用户服务:UserService
UserService 实现了 Spring Security 的 UserDetailsService 接口,负责用户注册(密码加密存储)和登录验证(按用户名加载用户信息):
java
package hello.service;
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.core.userdetails.UsernameNotFoundException;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import javax.inject.Inject;
import java.util.Collections;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@Service
public class UserService implements UserDetailsService {
private BCryptPasswordEncoder bCryptPasswordEncoder;
// 使用线程安全的 Map 临时存储用户(暂不接数据库)
private Map<String, String> userPasswords = new ConcurrentHashMap<>();
@Inject
public UserService(BCryptPasswordEncoder bCryptPasswordEncoder) {
this.bCryptPasswordEncoder = bCryptPasswordEncoder;
// 初始化一个默认用户用于测试
save("admin", "admin");
}
/**
* 注册用户,密码经 BCrypt 加密后存储
*/
public void save(String username, String password) {
userPasswords.put(username, bCryptPasswordEncoder.encode(password));
}
/**
* 查询用户加密后的密码
*/
public String getPassword(String username) {
return userPasswords.get(username);
}
/**
* Spring Security 调用此方法加载用户信息进行认证
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (!userPasswords.containsKey(username)) {
throw new UsernameNotFoundException(username + " 不存在!");
}
String encodedPassword = userPasswords.get(username);
return new User(username, encodedPassword, Collections.emptyList());
}
}
关键点解析:
@Inject等同于@Autowired,通过构造器注入BCryptPasswordEncoder,是推荐的依赖注入方式。ConcurrentHashMap是线程安全的,适合多线程环境下的临时数据存储。loadUserByUsername是UserDetailsService的核心方法,Spring Security 在登录时会自动调用它获取用户信息并完成密码校验。- 本节课暂不接入数据库,使用内存 Map 存储,后续章节会替换为真实持久层。
五、整体架构回顾
本节课完成之后,登录模块的基本流程如下:
scss
用户发起登录请求
↓
Controller 接收请求
↓
Spring Security 调用 UserDetailsService.loadUserByUsername()
↓
UserService 从 Map 中取出加密密码
↓
BCryptPasswordEncoder.matches() 校验密码
↓
认证成功 / 失败
六、下一步:会话与登录状态持久化
完成基本的注册与登录后,下一个核心问题是:
登录成功之后,如何在后续请求中携带用户身份信息?
这涉及到 Session / Token(如 JWT)机制的选择与实现,将在后续章节中详细展开。
小结
| 知识点 | 要点 |
|---|---|
| Spring 注解分类 | @Component 家族用于自定义类;@Bean 用于第三方类 |
| 密码加密原则 | 不可逆、不明文、使用标准算法(BCrypt) |
UserDetailsService |
Spring Security 认证的核心接口,必须实现 loadUserByUsername |
| 构造器注入 | 优于字段注入,利于测试和依赖明确 |
| 暂不接数据库 | 用 ConcurrentHashMap 模拟持久层,降低学习复杂度 |
系列课程:Java SpringBoot 多人协作平台实战 · 第七章 · SpringBoot完善登录模块