feat(auth): add role-based authorization

- Load the user's role during authentication and expose it as
  a granted authority on the authenticated token
- Add @RequiresAdmin and @RequiresSuperUser method-level
  annotations for role-gated endpoints
- Introduce a shared BaseRepository with specification support
- Replace Example-based credential queries with JPA specifications
This commit is contained in:
zihluwang committed 2026-08-10 15:49:49 +08:00
1 parent 471eda5353
commit 3f60df9de2
11 files changed
+78 -18

No files matched your search

@@ -0,0 +1,7 @@
package com.onixbyte.deltaforceguide.repository;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.JpaSpecificationExecutor;
public interface BaseRepository<T, K> extends JpaRepository<T, K>, JpaSpecificationExecutor<T> {
}
@@ -15,7 +15,7 @@ import java.util.List;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface FirearmRepository extends JpaRepository<Firearm, Long> { public interface FirearmRepository extends BaseRepository<Firearm, Long> {
Page<Firearm> findAllByType(FirearmType type, Pageable pageable); Page<Firearm> findAllByType(FirearmType type, Pageable pageable);
@@ -20,7 +20,7 @@ import java.util.Optional;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface ModificationRepository extends JpaRepository<Modification, Long> { public interface ModificationRepository extends BaseRepository<Modification, Long> {
@EntityGraph(attributePaths = {"firearm"}) @EntityGraph(attributePaths = {"firearm"})
Page<Modification> findAllBy(Pageable pageable); Page<Modification> findAllBy(Pageable pageable);
@@ -2,10 +2,7 @@ package com.onixbyte.deltaforceguide.repository;
import com.onixbyte.deltaforceguide.domain.entity.UserCredential; import com.onixbyte.deltaforceguide.domain.entity.UserCredential;
import com.onixbyte.deltaforceguide.domain.entity.UserCredentialId; import com.onixbyte.deltaforceguide.domain.entity.UserCredentialId;
import org.springframework.data.jpa.repository.Modifying; import org.springframework.data.jpa.repository.*;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param; import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
@@ -18,7 +15,9 @@ import java.util.Optional;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface UserCredentialRepository extends JpaRepository<UserCredential, UserCredentialId> { public interface UserCredentialRepository extends
JpaRepository<UserCredential, UserCredentialId>,
JpaSpecificationExecutor<UserCredential> {
/** /**
* Find all credentials belonging to a given user. * Find all credentials belonging to a given user.
@@ -16,7 +16,7 @@ import java.util.Optional;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface UserRepository extends JpaRepository<User, Long> { public interface UserRepository extends BaseRepository<User, Long> {
@Override @Override
@EntityGraph(attributePaths = {"credentials"}) @EntityGraph(attributePaths = {"credentials"})
@@ -17,7 +17,7 @@ import java.util.List;
* @author zihluwang * @author zihluwang
*/ */
@Repository @Repository
public interface UserRoleRepository extends JpaRepository<UserRole, UserRoleId> { public interface UserRoleRepository extends BaseRepository<UserRole, UserRoleId> {
/** /**
* Find all roles assigned to a given user. * Find all roles assigned to a given user.
@@ -0,0 +1,14 @@
package com.onixbyte.deltaforceguide.security.annotation;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("isAuthenticated() && hasAnyRole('ROLE_ADMIN', 'ROLE_SUPER_USER')")
public @interface RequiresAdmin {
}
@@ -0,0 +1,14 @@
package com.onixbyte.deltaforceguide.security.annotation;
import org.springframework.security.access.prepost.PreAuthorize;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("isAuthenticated() && hasRole('ROLE_SUPER_USER')")
public @interface RequiresSuperUser {
}
@@ -93,5 +93,9 @@ public class UsernamePasswordAuthentication implements Authentication, Credentia
public void setDetails(User user) { public void setDetails(User user) {
this.user = user; this.user = user;
} }
public void setAuthorities(Collection<? extends GrantedAuthority> authorities) {
this.authorities = new ArrayList<>(authorities);
}
} }
@@ -1,22 +1,28 @@
package com.onixbyte.deltaforceguide.security.provider; package com.onixbyte.deltaforceguide.security.provider;
import com.onixbyte.deltaforceguide.domain.entity.UserCredential; import com.onixbyte.deltaforceguide.domain.entity.UserRole;
import com.onixbyte.deltaforceguide.exeption.InternalServerErrorException; import com.onixbyte.deltaforceguide.exeption.InternalServerErrorException;
import com.onixbyte.deltaforceguide.exeption.UnauthorisedException; import com.onixbyte.deltaforceguide.exeption.UnauthorisedException;
import com.onixbyte.deltaforceguide.manager.UserManager; import com.onixbyte.deltaforceguide.manager.UserManager;
import com.onixbyte.deltaforceguide.repository.UserCredentialRepository; import com.onixbyte.deltaforceguide.repository.UserCredentialRepository;
import com.onixbyte.deltaforceguide.repository.UserRoleRepository;
import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication; import com.onixbyte.deltaforceguide.security.authentication.UsernamePasswordAuthentication;
import com.onixbyte.deltaforceguide.shared.CredentialProvider; import com.onixbyte.deltaforceguide.shared.CredentialProvider;
import com.onixbyte.deltaforceguide.shared.Role;
import jakarta.persistence.criteria.Predicate;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Example;
import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
/** /**
* Authentication provider that validates username/password credentials against stored BCrypt hashes. * Authentication provider that validates username/password credentials against stored BCrypt hashes.
* *
@@ -29,16 +35,18 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
private final UserManager userManager; private final UserManager userManager;
private final PasswordEncoder passwordEncoder; private final PasswordEncoder passwordEncoder;
private final UserCredentialRepository userCredentialRepository; private final UserCredentialRepository userCredentialRepository;
private final UserRoleRepository userRoleRepository;
@Autowired @Autowired
public UsernamePasswordAuthenticationProvider( public UsernamePasswordAuthenticationProvider(
UserManager userManager, UserManager userManager,
PasswordEncoder passwordEncoder, PasswordEncoder passwordEncoder,
UserCredentialRepository userCredentialRepository UserCredentialRepository userCredentialRepository,
) { UserRoleRepository userRoleRepository) {
this.userManager = userManager; this.userManager = userManager;
this.passwordEncoder = passwordEncoder; this.passwordEncoder = passwordEncoder;
this.userCredentialRepository = userCredentialRepository; this.userCredentialRepository = userCredentialRepository;
this.userRoleRepository = userRoleRepository;
} }
/** /**
@@ -62,12 +70,13 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
var user = userContainer.get(); var user = userContainer.get();
var userCredentialExample = new UserCredential();
userCredentialExample.setUserId(user.getId());
userCredentialExample.setProvider(CredentialProvider.LOCAL);
// get userContainer credentials from database // get userContainer credentials from database
var userCredentials = userCredentialRepository.findOne(Example.of(userCredentialExample)) var userCredentials = userCredentialRepository.findOne((root, query, cb) -> {
var predicates = new ArrayList<Predicate>();
predicates.add(cb.equal(root.get("provider"), CredentialProvider.LOCAL));
predicates.add(cb.equal(root.get("userId"), user.getId()));
return cb.and(predicates.toArray(Predicate[]::new));
})
.orElseThrow(() -> new UnauthorisedException("您还没有配置密码,请联系管理员处理。")); .orElseThrow(() -> new UnauthorisedException("您还没有配置密码,请联系管理员处理。"));
// validate password // validate password
@@ -76,12 +85,21 @@ public class UsernamePasswordAuthenticationProvider implements AuthenticationPro
throw new UnauthorisedException("用户名或密码错误。"); throw new UnauthorisedException("用户名或密码错误。");
} }
var role = userRoleRepository.findOne((root, query, cb) -> cb.equal(root.get("userId"), user.getId()))
.orElseGet(() -> {
var ur = new UserRole();
ur.setUserId(user.getId());
ur.setRole(Role.ROLE_USER);
return ur;
});
// erase credentials // erase credentials
usernamePasswordAuthentication.eraseCredentials(); usernamePasswordAuthentication.eraseCredentials();
// set values // set values
usernamePasswordAuthentication.setAuthenticated(true); usernamePasswordAuthentication.setAuthenticated(true);
usernamePasswordAuthentication.setDetails(user); usernamePasswordAuthentication.setDetails(user);
usernamePasswordAuthentication.setAuthorities(List.of(new SimpleGrantedAuthority(role.getRole())));
return usernamePasswordAuthentication; return usernamePasswordAuthentication;
} }
@@ -7,6 +7,10 @@ package com.onixbyte.deltaforceguide.shared;
*/ */
public class Role { public class Role {
public static final String ROLE_USER_VALUE = "USER";
public static final String ROLE_ADMIN_VALUE = "ADMIN";
public static final String ROLE_SUPER_USER_VALUE = "SUPER_USER";
public static final String ROLE_USER = "ROLE_USER"; public static final String ROLE_USER = "ROLE_USER";
public static final String ROLE_ADMIN = "ROLE_ADMIN"; public static final String ROLE_ADMIN = "ROLE_ADMIN";
public static final String ROLE_SUPER_USER = "ROLE_SUPER_USER"; public static final String ROLE_SUPER_USER = "ROLE_SUPER_USER";