Use Privilege-role system instead of enum Role one
This commit is contained in:
5
pom.xml
5
pom.xml
@ -78,6 +78,11 @@
|
||||
<artifactId>passay</artifactId>
|
||||
<version>${passay.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.postgresql</groupId>
|
||||
<artifactId>postgresql</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
|
@ -0,0 +1,104 @@
|
||||
package com.backend.user.service.anyame.component;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "authorization")
|
||||
public class AuthorizationProperties {
|
||||
|
||||
private List<RoleConfig> roles;
|
||||
private String hierarchy;
|
||||
private String defaultRole;
|
||||
|
||||
public List<RoleConfig> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public String getHierarchy() {
|
||||
return hierarchy;
|
||||
}
|
||||
|
||||
public String getDefaultRole() {
|
||||
return defaultRole;
|
||||
}
|
||||
|
||||
public AuthorizationProperties setRoles(List<RoleConfig> roles) {
|
||||
this.roles = roles;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthorizationProperties setHierarchy(String hierarchy) {
|
||||
this.hierarchy = hierarchy;
|
||||
return this;
|
||||
}
|
||||
|
||||
public AuthorizationProperties setDefaultRole(String defaultRole) {
|
||||
this.defaultRole = defaultRole;
|
||||
return this;
|
||||
}
|
||||
|
||||
public Optional<Duration> getAccessExpiry(String roleName) {
|
||||
return roles.stream()
|
||||
.filter(role -> role.getName().equals(roleName))
|
||||
.map(RoleConfig::getAccessExpiry)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public Optional<Duration> getRefreshExpiry(String roleName) {
|
||||
return roles.stream()
|
||||
.filter(role -> role.getName().equals(roleName))
|
||||
.map(RoleConfig::getRefreshExpiry)
|
||||
.findFirst();
|
||||
}
|
||||
|
||||
public static class RoleConfig {
|
||||
|
||||
private String name;
|
||||
private Duration accessExpiry;
|
||||
private Duration refreshExpiry;
|
||||
private List<String> privileges;
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Duration getAccessExpiry() {
|
||||
return accessExpiry;
|
||||
}
|
||||
|
||||
public Duration getRefreshExpiry() {
|
||||
return refreshExpiry;
|
||||
}
|
||||
|
||||
public List<String> getPrivileges() {
|
||||
return privileges;
|
||||
}
|
||||
|
||||
public RoleConfig setName(String name) {
|
||||
this.name = name;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RoleConfig setAccessExpiry(Duration accessExpiry) {
|
||||
this.accessExpiry = accessExpiry;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RoleConfig setRefreshExpiry(Duration refreshExpiry) {
|
||||
this.refreshExpiry = refreshExpiry;
|
||||
return this;
|
||||
}
|
||||
|
||||
public RoleConfig setPrivileges(List<String> privileges) {
|
||||
this.privileges = privileges;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,28 @@
|
||||
package com.backend.user.service.anyame.component;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
public class DefaultRoleProvider {
|
||||
private final AuthorizationProperties authorizationProperties;
|
||||
private final RoleRepository repository;
|
||||
private Role defaultRole;
|
||||
|
||||
public DefaultRoleProvider(AuthorizationProperties authorizationProperties, RoleRepository repository) {
|
||||
this.authorizationProperties = authorizationProperties;
|
||||
this.repository = repository;
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void init() {
|
||||
defaultRole = repository.findByName(authorizationProperties.getDefaultRole()).orElseThrow();
|
||||
}
|
||||
|
||||
public Role getDefaultRole() {
|
||||
return defaultRole;
|
||||
}
|
||||
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
package com.backend.user.service.anyame.component;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
@ConfigurationProperties(prefix = "jwt.expiry")
|
||||
public class JwtExpiryProperties {
|
||||
|
||||
private Map<Role, Duration> access = new HashMap<>();
|
||||
private Map<Role, Duration> refresh = new HashMap<>();
|
||||
|
||||
public Map<Role, Duration> getAccess() {
|
||||
return access;
|
||||
}
|
||||
|
||||
public void setAccess(Map<Role, Duration> access) {
|
||||
this.access = access;
|
||||
}
|
||||
|
||||
public Map<Role, Duration> getRefresh() {
|
||||
return refresh;
|
||||
}
|
||||
|
||||
public void setRefresh(Map<Role, Duration> refresh) {
|
||||
this.refresh = refresh;
|
||||
}
|
||||
|
||||
public Duration getAccessExpiry(Role role) {
|
||||
return access.getOrDefault(role, Duration.ofDays(1));
|
||||
}
|
||||
|
||||
public Duration getRefreshExpiry(Role role) {
|
||||
return refresh.getOrDefault(role, Duration.ofDays(30));
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,64 @@
|
||||
package com.backend.user.service.anyame.component;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Privilege;
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import com.backend.user.service.anyame.repository.PrivilegeRepository;
|
||||
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||
import org.springframework.context.ApplicationListener;
|
||||
import org.springframework.context.event.ContextRefreshedEvent;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
@Component
|
||||
public class SetupDataLoader implements
|
||||
ApplicationListener<ContextRefreshedEvent> {
|
||||
|
||||
boolean alreadySetup = false;
|
||||
private final AuthorizationProperties authorizationProperties;
|
||||
private final RoleRepository roleRepository;
|
||||
private final PrivilegeRepository privilegeRepository;
|
||||
|
||||
public SetupDataLoader(AuthorizationProperties authorizationProperties,
|
||||
RoleRepository roleRepository,
|
||||
PrivilegeRepository privilegeRepository) {
|
||||
this.authorizationProperties = authorizationProperties;
|
||||
this.roleRepository = roleRepository;
|
||||
this.privilegeRepository = privilegeRepository;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onApplicationEvent(ContextRefreshedEvent event) {
|
||||
if (alreadySetup)
|
||||
return;
|
||||
authorizationProperties.getRoles().forEach(roleConfig -> {
|
||||
List<Privilege> privileges = roleConfig.getPrivileges()
|
||||
.stream()
|
||||
.map(this::createPrivilegeIfNotFound)
|
||||
.toList();
|
||||
createRoleIfNotFound(roleConfig.getName(), privileges);
|
||||
});
|
||||
}
|
||||
|
||||
Privilege createPrivilegeIfNotFound(String name) {
|
||||
Optional<Privilege> privilegeOptional = privilegeRepository.findByName(name);
|
||||
if (privilegeOptional.isEmpty()) {
|
||||
privilegeOptional = Optional.of(new Privilege(name));
|
||||
privilegeRepository.save(privilegeOptional.get());
|
||||
}
|
||||
return privilegeOptional.get();
|
||||
}
|
||||
|
||||
Role createRoleIfNotFound(String name, Collection<Privilege> privileges) {
|
||||
Optional<Role> roleOptional = roleRepository.findByName(name);
|
||||
if (roleOptional.isEmpty()) {
|
||||
roleOptional = Optional.of(new Role(name));
|
||||
roleOptional.get().setPrivileges(privileges);
|
||||
roleRepository.save(roleOptional.get());
|
||||
}
|
||||
return roleOptional.get();
|
||||
}
|
||||
|
||||
}
|
@ -1,5 +1,6 @@
|
||||
package com.backend.user.service.anyame.config;
|
||||
|
||||
import com.backend.user.service.anyame.component.AuthorizationProperties;
|
||||
import org.passay.CharacterRule;
|
||||
import org.passay.EnglishCharacterData;
|
||||
import org.passay.LengthRule;
|
||||
@ -7,14 +8,24 @@ import org.passay.PasswordValidator;
|
||||
import org.passay.WhitespaceRule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(c -> c.anyRequest().permitAll()).build();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
@ -32,4 +43,9 @@ public class SecurityConfig {
|
||||
));
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RoleHierarchy roleHierarchy(AuthorizationProperties authorizationProperties) {
|
||||
return RoleHierarchyImpl.fromHierarchy(authorizationProperties.getHierarchy());
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -3,8 +3,10 @@ package com.backend.user.service.anyame.controller;
|
||||
import com.backend.user.service.anyame.dto.AuthResponse;
|
||||
import com.backend.user.service.anyame.dto.LoginRequest;
|
||||
import com.backend.user.service.anyame.dto.RegisterRequest;
|
||||
import com.backend.user.service.anyame.exception.EmailAlreadyExistsException;
|
||||
import com.backend.user.service.anyame.exception.UserAlreadyExistsException;
|
||||
import com.backend.user.service.anyame.exception.InvalidCredentialsException;
|
||||
import com.backend.user.service.anyame.exception.InvalidRoleException;
|
||||
import com.backend.user.service.anyame.exception.NoExpiryDurationException;
|
||||
import com.backend.user.service.anyame.exception.UnsafePasswordException;
|
||||
import com.backend.user.service.anyame.service.AuthService;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@ -31,8 +33,12 @@ public class AuthController {
|
||||
return ResponseEntity.ok(authService.register(request));
|
||||
} catch (UnsafePasswordException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "unsafe password, length 8-64, 1 upper case, 1 lower case, 1 digit, 1 special symbol. No whitespaces in password.");
|
||||
} catch (EmailAlreadyExistsException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already registered");
|
||||
} catch (UserAlreadyExistsException e) {
|
||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "user already registered");
|
||||
} catch (NoExpiryDurationException e) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config");
|
||||
} catch (InvalidRoleException e) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole());
|
||||
}
|
||||
}
|
||||
|
||||
@ -42,6 +48,10 @@ public class AuthController {
|
||||
return ResponseEntity.ok(authService.login(request));
|
||||
} catch (InvalidCredentialsException e) {
|
||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials", e);
|
||||
} catch (NoExpiryDurationException e) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "no expiry duration found, invalid server config");
|
||||
} catch (InvalidRoleException e) {
|
||||
throw new ResponseStatusException(HttpStatus.INTERNAL_SERVER_ERROR, "invalid role defined: " + e.getInvalidRole());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package com.backend.user.service.anyame.dto;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import java.util.List;
|
||||
|
||||
public record AuthResponse(String accessToken, String refreshToken, long id, String email, String name, Role role) {
|
||||
public record AuthResponse(String accessToken, String refreshToken, long id, String email, String name, List<String> roles) {
|
||||
}
|
@ -0,0 +1,40 @@
|
||||
package com.backend.user.service.anyame.entity;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Entity
|
||||
public class Privilege {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
private String name;
|
||||
@ManyToMany(mappedBy = "privileges")
|
||||
private Collection<Role> roles;
|
||||
|
||||
protected Privilege() {
|
||||
}
|
||||
|
||||
public Privilege(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Collection<Role> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
}
|
@ -1,9 +1,61 @@
|
||||
package com.backend.user.service.anyame.entity;
|
||||
|
||||
public enum Role {
|
||||
USER,
|
||||
MODER,
|
||||
ADMIN
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
}
|
||||
@Entity
|
||||
public class Role {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.AUTO)
|
||||
private Long id;
|
||||
|
||||
private String name;
|
||||
@ManyToMany(mappedBy = "roles")
|
||||
private Collection<User> users;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "roles_privileges",
|
||||
joinColumns = @JoinColumn(
|
||||
name = "role_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(
|
||||
name = "privilege_id", referencedColumnName = "id"))
|
||||
private Collection<Privilege> privileges;
|
||||
|
||||
protected Role() {
|
||||
}
|
||||
|
||||
public Role(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public Collection<User> getUsers() {
|
||||
return users;
|
||||
}
|
||||
|
||||
public Collection<Privilege> getPrivileges() {
|
||||
return privileges;
|
||||
}
|
||||
|
||||
public Role setPrivileges(Collection<Privilege> privileges) {
|
||||
this.privileges = privileges;
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
@ -2,13 +2,16 @@ package com.backend.user.service.anyame.entity;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.EnumType;
|
||||
import jakarta.persistence.Enumerated;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.JoinColumn;
|
||||
import jakarta.persistence.JoinTable;
|
||||
import jakarta.persistence.ManyToMany;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
import java.util.Collection;
|
||||
|
||||
@Entity
|
||||
@Table(name = "users")
|
||||
public class User {
|
||||
@ -20,24 +23,29 @@ public class User {
|
||||
@Column(unique = true, nullable = false)
|
||||
private String email;
|
||||
private String password;
|
||||
@Enumerated(EnumType.STRING)
|
||||
private Role role;
|
||||
private boolean emailVerified = true;
|
||||
private boolean enabled = true;
|
||||
|
||||
@ManyToMany
|
||||
@JoinTable(
|
||||
name = "users_roles",
|
||||
joinColumns = @JoinColumn(
|
||||
name = "user_id", referencedColumnName = "id"),
|
||||
inverseJoinColumns = @JoinColumn(
|
||||
name = "role_id", referencedColumnName = "id"))
|
||||
private Collection<Role> roles;
|
||||
|
||||
protected User() {
|
||||
}
|
||||
|
||||
public User(String name, String email, String password, Role role) {
|
||||
public User(String name, String email, String password) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.password = password;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public User(String name, String email, Role role) {
|
||||
public User(String name, String email) {
|
||||
this.name = name;
|
||||
this.email = email;
|
||||
this.role = role;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
@ -61,16 +69,21 @@ public class User {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Role getRole() {
|
||||
return role;
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public boolean isEmailVerified() {
|
||||
return emailVerified;
|
||||
public User setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
public User setEmailVerified(boolean emailVerified) {
|
||||
this.emailVerified = emailVerified;
|
||||
public Collection<Role> getRoles() {
|
||||
return roles;
|
||||
}
|
||||
|
||||
public User setRoles(Collection<Role> roles) {
|
||||
this.roles = roles;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -1,9 +0,0 @@
|
||||
package com.backend.user.service.anyame.exception;
|
||||
|
||||
public class EmailAlreadyExistsException extends Exception {
|
||||
|
||||
public EmailAlreadyExistsException() {
|
||||
super("email already exists");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,19 @@
|
||||
package com.backend.user.service.anyame.exception;
|
||||
|
||||
public class InvalidRoleException extends Exception {
|
||||
private final String invalidRole;
|
||||
|
||||
public InvalidRoleException(String invalidRole) {
|
||||
this.invalidRole = invalidRole;
|
||||
}
|
||||
|
||||
public InvalidRoleException(String message, String invalidRole) {
|
||||
super(message);
|
||||
this.invalidRole = invalidRole;
|
||||
}
|
||||
|
||||
public String getInvalidRole() {
|
||||
return invalidRole;
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,5 @@
|
||||
package com.backend.user.service.anyame.exception;
|
||||
|
||||
public class NoExpiryDurationException extends Exception {
|
||||
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
package com.backend.user.service.anyame.exception;
|
||||
|
||||
public class UserAlreadyExistsException extends Exception {
|
||||
|
||||
public UserAlreadyExistsException() {
|
||||
super("user already exists");
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.backend.user.service.anyame.repository;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Privilege;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface PrivilegeRepository extends JpaRepository<Privilege, Long> {
|
||||
|
||||
Optional<Privilege> findByName(String name);
|
||||
|
||||
}
|
@ -0,0 +1,12 @@
|
||||
package com.backend.user.service.anyame.repository;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface RoleRepository extends JpaRepository<Role, Long> {
|
||||
|
||||
Optional<Role> findByName(String roleName);
|
||||
|
||||
}
|
@ -6,5 +6,9 @@ import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import java.util.Optional;
|
||||
|
||||
public interface UserRepository extends JpaRepository<User, Long> {
|
||||
|
||||
Optional<User> findByEmail(String email);
|
||||
|
||||
Optional<User> findByEmailOrName(String email, String name);
|
||||
|
||||
}
|
||||
|
@ -1,17 +1,23 @@
|
||||
package com.backend.user.service.anyame.service;
|
||||
|
||||
import com.backend.user.service.anyame.component.DefaultRoleProvider;
|
||||
import com.backend.user.service.anyame.dto.AuthResponse;
|
||||
import com.backend.user.service.anyame.dto.LoginRequest;
|
||||
import com.backend.user.service.anyame.dto.RegisterRequest;
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import com.backend.user.service.anyame.entity.User;
|
||||
import com.backend.user.service.anyame.exception.EmailAlreadyExistsException;
|
||||
import com.backend.user.service.anyame.exception.UserAlreadyExistsException;
|
||||
import com.backend.user.service.anyame.exception.InvalidCredentialsException;
|
||||
import com.backend.user.service.anyame.exception.InvalidRoleException;
|
||||
import com.backend.user.service.anyame.exception.NoExpiryDurationException;
|
||||
import com.backend.user.service.anyame.exception.UnsafePasswordException;
|
||||
import com.backend.user.service.anyame.repository.UserRepository;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AuthService {
|
||||
|
||||
@ -19,20 +25,28 @@ public class AuthService {
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final JwtService jwtService;
|
||||
private final PasswordValidatorService passwordValidator;
|
||||
private final DefaultRoleProvider defaultRoleProvider;
|
||||
|
||||
public AuthService(UserRepository userRepository, PasswordEncoder passwordEncoder, JwtService jwtService, PasswordValidatorService passwordValidator) {
|
||||
public AuthService(UserRepository userRepository,
|
||||
PasswordEncoder passwordEncoder,
|
||||
JwtService jwtService,
|
||||
PasswordValidatorService passwordValidator,
|
||||
DefaultRoleProvider defaultRoleProvider) {
|
||||
this.userRepository = userRepository;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.jwtService = jwtService;
|
||||
this.passwordValidator = passwordValidator;
|
||||
this.defaultRoleProvider = defaultRoleProvider;
|
||||
}
|
||||
|
||||
public AuthResponse register(RegisterRequest request) throws UnsafePasswordException, EmailAlreadyExistsException {
|
||||
if (userRepository.findByEmail(request.email()).isPresent()) {
|
||||
throw new EmailAlreadyExistsException();
|
||||
public AuthResponse register(
|
||||
RegisterRequest request) throws UnsafePasswordException, UserAlreadyExistsException, NoExpiryDurationException, InvalidRoleException {
|
||||
if (userRepository.findByEmailOrName(request.email(), request.name()).isPresent()) {
|
||||
throw new UserAlreadyExistsException();
|
||||
}
|
||||
|
||||
User user = new User(request.name(), request.email(), Role.USER);
|
||||
User user = new User(request.name(), request.email());
|
||||
user.setRoles(Collections.singleton(defaultRoleProvider.getDefaultRole()));
|
||||
|
||||
if (request.password() != null && !request.password().isBlank()) {
|
||||
if (!passwordValidator.validate(request.password())) {
|
||||
@ -40,11 +54,12 @@ public class AuthService {
|
||||
}
|
||||
user.setPassword(passwordEncoder.encode(request.password()));
|
||||
}
|
||||
user = userRepository.save(user);
|
||||
|
||||
return generateAuthResponse(user);
|
||||
}
|
||||
|
||||
public AuthResponse login(LoginRequest request) throws InvalidCredentialsException {
|
||||
public AuthResponse login(LoginRequest request) throws InvalidCredentialsException, NoExpiryDurationException, InvalidRoleException {
|
||||
User user = userRepository.findByEmail(request.email())
|
||||
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
|
||||
|
||||
@ -55,10 +70,11 @@ public class AuthService {
|
||||
return generateAuthResponse(user);
|
||||
}
|
||||
|
||||
private AuthResponse generateAuthResponse(User user) {
|
||||
private AuthResponse generateAuthResponse(User user) throws NoExpiryDurationException, InvalidRoleException {
|
||||
String accessToken = jwtService.generateAccessToken(user);
|
||||
String refreshToken = jwtService.generateRefreshToken(user);
|
||||
return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), user.getRole());
|
||||
List<String> roles = user.getRoles().stream().map(Role::getName).toList();
|
||||
return new AuthResponse(accessToken, refreshToken, user.getId(), user.getEmail(), user.getName(), roles);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -0,0 +1,58 @@
|
||||
package com.backend.user.service.anyame.service;
|
||||
|
||||
import com.backend.user.service.anyame.entity.Privilege;
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import com.backend.user.service.anyame.entity.User;
|
||||
import com.backend.user.service.anyame.exception.InvalidRoleException;
|
||||
import com.backend.user.service.anyame.repository.RoleRepository;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.security.core.GrantedAuthority;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
public class AuthorityResolver {
|
||||
private final RoleRepository roleRepository;
|
||||
private final RoleHierarchy roleHierarchy;
|
||||
|
||||
public AuthorityResolver(RoleRepository roleRepository, RoleHierarchy roleHierarchy) {
|
||||
this.roleRepository = roleRepository;
|
||||
this.roleHierarchy = roleHierarchy;
|
||||
}
|
||||
|
||||
public List<String> getAuthorities(User user) throws InvalidRoleException {
|
||||
List<GrantedAuthority> grantedAuthorities = user.getRoles().stream()
|
||||
.map(role -> new SimpleGrantedAuthority(role.getName()))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Collection<? extends GrantedAuthority> reachableAuthorities =
|
||||
roleHierarchy.getReachableGrantedAuthorities(grantedAuthorities);
|
||||
|
||||
Set<String> privileges = new HashSet<>();
|
||||
for (GrantedAuthority authority : reachableAuthorities) {
|
||||
String roleName = authority.getAuthority();
|
||||
Role role = roleRepository.findByName(roleName)
|
||||
.orElseThrow(() -> new InvalidRoleException(roleName));
|
||||
if (role != null && role.getPrivileges() != null) {
|
||||
privileges.addAll(role.getPrivileges().stream()
|
||||
.map(Privilege::getName)
|
||||
.collect(Collectors.toSet()));
|
||||
}
|
||||
}
|
||||
|
||||
Set<String> allAuthorities = reachableAuthorities.stream()
|
||||
.map(GrantedAuthority::getAuthority)
|
||||
.collect(Collectors.toSet());
|
||||
allAuthorities.addAll(privileges);
|
||||
|
||||
return new ArrayList<>(allAuthorities);
|
||||
}
|
||||
|
||||
}
|
@ -1,36 +1,54 @@
|
||||
package com.backend.user.service.anyame.service;
|
||||
|
||||
import com.backend.user.service.anyame.component.AuthorizationProperties;
|
||||
import com.backend.user.service.anyame.component.JWTSecretProvider;
|
||||
import com.backend.user.service.anyame.component.JwtExpiryProperties;
|
||||
import com.backend.user.service.anyame.entity.Role;
|
||||
import com.backend.user.service.anyame.entity.User;
|
||||
import com.backend.user.service.anyame.exception.InvalidRoleException;
|
||||
import com.backend.user.service.anyame.exception.NoExpiryDurationException;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtBuilder;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
|
||||
@Service
|
||||
public class JwtService {
|
||||
|
||||
private final JWTSecretProvider secretProvider;
|
||||
private final JwtExpiryProperties expiryProperties;
|
||||
private final AuthorizationProperties authorizationProperties;
|
||||
private final AuthorityResolver authorityResolver;
|
||||
|
||||
public JwtService(JWTSecretProvider secretProvider, JwtExpiryProperties expiryProperties) {
|
||||
public JwtService(JWTSecretProvider secretProvider,
|
||||
AuthorizationProperties authorizationProperties,
|
||||
AuthorityResolver authorityResolver) {
|
||||
this.secretProvider = secretProvider;
|
||||
this.expiryProperties = expiryProperties;
|
||||
this.authorizationProperties = authorizationProperties;
|
||||
this.authorityResolver = authorityResolver;
|
||||
}
|
||||
|
||||
public String generateRefreshToken(User user) {
|
||||
public String generateRefreshToken(User user) throws InvalidRoleException, NoExpiryDurationException {
|
||||
List<String> authorities = authorityResolver.getAuthorities(user);
|
||||
List<String> roles = user.getRoles().stream()
|
||||
.map(Role::getName)
|
||||
.toList();
|
||||
JwtBuilder builder = Jwts.builder()
|
||||
.subject(user.getEmail())
|
||||
.claim("name", user.getName())
|
||||
.claim("id", user.getId())
|
||||
.claim("role", user.getRole())
|
||||
.claim("roles", roles)
|
||||
.claim("authorities", authorities)
|
||||
.claim("type", "refresh");
|
||||
Duration refreshExpiry = expiryProperties.getRefreshExpiry(user.getRole());
|
||||
Duration refreshExpiry = getMaxExpiry(user,
|
||||
role -> authorizationProperties.getRefreshExpiry(role.getName()))
|
||||
.orElseThrow(NoExpiryDurationException::new);
|
||||
|
||||
Date issuedAt = new Date();
|
||||
Date expiryDate = Date.from(Instant.now().plus(refreshExpiry));
|
||||
@ -41,14 +59,21 @@ public class JwtService {
|
||||
.compact();
|
||||
}
|
||||
|
||||
public String generateAccessToken(User user) {
|
||||
public String generateAccessToken(User user) throws InvalidRoleException, NoExpiryDurationException {
|
||||
List<String> authorities = authorityResolver.getAuthorities(user);
|
||||
List<String> roles = user.getRoles().stream()
|
||||
.map(Role::getName)
|
||||
.toList();
|
||||
JwtBuilder builder = Jwts.builder()
|
||||
.subject(user.getEmail())
|
||||
.claim("name", user.getName())
|
||||
.claim("id", user.getId())
|
||||
.claim("role", user.getRole())
|
||||
.claim("roles", roles)
|
||||
.claim("authorities", authorities)
|
||||
.claim("type", "access");
|
||||
Duration accessExpiry = expiryProperties.getAccessExpiry(user.getRole());
|
||||
Duration accessExpiry = getMaxExpiry(user,
|
||||
role -> authorizationProperties.getAccessExpiry(role.getName()))
|
||||
.orElseThrow(NoExpiryDurationException::new);
|
||||
|
||||
Date issuedAt = new Date();
|
||||
Date expiryDate = Date.from(Instant.now().plus(accessExpiry));
|
||||
@ -59,6 +84,14 @@ public class JwtService {
|
||||
.compact();
|
||||
}
|
||||
|
||||
private Optional<Duration> getMaxExpiry(User user, Function<Role, Optional<Duration>> mapper) {
|
||||
return user.getRoles().stream()
|
||||
.map(mapper)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.max(Duration::compareTo);
|
||||
}
|
||||
|
||||
public String extractEmail(String token) {
|
||||
return getClaims(token).getSubject();
|
||||
}
|
||||
|
@ -1,11 +1,30 @@
|
||||
jwt:
|
||||
secret: ${JWT_SECRET}
|
||||
expiry:
|
||||
access:
|
||||
USER: 1d
|
||||
MODER: 12h
|
||||
ADMIN: 30m
|
||||
refresh:
|
||||
USER: 90d
|
||||
MODER: 7d
|
||||
ADMIN: 2h
|
||||
authorization:
|
||||
roles:
|
||||
- name: ROLE_USER
|
||||
access-expiry: '1d'
|
||||
refresh-expiry: '90d'
|
||||
privileges:
|
||||
- READ_PRIVILEGE
|
||||
- name: ROLE_ADMIN
|
||||
access-expiry: '30m'
|
||||
refresh-expiry: '2h'
|
||||
privileges:
|
||||
- WRITE_PRIVILEGE
|
||||
- CHANGE_PASSWORD_PRIVILEGE
|
||||
hierarchy: |
|
||||
ROLE_ADMIN > ROLE_USER
|
||||
default-role: ROLE_USER
|
||||
|
||||
spring:
|
||||
datasource:
|
||||
url: ${DATABASE_URL}
|
||||
username: ${DATABASE_USERNAME}
|
||||
password: ${DATABASE_PASSWORD}
|
||||
jpa:
|
||||
hibernate:
|
||||
ddl-auto: none
|
||||
server:
|
||||
error:
|
||||
include-message: always
|
||||
|
Reference in New Issue
Block a user