Spring Security实战指南:从数据库认证到前后端分离的完整实现
在企业级后端开发中,Spring Security是微服务架构下保障API安全的首选中间件。本文不深究其底层原理,而是聚焦于实战应用场景,带你一步步实现基于数据库的登录认证、密码加密、前后端JSON响应、跨域配置及会话并发控制。无论你是初次接触Spring Security,还是希望梳理完整流程,这篇文章都能提供可直接落地的参考。
一、基于数据库的用户认证实现
在实际项目中,用户信息通常存储在关系型数据库中。Spring Security支持自定义数据源,只需实现UserDetailsManager和UserDetailsPasswordService接口即可。以下是核心步骤:
- 创建数据库表:设计用户表(字段包括id、用户名、密码、状态等),并插入测试数据。
- 创建实体类:定义User实体,映射数据库字段。
- 定义DBUserDetailsManager:实现上述两个接口,重写
loadUserByUsername、createUser、updatePassword等方法,从数据库查询并返回UserDetails对象。 - 编辑配置:在SecurityConfig中注入自定义的UserDetailsManager,并配置filterChain。若不主动配置,框架会使用默认的内存用户。
代码示例见下方占位符:
-- 创建数据库
CREATE DATABASE `security-demo`;
USE `security-demo`;
-- 创建用户表
CREATE TABLE `user`(
`id` INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
`username` VARCHAR(50) DEFAULT NULL ,
`password` VARCHAR(500) DEFAULT NULL,
`enabled` BOOLEAN NOT NULL
);
-- 唯一索引
CREATE UNIQUE INDEX `user_username_uindex` ON `user`(`username`);
-- 插入用户数据(密码是 "abc" )
INSERT INTO `user` (`username`, `password`, `enabled`) VALUES
('admin', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Helen', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE),
('Tom', '{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW', TRUE);
@Data
public class User {
@TableId(value = "id", type = IdType.AUTO)
private Integer id;
private String username;
private String password;
private Boolean enabled;
}
@Component
public class DBUserDetailsManager implements UserDetailsManager, UserDetailsPasswordService {
@Autowired
private UserService userService;
@Override
public UserDetails updatePassword(UserDetails user, String newPassword) {
return null;
}
/**
* 根据用户名加载用户详情
* @param username 用户名
* @return 用户详情
* @throws UsernameNotFoundException 如果用户不存在
*/
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userService.lambdaQuery().eq(User::getUsername, username).one();
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
// 返回自定义的安全用户对象,将数据库实体包装在内
return new SecurityUser(user);
}
@Override
public void createUser(UserDetails user) {
}
@Override
public void updateUser(UserDetails user) {
}
@Override
public void deleteUser(String username) {
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return false;
}
}
@Configuration
@EnableMethodSecurity // 开启方法级权限控制
public class WebSecurityConfig {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// 1. 配置请求授权
.authorizeHttpRequests(authorize -> authorize
.requestMatchers("/user/add", "/login", "/error").permitAll() // 明确放行注册、登录和错误页
.requestMatchers("/static/**", "/css/**", "/js/**").permitAll() // 放行静态资源
.anyRequest().authenticated() // 剩下的所有请求都需要登录
)
// 2. 自定义表单登录逻辑
.formLogin(form -> form
.defaultSuccessUrl("/", true) // 登录成功后强制跳转到首页
.permitAll()
)
// 3. 配置注销功能
.logout(logout -> logout
.logoutUrl("/logout") // 注销接口地址
.logoutSuccessUrl("/login?logout") // 注销成功后跳转回登录页并带上提示
.invalidateHttpSession(true) // 销毁 Session .deleteCookies("JSESSIONID") // 删除 Cookie .permitAll()
)
// 4. 防护配置
.csrf(csrf -> csrf.disable()); // Demo 阶段保持禁用以便 API 测试
return http.build();
}
}
实践建议:在实现DBUserDetailsManager时,注意处理用户不存在、密码错误等异常,并返回对应的AuthenticationException子类,以便后续统一处理。
二、密码加密与用户注册
明文存储密码是安全大忌。Spring Security推荐使用BCryptPasswordEncoder进行哈希加密。配置方式如下:
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
在注册或修改密码时,调用passwordEncoder.encode()方法加密:
passwordEncoder.encode(user.getPassword())为了保持实体类的纯粹性(不依赖Spring Security框架),建议自定义一个SecurityUser包装类,实现UserDetails接口。这样既能携带业务数据,又能与安全上下文解耦:
public class SecurityUser implements UserDetails {
@Getter
private final User user; // 持有原始的数据库实体对象
public SecurityUser(User user) {
this.user = user;
}
@Override
public String getUsername() {
return user.getUsername();
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
// 后续可以在此根据 user 角色添加权限
return new ArrayList<>();
}
@Override
public boolean isAccountNonExpired() { return true; }
@Override
public boolean isAccountNonLocked() { return true; }
@Override
public boolean isCredentialsNonExpired() { return true; }
@Override
public boolean isEnabled() {
return user.getEnabled() != null && user.getEnabled();
}
}
然后在DBUserDetailsManager中重写方法,将User实体转换为SecurityUser:
@Override
public void createUser(UserDetails userDetails) {
if (userDetails instanceof SecurityUser) {
// 如果是自定义类型,直接获取内部持有实体保存
userService.save(((SecurityUser) userDetails).getUser());
} else {
// 兜底逻辑:处理标准的 UserDetails 对象
User user = new User();
user.setUsername(userDetails.getUsername());
user.setPassword(userDetails.getPassword());
user.setEnabled(userDetails.isEnabled());
userService.save(user);
}
}
@Override
public void updateUser(UserDetails user) {
// 根据业务需求实现,例如:
// userService.updateById(convertToEntity(user));
}
@Override
public void deleteUser(String username) {
userService.lambdaUpdate().eq(User::getUsername, username).remove();
}
@Override
public void changePassword(String oldPassword, String newPassword) {
}
@Override
public boolean userExists(String username) {
return userService.lambdaQuery().eq(User::getUsername, username).exists();
}
最后在UserController中调用这些方法,完成用户注册的API接口:
@RequiredArgsConstructor
@RestController
@RequestMapping("/user")
public class UserController {
private final UserService userService;
private final UserDetailsManager userDetailsManager;
private final PasswordEncoder passwordEncoder;
@PostMapping("/add")
public String add(@RequestBody User user) {
// 1. 校验用户是否存在
if (userDetailsManager.userExists(user.getUsername())) {
return "添加失败,用户名已存在";
}
// 2. 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
// 3. 使用自定义的 SecurityUser 包装实体类
SecurityUser securityUser = new SecurityUser(user);
// 4. 通过标准接口保存用户
try {
userDetailsManager.createUser(securityUser);
return "添加成功";
} catch (Exception e) {
log.error("添加用户失败", e);
return "添加失败:" + e.getMessage();
}
}
}
⚠️ 注意:密码加密的强度因子(strength)默认为10,可根据服务器性能适当调整。生产环境建议使用12~15。
三、前后端分离:JSON响应与跨域配置
在微服务架构中,前后端通过API通信。Spring Security默认的登录成功/失败响应是页面跳转,我们需要改为返回JSON数据。实现步骤如下:
- 登录成功处理器:实现AuthenticationSuccessHandler,在onAuthenticationSuccess方法中返回JSON(如状态码、token、用户信息)。
- 登录失败处理器:实现AuthenticationFailureHandler,返回错误信息。
- 注销成功处理器:实现LogoutSuccessHandler,返回注销成功提示。
@Component
public class MyAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//获取用户身份信息
Object principal = authentication.getPrincipal();
//创建结果对象
HashMap result = new HashMap();
result.put("code", 0);
result.put("message", "登录成功");
result.put("data", principal);
//转换成json字符串
String json = JSON.toJSONString(result);
//返回响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
@Component
public class MyAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
//获取错误信息
String localizedMessage = exception.getLocalizedMessage();
//创建结果对象
HashMap result = new HashMap();
result.put("code", -1);
result.put("message", localizedMessage);
//转换成json字符串
String json = JSON.toJSONString(result);
//返回响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
@Component
public class MyLogoutSuccessHandler implements LogoutSuccessHandler {
@Override
public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
//创建结果对象
HashMap result = new HashMap();
result.put("code", 0);
result.put("message", "注销成功");
//转换成json字符串
String json = JSON.toJSONString(result);
//返回响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(json);
}
}
然后将这些处理器挂载到Security配置中:
@Configuration
@EnableMethodSecurity // 开启方法级权限控制
public class WebSecurityConfig {
private final MyAuthenticationSuccessHandler successHandler;
private final MyLogoutSuccessHandler logoutSuccessHandler;
private final MyAuthenticationFailureHandler failureHandler;
// 构造器注入
public WebSecurityConfig(MyAuthenticationSuccessHandler successHandler, MyLogoutSuccessHandler logoutSuccessHandler, MyAuthenticationFailureHandler failureHandler) {
this.successHandler = successHandler;
this.logoutSuccessHandler = logoutSuccessHandler;
this.failureHandler = failureHandler;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.formLogin(form -> form
.successHandler(successHandler) // 启用成功处理器
.failureHandler(failureHandler) // 启用失败处理器(
);
http.logout(logout -> logout
.logoutSuccessHandler(logoutSuccessHandler) // 启用注销处理器
.invalidateHttpSession(true) // 销毁 Session
);
return http.build();
}
}
跨域问题:浏览器同源策略会阻止不同域名、端口、协议下的请求。在Spring Security中,只需添加CORS配置即可:
//跨域
http.cors(withDefaults());
扩展建议:生产环境中建议将CORS配置细化,仅允许可信域名,并配合JWT(JSON Web Token)做无状态认证,提升API安全性。
[AFFILIATE_SLOT_1]四、身份认证核心概念与用户信息获取
理解Spring Security的身份认证架构,有助于调试和扩展。下图展示了核心组件的关系:

