以下是基于 Spring Security + MySQL + MyBatis 实现认证系统的完整步骤:
在 pom.xml
中添加依赖:
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<scope>runtimescope>
dependency>
<dependency>
<groupId>org.mybatis.spring.bootgroupId>
<artifactId>mybatis-spring-boot-starterartifactId>
<version>2.2.0version>
dependency>
dependencies>
创建用户表:
CREATE TABLE `sys_user` (
`id` INT NOT NULL AUTO_INCREMENT,
`username` VARCHAR(50) UNIQUE NOT NULL,
`password` VARCHAR(100) NOT NULL,
`enabled` TINYINT(1) NOT NULL DEFAULT 1,
`create_time` DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `sys_role` (
`id` INT NOT NULL AUTO_INCREMENT,
`name` VARCHAR(50) NOT NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `sys_user_role` (
`user_id` INT NOT NULL,
`role_id` INT NOT NULL,
PRIMARY KEY (`user_id`,`role_id`)
);
-- 初始化角色数据
INSERT INTO sys_role(name) VALUES('ROLE_USER'), ('ROLE_ADMIN');
// User.java
@Data
public class User {
private Integer id;
private String username;
private String password;
private Boolean enabled;
private Date createTime;
private List<Role> roles;
}
// Role.java
@Data
public class Role {
private Integer id;
private String name;
}
@Mapper
public interface UserMapper {
@Insert("INSERT INTO sys_user(username, password) VALUES(#{username}, #{password})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);
@Select("SELECT * FROM sys_user WHERE username = #{username}")
@Results({
@Result(property = "roles", column = "id",
many = @Many(select = "com.example.mapper.RoleMapper.findByUserId"))
})
User findByUsername(String username);
}
@Mapper
public interface RoleMapper {
@Select("SELECT r.* FROM sys_role r " +
"JOIN sys_user_role ur ON r.id = ur.role_id " +
"WHERE ur.user_id = #{userId}")
List<Role> findByUserId(Integer userId);
}
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/register").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginProcessingUrl("/login")
.successHandler((req, res, auth) -> {
res.getWriter().write("Login success!");
})
)
.csrf().disable();
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserMapper userMapper;
public UserDetailsServiceImpl(UserMapper userMapper) {
this.userMapper = userMapper;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userMapper.findByUsername(username);
if (user == null) {
throw new UsernameNotFoundException("用户不存在");
}
return org.springframework.security.core.userdetails.User
.withUsername(username)
.password(user.getPassword())
.authorities(getAuthorities(user.getRoles()))
.build();
}
private String[] getAuthorities(List<Role> roles) {
return roles.stream()
.map(Role::getName)
.toArray(String[]::new);
}
}
@RestController
public class AuthController {
private final UserMapper userMapper;
private final PasswordEncoder passwordEncoder;
public AuthController(UserMapper userMapper, PasswordEncoder passwordEncoder) {
this.userMapper = userMapper;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody User user) {
// 检查用户名是否已存在
if (userMapper.findByUsername(user.getUsername()) != null) {
return ResponseEntity.badRequest().body("用户名已存在");
}
// 加密密码
user.setPassword(passwordEncoder.encode(user.getPassword()));
userMapper.insert(user);
// 分配默认角色
userMapper.insertUserRole(user.getId(), 1); // ROLE_USER
return ResponseEntity.ok("注册成功");
}
}
# application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/security_db?useSSL=false
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"username":"test","password":"123456"}'
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=test&password=123456"
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser │ │ Spring Security Filter Chain │
└──────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ /register (POST) │ │
│──────────────────────>│ │
│ │─User Insert──────────>│ MySQL
│ │<─Success Response─────│
│<──────────────────────│ │
│ │ │
│ /login (POST) │ │
│──────────────────────>│ │
│ │─Query User───────────>│
│ │<─User Details─────────│
│ │─BCrypt Verify────────>│
│<─Auth Success─────────│ │
建议使用Postman进行接口测试,并使用MySQL Workbench验证数据存储的正确性。
以下是基于 Spring Security + MySQL + Spring Data JPA 的完整认证系统实现方案:
src/main/java
├── com/example
│ ├── config # 安全配置
│ ├── controller # 控制器
│ ├── entity # JPA实体
│ ├── repository # JPA仓库
│ └── security # 安全相关类
spring:
datasource:
url: jdbc:mysql://localhost:3306/auth_db?useSSL=false
username: root
password: yourpassword
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate:
ddl-auto: update
show-sql: true
properties:
hibernate:
format_sql: true
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String username;
@Column(nullable = false)
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(
name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();
// Getters and Setters
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(unique = true, nullable = false)
private String name;
// Getters and Setters
}
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
boolean existsByUsername(String username);
}
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(String name);
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig {
private final UserDetailsServiceImpl userDetailsService;
public SecurityConfig(UserDetailsServiceImpl userDetailsService) {
this.userDetailsService = userDetailsService;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests(auth -> auth
.antMatchers("/register", "/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginProcessingUrl("/login")
.successHandler((request, response, authentication) -> {
response.getWriter().println("Login success!");
})
)
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessHandler((request, response, authentication) -> {
response.getWriter().println("Logout success!");
})
);
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authManager(HttpSecurity http) throws Exception {
return http.getSharedObject(AuthenticationManagerBuilder.class)
.userDetailsService(userDetailsService)
.passwordEncoder(passwordEncoder())
.and()
.build();
}
}
@Service
public class UserDetailsServiceImpl implements UserDetailsService {
private final UserRepository userRepository;
public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
return org.springframework.security.core.userdetails.User
.withUsername(username)
.password(user.getPassword())
.authorities(getAuthorities(user.getRoles()))
.build();
}
private String[] getAuthorities(Set<Role> roles) {
return roles.stream()
.map(Role::getName)
.toArray(String[]::new);
}
}
@RestController
public class AuthController {
private final UserRepository userRepository;
private final RoleRepository roleRepository;
private final PasswordEncoder passwordEncoder;
public AuthController(UserRepository userRepository,
RoleRepository roleRepository,
PasswordEncoder passwordEncoder) {
this.userRepository = userRepository;
this.roleRepository = roleRepository;
this.passwordEncoder = passwordEncoder;
}
@PostMapping("/register")
public ResponseEntity<?> register(@RequestBody UserRegistrationDto dto) {
if (userRepository.existsByUsername(dto.getUsername())) {
return ResponseEntity.badRequest().body("Username already exists");
}
User user = new User();
user.setUsername(dto.getUsername());
user.setPassword(passwordEncoder.encode(dto.getPassword()));
// 分配默认角色
Role userRole = roleRepository.findByName("ROLE_USER")
.orElseGet(() -> roleRepository.save(new Role("ROLE_USER")));
user.getRoles().add(userRole);
userRepository.save(user);
return ResponseEntity.ok("Registration success");
}
}
// DTO类
public class UserRegistrationDto {
private String username;
private String password;
// Getters and Setters
}
-- 手动初始化角色表(或通过JPA自动生成)
INSERT INTO roles(name) VALUES('ROLE_USER'), ('ROLE_ADMIN');
curl -X POST http://localhost:8080/register \
-H "Content-Type: application/json" \
-d '{"username":"test", "password":"123456"}'
curl -X POST http://localhost:8080/login \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "username=test&password=123456"
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Client │ │ Spring Security Filter Chain │ │ MySQL
└──────┬───────┘ └───────┬───────┘ └───────┬───────┘
│ /register (POST) │ │
│──────────────────────>│ │
│ │─Save User─────────────>│
│ │<─JPA Transaction──────│
│<─201 Created──────────│ │
│ │ │
│ /login (POST) │ │
│──────────────────────>│ │
│ │─JPA Query─────────────>│
│ │<─User Entity───────────│
│ │─BCrypt Verify─────────>│
│<─200 OK───────────────│ │
问题现象 | 解决方案 |
---|---|
角色未正确分配 | 检查角色初始化脚本或JPA自动生成逻辑 |
密码验证失败 | 确认密码加密方式是否一致 |
用户重复注册 | 检查existsByUsername 查询逻辑 |
JPA关联关系失效 | 验证@ManyToMany 注解配置 |
以下是 Spring Security + MySQL + Spring Data JPA 与 Spring Security + MySQL + MyBatis 的核心区别对比:
对比维度 | Spring Data JPA | MyBatis |
---|---|---|
持久层框架 | 基于 JPA 规范(如 Hibernate 实现)的 ORM 框架 | SQL 映射框架(非 ORM) |
SQL 控制 | 自动生成 SQL(可自定义 JPQL/HQL) | 完全手动编写 SQL |
对象关系映射 | 自动映射实体与表结构(@Entity 注解) | 手动配置映射(XML 或注解) |
缓存机制 | 一级/二级缓存自动管理 | 需手动配置缓存(如集成 Redis) |
// Spring Data JPA 方式(自动方法名推导)
public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByUsername(String username);
}
// MyBatis 方式(需手动写 SQL)
@Mapper
public interface UserMapper {
@Select("SELECT * FROM users WHERE username = #{username}")
User findByUsername(String username);
}
// JPA 方式(直接注入 Repository)
@Service
public class JpaUserDetailsService implements UserDetailsService {
private final UserRepository userRepo;
public UserDetails loadUserByUsername(String username) {
return userRepo.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("用户不存在"));
}
}
// MyBatis 方式(注入 Mapper)
@Service
public class MyBatisUserDetailsService implements UserDetailsService {
private final UserMapper userMapper;
public UserDetails loadUserByUsername(String username) {
User user = userMapper.findByUsername(username);
if (user == null) throw new UsernameNotFoundException("用户不存在");
return user;
}
}
场景 | Spring Data JPA | MyBatis |
---|---|---|
简单 CRUD | 快速(方法名自动推导) | 需手写 SQL 和映射配置 |
复杂联表查询 | 需自定义 JPQL/HQL 或 Native Query | 直接编写优化后的 SQL(更灵活) |
动态条件查询 | 使用 Specification 或 QueryDSL | 通过 XML 动态拼接 SQL |
数据库移植性 | 高(Hibernate 自动适配方言) | 低(SQL 需适配不同数据库) |
spring:
jpa:
hibernate:
ddl-auto: update
show-sql: true
mybatis:
mapper-locations: classpath:mapper/*.xml
type-aliases-package: com.example.entity
优化方向 | Spring Data JPA | MyBatis |
---|---|---|
SQL 调优 | 通过 Hibernate 的 fetch plan 优化 | 直接优化原生 SQL |
批量操作 | 需配置 hibernate.jdbc.batch_size | 通过 批量处理 |
懒加载 | 自动支持(@ManyToOne(fetch=FetchType.LAZY)) | 需手动处理关联查询 |
技术组合 | 推荐场景 |
---|---|
Spring Security + JPA | 快速开发、需求变化频繁、团队熟悉 ORM 规范、需要数据库移植性 |
Spring Security + MyBatis | 复杂 SQL 优化需求、遗留系统维护、需要精细控制 SQL、团队熟悉传统 SQL 开发模式 |
// JPA 方式(自动分页)
Page<User> findAll(Pageable pageable);
// MyBatis 方式(手动编写分页 SQL)
@Select("SELECT * FROM users LIMIT #{offset}, #{pageSize}")
List<User> findUsers(@Param("offset") int offset, @Param("pageSize") int pageSize);
// JPA 方式(自动关联查询)
@Entity
public class User {
@ManyToMany
private Set<Role> roles;
}
// MyBatis 方式(手动联表查询)
<select id="findUserWithRoles" resultMap="userRoleMap">
SELECT u.*, r.id as role_id, r.name
FROM users u
LEFT JOIN user_roles ur ON u.id = ur.user_id
LEFT JOIN roles r ON ur.role_id = r.id
WHERE u.username = #{username}
</select>
• 选择 JPA:当需要快速迭代、减少 SQL 编写、利用 ORM 特性时
• 选择 MyBatis:当需要完全控制 SQL、优化复杂查询、维护遗留系统时
• Spring Security 集成:两者在认证授权流程上完全一致,区别仅在于用户数据访问层的实现方式