前言
承襲 {初探 Spring Security 文章},使用 InMemoryUserDetailsManager,建立帳號與密碼並儲存於記憶體中。
現實中,我們不會將帳號與密碼這們做,一般情況下,都會存放在資料庫,或者LDAP。
以下我們將改寫使用 MySQL 來管理我們的使用者帳戶。
專案實作
(本次代碼有點多,請細看)
1. 新增 pom.xml 相關 Dependencies
Pom.xml
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
2.增修相關代碼
修改 Web 安全性, 網路安全配置類別 WebSecurityConfig(使用 HTTP Basic Authentication)
增修 SecurityConfig
//SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(auth - > auth
// Read permissions (Guest, User, Admin)
.requestMatchers(HttpMethod.GET, "/api/users", "/api/user/{uid}")
.hasAnyAuthority("read") // , "ROLE_GUEST")
// Create permissions (User, Admin)
.requestMatchers(HttpMethod.POST, "/api/user").hasAnyAuthority("create")
// Requirement : Admin (CRUD) - DELETE/PUT will be handled by @PreAuthorize
.requestMatchers(HttpMethod.PUT, "/api/users/{uid}").hasAnyAuthority("update")
.requestMatchers(HttpMethod.DELETE, "/users/{uid}").hasAnyAuthority("delete")
.anyRequest().authenticated())
.httpBasic(Customizer.withDefaults())
// .formLogin(Customizer.withDefaults())
.sessionManagement(sess - > sess.sessionCreationPolicy(SessionCreationPolicy.STATELESS));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
增修 CustomUserDetailsService
//CustomUserDetailsService.java
@Service
@Transactional(readOnly = true)
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new UsernameNotFoundException("User not found: " + username));
Set<GrantedAuthority> authorities = new HashSet<>();
for (UserRole ur : user.getUserRoles()) {
Role role = ur.getRole();
authorities.add(new SimpleGrantedAuthority(
"ROLE_" + role.getName().name()));
// permission-based authority
for (String p : role.getPermissions()) {
authorities.add(new SimpleGrantedAuthority(p));
}
}
return new org.springframework.security.core.userdetails.User(
user.getUsername(),
user.getPassword(),
authorities);
}
}
增修 Entity
// Role.java
@Entity
@Getter
@Setter
@AllArgsConstructor
@Builder
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Enumerated(EnumType.STRING)
@Column(nullable = false, unique = true)
private RoleName name;
public Role() {
}
public Role(RoleName role) {
this.name = role;
}
public Role(RoleName name, Set<String> permissions) {
this.name = name;
this.permissions = permissions;
}
@Builder.Default
@ElementCollection(fetch = FetchType.EAGER)
@CollectionTable(name = "role_permissions", joinColumns = @JoinColumn(name = "role_id"))
@Column(name = "permission")
private Set<String> permissions = new HashSet<>();
@Builder.Default
@JsonBackReference
@OneToMany(mappedBy = "role", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> userRoles = new HashSet<>();
}
// RoleName.java
public enum RoleName {
ADMIN,
USER,
GUEST;
}
// 角色權限 RolePermission.java
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "role_permissions")
public class RolePermission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "role_id")
private Long role_id;
@Column(name = "permission", length = 255)
private String permission;
}
// 使用者 User.java
@Entity
@Getter
@Setter
@AllArgsConstructor
@Builder
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false, unique = true)
private String username;
@Column(name = "password", nullable = false)
private String password;
@Column(name = "first_name", nullable = true)
private String firstName;
@Column(name = "last_name", nullable = true)
private String lastName;
@Column(name = "email", nullable = false, unique = true)
private String email;
@JsonManagedReference
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private Set<UserRole> userRoles = new HashSet<>();
public User() {
}
public User(String username, String password, String firstName, String lastName, String email) {
this.username = username;
this.password = password;
this.firstName = firstName;
this.lastName = lastName;
this.email = email;
}
public void addRole(UserRole role) {
userRoles.add(role);
role.setUser(this);
}
public void removeRole(UserRole role) {
userRoles.remove(role);
role.setUser(null);
}
}
//UserRole.java
/**
* 中間實體定義(UserRole)
*/
@Entity
@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Table(name = "users_roles")
public class UserRole implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// ManyToOne 關係到 User
@JsonBackReference
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
// ManyToOne 關係到 Role
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "role_id")
private Role role;
@Column(name = "assigned_at")
private LocalDateTime assignedAt = LocalDateTime.now();
public UserRole(User user, Role role) {
this.user = user;
this.role = role;
}
}
增修 Repository
// UserRepository.java
@Repository
public interface UserRepository extends JpaRepository<User, Long> {
@Query("SELECT u FROM User u " +
"LEFT JOIN FETCH u.userRoles ur " + // 載入 UserRole 集合
"LEFT JOIN FETCH ur.role " + // 透過 ur 載入 Role 實體本身
"WHERE u.id = :id")
Optional<User> findByIdWithRolesAndRoleDetails(@Param("id") Long id);
Optional<User> findByUsername(String username);
}
@Repository
public interface RoleRepository extends JpaRepository<Role, Long> {
Optional<Role> findByName(RoleName name);
}
@Repository
public interface UserRoleRepository extends JpaRepository<UserRole, Long> {
}
增修 Service
// UserService.java
@Slf4j
@Service
public class UserService {
@Autowired
private PasswordEncoder passwordEncoder; // Used for hashing passwords
@Autowired
private UserMapper userMapper;
@Autowired
private UserRepository userRepository;
@Transactional
public User createUser(User newUser) {////////////////////////////
if (newUser == null) {
throw new IllegalArgumentException("User must not be null");
}
newUser.setPassword(passwordEncoder.encode(newUser.getPassword()));
User user = userRepository.save(newUser);
return user;
}
/**
* 尋找單一使用者,並返回 DTO
*/
public Optional<UserDto> findByIdDto(Long id) {
return userRepository.findById(id)
// 使用 mapper to DTO
.map(userMapper::toUserDto);
}
public List<User> findAll() {
return userRepository.findAll();
}
/**
* 查找所有使用者,並返回 DTO 列表
*/
public List<UserDto> findAllDto() {////////////////////////////
return userRepository.findAll().stream()
// 使用 mapper to DTO
.map(userMapper::toUserDto)
.collect(Collectors.toList());
}
public Optional<User> findById(Long id) {////////////////////////////
return userRepository.findByIdWithRolesAndRoleDetails(id);
}
public User getUserById(Long uid) {
if (uid == null) {
throw new UserNotFoundException(null);
}
User user = userRepository.findById(uid)
.orElseThrow(() -> new UserNotFoundException(uid));
return user;
}
@Transactional
public User updateUser(@PathVariable Long id, @RequestBody User newUser) {
log.info("Updating user with id: " + id);
return userRepository.findById(id)
.map(user -> {
user.setUsername(newUser.getUsername());
// Update password only if provided
if (newUser.getPassword() != null && !newUser.getPassword().isEmpty()) {
user.setPassword(passwordEncoder.encode(newUser.getPassword()));
}
user.setFirstName(null == newUser.getFirstName() ? user.getFirstName() : newUser.getFirstName());
user.setLastName(null == newUser.getLastName() ? user.getLastName() : newUser.getLastName());
user.setEmail(null == newUser.getEmail() ? user.getEmail() : newUser.getEmail());
return userRepository.save(user);
}).orElseThrow(() -> new RuntimeException("User not found with id " + id));
}
/**
* 刪除使用者
*/
@Transactional
public void deleteUser(Long uid) {
if (uid == null) {
throw new UserNotFoundException(null);
}
userRepository.deleteById(uid);
}
}
// UserRoleService.java
@Service
public class UserRoleService {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@Transactional
public User addRole(Long userId, RoleName roleName) {
// 1. 查找使用者
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("User not found with ID: " + userId));
// 強制初始化集合,避免潛在的 LazyInitializationException
Hibernate.initialize(user.getUserRoles());
// 2. 查找角色
Role role = roleRepository.findByName(roleName)
.orElseThrow(() -> new RuntimeException("Role not found: " + roleName));
// 3. 檢查是否已存在角色
boolean alreadyExists = user.getUserRoles().stream()
.anyMatch(userRole -> userRole.getRole().getName().equals(roleName));
if (alreadyExists) {
return user;
}
// 4. 建立並設定 UserRole 關聯實體
UserRole userRole = new UserRole();
userRole.setUser(user);
userRole.setRole(role);
user.getUserRoles().add(userRole);
userRoleRepository.save(userRole);
return userRepository.save(user);
}
}
// RoleService.java
@Service
public class RoleService {
private final RoleRepository roleRepository;
@Autowired
public RoleService(RoleRepository roleRepository) {
this.roleRepository = roleRepository;
}
/**
* 創建一個新的角色。
*
* @param role 欲儲存的角色實體
* @return 儲存後的角色實體
*/
@Transactional
public Role createRole(Role role) {
// 可以在此處添加驗證,例如檢查角色名稱是否已存在
// RoleName roleName = RoleName.valueOf(role.getName().name().toUpperCase());
Optional<Role> existingRole = roleRepository.findByName(role.getName());
if (existingRole.isPresent()) {
throw new IllegalArgumentException("Role name already exists: " +
role.getName());
}
return roleRepository.save(role);
}
}
增修 Controller
// UserController.java
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserService userService;
// Create (User/Admin)
@PostMapping("/user")
public ResponseEntity<?> createUser(@RequestBody User newUser) {
User user = userService.createUser(newUser);
return new ResponseEntity<>(user, HttpStatus.CREATED);
}
// Read One (Guest/User/Admin)
@GetMapping("/user/{uid}")
public ResponseEntity<User> getUserById(@PathVariable Long uid) {
return userService.findById(uid)
.map(ResponseEntity::ok)
.orElse(ResponseEntity.notFound().build());
}
@GetMapping("/users")
@PreAuthorize("hasAuthority('read')")
public ResponseEntity<List<UserDto>> getAllUsers() {
List<UserDto> userDtos = userService.findAllDto();
return ResponseEntity.ok(userDtos);
}
// Update (Admin)
@PutMapping("/users/{uid}")
public ResponseEntity<User> updateUser(@PathVariable Long uid, @RequestBody User userDetails) {
User updatedUser = userService.updateUser(uid, userDetails);
return ResponseEntity.ok(updatedUser);
}
@DeleteMapping("/users/{uid}")
@PreAuthorize("hasAuthority('delete') or hasRole('ADMIN')")
public ResponseEntity<Void> deleteUser(@PathVariable Long uid) {
userService.deleteUser(uid);
return ResponseEntity.noContent().build();
}
}
增修 DTO Mapper
// UserMapper.java
@Component
public class UserMapper {
public RoleDto toRoleDto(Role role) {
if (role == null) {
return null;
}
RoleDto roleDto = new RoleDto();
roleDto.setId(role.getId());
roleDto.setName(role.getName());
return roleDto;
}
public Set<RoleDto> toRoleDtoSet(Set<UserRole> userRoles) {
if (userRoles == null) {
return Collections.emptySet();
}
return userRoles.stream()
.map(UserRole::getRole)
.map(this::toRoleDto)
.filter(Objects::nonNull)
// .sorted(Comparator.comparing(RoleDto::getName))
.collect(Collectors.toSet());
}
/**
* 將 User 實體轉換為 UserDto
*/
public UserDto toUserDto(User user) {
if (user == null) {
return null;
}
UserDto userDto = new UserDto();
userDto.setId(user.getId());
userDto.setUsername(user.getUsername());
userDto.setFirstName(user.getFirstName());
userDto.setLastName(user.getLastName());
userDto.setEmail(user.getEmail());
userDto.setRoles(toRoleDtoSet(user.getUserRoles()));
return userDto;
}
}
增修 自定 Exception
// UserNotFoundException.java
public class UserNotFoundException extends RuntimeException {
public UserNotFoundException(Long id) {
String errString = "";
if (id == null) {
errString = "User ID must not be null";
} else {
errString = "User with ID " + id + " not found";
}
super(errString);
}
}
增修 初始資料,測試用
@Slf4j
@Component
public class DataInitializer implements CommandLineRunner {
@Autowired
private UserRepository userRepository;
@Autowired
private RoleRepository roleRepository;
@Autowired
private UserRoleRepository userRoleRepository;
@Autowired
private PasswordEncoder passwordEncoder;
@Override
public void run(String... args) throws Exception {
userRoleRepository.deleteAll();
userRepository.deleteAll();
roleRepository.deleteAll();
// --- 1. Create Roles ---
Role adminRole = createRole(RoleName.ADMIN, Set.of("create", "read",
"update", "delete"));
Role userRole = createRole(RoleName.USER, Set.of("create", "read"));
Role guestRole = createRole(RoleName.GUEST, Set.of("read"));
// --- 2. Create Users ---
User adminUser = createUser("admin", "password", "admin", "user",
"admin@example.com");
User standardUser = createUser("user", "password", "standard", "user",
"standard@example.com");
User guestUser = createUser("guest", "password", "guest", "user",
"guest@example.com");
// --- 3. Link Users to Roles (UserRole) ---
linkUserToRole(adminUser, adminRole);
linkUserToRole(standardUser, userRole);
linkUserToRole(guestUser, guestRole);
}
@Transactional
private Role createRole(RoleName name, Set<String> permissions) {
Role role = new Role();
role.setName(name);
role.setPermissions(permissions);
return roleRepository.save(role);
}
@Transactional
private User createUser(String username, String rawPassword, String firstName, String lastName, String email) {
User user = new User();
user.setUsername(username);
user.setPassword(passwordEncoder.encode(rawPassword));
user.setFirstName(firstName);
user.setLastName(lastName);
user.setEmail(email);
return userRepository.save(user);
}
@Transactional
private void linkUserToRole(User user, Role role) {
UserRole userRole = new UserRole();
userRole.setUser(user);
userRole.setRole(role);
userRoleRepository.save(userRole);
user.getUserRoles().add(userRole);
userRepository.save(user);
}
}
啟動App
初始寫入測試資料
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v3.5.8)
21:18:10.278 WARN [com.dannyyu.backend.SpringbootBackendApplication.main()][deprecation.constructDialect\(DialectFactoryImpl.java:153\21:18:10.974 WARN [com.dannyyu.backend.SpringbootBackendApplication.main()][JpaBaseConfiguration$JpaWebConfiguration.openEntityManagerInViewInterceptor\(JpaBaseConfiguration.java:258\Hibernate: select ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,ur1_0.user_id from users_roles ur1_0
. . .
Hibernate: insert into roles (name) values (?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into roles (name) values (?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into roles (name) values (?)
Hibernate: insert into role_permissions (role_id,permission) values (?,?)
Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)
Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)
Hibernate: insert into users (email,first_name,last_name,password,username) values (?,?,?,?,?)
Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)
Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?
Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)
Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?
Hibernate: insert into users_roles (assigned_at,role_id,user_id) values (?,?,?)
Hibernate: select u1_0.id,u1_0.email,u1_0.first_name,u1_0.last_name,u1_0.password,ur1_0.user_id,ur1_0.id,ur1_0.assigned_at,ur1_0.role_id,u1_0.username from users u1_0 left join users_roles ur1_0 on u1_0.id=ur1_0.user_id where u1_0.id=?
確認測試資料已存入DB
測試
測試案例
User 資料
{
"username": "test",
"password": "123456",
"firstName": "test",
"lastName": "yu",
"email": "test@example.com"
}
GUEST:不能 POST,不能新增 User
回應
ADMIN:新增 User
回應
確認數據已進DB
GUEST:可以 GET
測試工具 Postman
Postman 設定方式(HTTP Basic)
- 開 Postman
- 選擇 API(例如 GET /api/users)
- 點 Authorization
- 設定:
- Type:Basic Auth
- Username:你的帳號
- Password:你的密碼(明碼)
Postman 會自動幫你產生 Header
常見錯誤 & 解法
401 Unauthorized
原因:
.沒送 Authorization
.帳號或密碼錯
檢查:
.postman Authorization 是否設定
.密碼是否為「明碼」而不是 BCrypt
403 Forbidden
原因:
.有登入成功
.但 authority 不符合
檢查:
.hasAnyAuthority("read")
.是否真的有回傳 "read"(不是 "ROLE_READ")
留言