- SecurityContextHolder:存储当前线程的安全上下文。
- SecurityContext:持有Authentication对象。
- Authentication:包含Principal(用户标识)、Credentials(凭证)和GrantedAuthority(权限)。
- Principal:通常是用户名或UserDetails对象。
- Credentials:密码或证书,认证后通常会被清除。
在Controller中获取当前用户信息的典型方式:
@RestController
public class IndexController {
/**
* 获取当前登录用户的详细信息
* 用于学习 SecurityContextHolder 相关 API
*/ @GetMapping("/user/info")
public String getUserInfo() {
// 1. 从 SecurityContextHolder 中获取认证对象
// SecurityContextHolder 是一个工具类,内部通过 ThreadLocal 存储了当前线程的认证信息
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
// 2. 获取用户身份主体 (在你的项目中,你自定义的 SecurityUser )
Object principal = authentication.getPrincipal();
// 3. 获取用户权限信息 (如角色 ROLE_ADMIN 等,均为自定义)
Object authorities = authentication.getAuthorities();
// 4. 获取认证状态 (是否已登录)
boolean isAuthenticated = authentication.isAuthenticated();
// 为了方便查看,我们将这些信息封装到 Map 中返回
Map<String, Object> info = new java.util.HashMap<>();
info.put("principal", principal);
info.put("authorities", authorities);
info.put("isAuthenticated", isAuthenticated);
info.put("name", authentication.getName()); // 获取用户名
// 使用 Fastjson2 手动转换为 JSON 字符串
return JSON.toJSONString(info);
}
}
✅ 最佳实践:在微服务后端架构中,建议将用户信息封装成公共工具类,通过SecurityContextHolder.getContext().getAuthentication()获取,避免在业务代码中直接依赖Spring Security API。
五、会话并发控制与注销处理
在需要限制同一账号多地登录的场景中,Spring Security提供了会话并发管理。核心是配置SessionInformationExpiredStrategy,当后登录的账号使先登录的账号失效时,触发该策略。
实现处理器:
@Component
public class MySessionInformationExpiredStrategy implements SessionInformationExpiredStrategy {
@Override
public void onExpiredSessionDetected(SessionInformationExpiredEvent event) throws IOException, ServletException {
// 1. 获取响应对象
HttpServletResponse response = event.getResponse();
// 2. 创建你要返回的 JSON 结果
Map<String, Object> result = new HashMap<>();
result.put("code", 401);
result.put("message", "您的账号已在其他地方登录,当前会话已失效");
// 3. 将结果转换为 JSON 并写入响应
response.setContentType("application/json;charset=UTF-8");
response.getWriter().println(JSON.toJSONString(result));
}
}
然后在配置中挂载:
http.sessionManagement(session -> session
.maximumSessions(1) // 限制同一个账号只能 1 个登录
.expiredSessionStrategy(sessionInformationExpiredStrategy) // 挂载自定义策略
);
同时,建议在注销时清除会话信息,并返回JSON响应(如前述LogoutSuccessHandler)。这样前后端可以统一处理登录状态。
[AFFILIATE_SLOT_2]总结
本文从实战出发,完整演示了Spring Security基于数据库的认证流程:从建表、自定义UserDetailsManager,到密码加密、前后端JSON交互、跨域配置,再到会话并发控制。这些技术点覆盖了大多数后端API安全需求。建议读者在理解的基础上,结合项目实际需求进行定制,例如引入JWT、OAuth2等更高级的安全机制。希望这篇指南能成为你Spring Security路上的实用参考。
浙公网安备 33010602011771号