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>
|
<artifactId>passay</artifactId>
|
||||||
<version>${passay.version}</version>
|
<version>${passay.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.postgresql</groupId>
|
||||||
|
<artifactId>postgresql</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<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;
|
package com.backend.user.service.anyame.config;
|
||||||
|
|
||||||
|
import com.backend.user.service.anyame.component.AuthorizationProperties;
|
||||||
import org.passay.CharacterRule;
|
import org.passay.CharacterRule;
|
||||||
import org.passay.EnglishCharacterData;
|
import org.passay.EnglishCharacterData;
|
||||||
import org.passay.LengthRule;
|
import org.passay.LengthRule;
|
||||||
@ -7,14 +8,24 @@ import org.passay.PasswordValidator;
|
|||||||
import org.passay.WhitespaceRule;
|
import org.passay.WhitespaceRule;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
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.bcrypt.BCryptPasswordEncoder;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
|
||||||
@Configuration
|
@Configuration
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||||
|
return http.csrf(AbstractHttpConfigurer::disable).authorizeHttpRequests(c -> c.anyRequest().permitAll()).build();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public PasswordEncoder passwordEncoder() {
|
public PasswordEncoder passwordEncoder() {
|
||||||
return new BCryptPasswordEncoder();
|
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.AuthResponse;
|
||||||
import com.backend.user.service.anyame.dto.LoginRequest;
|
import com.backend.user.service.anyame.dto.LoginRequest;
|
||||||
import com.backend.user.service.anyame.dto.RegisterRequest;
|
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.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.exception.UnsafePasswordException;
|
||||||
import com.backend.user.service.anyame.service.AuthService;
|
import com.backend.user.service.anyame.service.AuthService;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
@ -31,8 +33,12 @@ public class AuthController {
|
|||||||
return ResponseEntity.ok(authService.register(request));
|
return ResponseEntity.ok(authService.register(request));
|
||||||
} catch (UnsafePasswordException e) {
|
} 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.");
|
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) {
|
} catch (UserAlreadyExistsException e) {
|
||||||
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "email already registered");
|
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));
|
return ResponseEntity.ok(authService.login(request));
|
||||||
} catch (InvalidCredentialsException e) {
|
} catch (InvalidCredentialsException e) {
|
||||||
throw new ResponseStatusException(HttpStatus.UNAUTHORIZED, "invalid credentials", 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;
|
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;
|
package com.backend.user.service.anyame.entity;
|
||||||
|
|
||||||
public enum Role {
|
import jakarta.persistence.Entity;
|
||||||
USER,
|
import jakarta.persistence.GeneratedValue;
|
||||||
MODER,
|
import jakarta.persistence.GenerationType;
|
||||||
ADMIN
|
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.Column;
|
||||||
import jakarta.persistence.Entity;
|
import jakarta.persistence.Entity;
|
||||||
import jakarta.persistence.EnumType;
|
|
||||||
import jakarta.persistence.Enumerated;
|
|
||||||
import jakarta.persistence.GeneratedValue;
|
import jakarta.persistence.GeneratedValue;
|
||||||
import jakarta.persistence.GenerationType;
|
import jakarta.persistence.GenerationType;
|
||||||
import jakarta.persistence.Id;
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.JoinColumn;
|
||||||
|
import jakarta.persistence.JoinTable;
|
||||||
|
import jakarta.persistence.ManyToMany;
|
||||||
import jakarta.persistence.Table;
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
|
||||||
@Entity
|
@Entity
|
||||||
@Table(name = "users")
|
@Table(name = "users")
|
||||||
public class User {
|
public class User {
|
||||||
@ -20,24 +23,29 @@ public class User {
|
|||||||
@Column(unique = true, nullable = false)
|
@Column(unique = true, nullable = false)
|
||||||
private String email;
|
private String email;
|
||||||
private String password;
|
private String password;
|
||||||
@Enumerated(EnumType.STRING)
|
private boolean enabled = true;
|
||||||
private Role role;
|
|
||||||
private boolean emailVerified = 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() {
|
protected User() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public User(String name, String email, String password, Role role) {
|
public User(String name, String email, String password) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.password = password;
|
this.password = password;
|
||||||
this.role = role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User(String name, String email, Role role) {
|
public User(String name, String email) {
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.email = email;
|
this.email = email;
|
||||||
this.role = role;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Long getId() {
|
public Long getId() {
|
||||||
@ -61,16 +69,21 @@ public class User {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Role getRole() {
|
public boolean isEnabled() {
|
||||||
return role;
|
return enabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isEmailVerified() {
|
public User setEnabled(boolean enabled) {
|
||||||
return emailVerified;
|
this.enabled = enabled;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public User setEmailVerified(boolean emailVerified) {
|
public Collection<Role> getRoles() {
|
||||||
this.emailVerified = emailVerified;
|
return roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User setRoles(Collection<Role> roles) {
|
||||||
|
this.roles = roles;
|
||||||
return this;
|
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;
|
import java.util.Optional;
|
||||||
|
|
||||||
public interface UserRepository extends JpaRepository<User, Long> {
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
|
||||||
Optional<User> findByEmail(String email);
|
Optional<User> findByEmail(String email);
|
||||||
|
|
||||||
|
Optional<User> findByEmailOrName(String email, String name);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -1,17 +1,23 @@
|
|||||||
package com.backend.user.service.anyame.service;
|
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.AuthResponse;
|
||||||
import com.backend.user.service.anyame.dto.LoginRequest;
|
import com.backend.user.service.anyame.dto.LoginRequest;
|
||||||
import com.backend.user.service.anyame.dto.RegisterRequest;
|
import com.backend.user.service.anyame.dto.RegisterRequest;
|
||||||
import com.backend.user.service.anyame.entity.Role;
|
import com.backend.user.service.anyame.entity.Role;
|
||||||
import com.backend.user.service.anyame.entity.User;
|
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.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.exception.UnsafePasswordException;
|
||||||
import com.backend.user.service.anyame.repository.UserRepository;
|
import com.backend.user.service.anyame.repository.UserRepository;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AuthService {
|
public class AuthService {
|
||||||
|
|
||||||
@ -19,20 +25,28 @@ public class AuthService {
|
|||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
private final JwtService jwtService;
|
private final JwtService jwtService;
|
||||||
private final PasswordValidatorService passwordValidator;
|
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.userRepository = userRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
this.jwtService = jwtService;
|
this.jwtService = jwtService;
|
||||||
this.passwordValidator = passwordValidator;
|
this.passwordValidator = passwordValidator;
|
||||||
|
this.defaultRoleProvider = defaultRoleProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
public AuthResponse register(RegisterRequest request) throws UnsafePasswordException, EmailAlreadyExistsException {
|
public AuthResponse register(
|
||||||
if (userRepository.findByEmail(request.email()).isPresent()) {
|
RegisterRequest request) throws UnsafePasswordException, UserAlreadyExistsException, NoExpiryDurationException, InvalidRoleException {
|
||||||
throw new EmailAlreadyExistsException();
|
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 (request.password() != null && !request.password().isBlank()) {
|
||||||
if (!passwordValidator.validate(request.password())) {
|
if (!passwordValidator.validate(request.password())) {
|
||||||
@ -40,11 +54,12 @@ public class AuthService {
|
|||||||
}
|
}
|
||||||
user.setPassword(passwordEncoder.encode(request.password()));
|
user.setPassword(passwordEncoder.encode(request.password()));
|
||||||
}
|
}
|
||||||
|
user = userRepository.save(user);
|
||||||
|
|
||||||
return generateAuthResponse(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())
|
User user = userRepository.findByEmail(request.email())
|
||||||
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
|
.orElseThrow(() -> new RuntimeException("Invalid credentials"));
|
||||||
|
|
||||||
@ -55,10 +70,11 @@ public class AuthService {
|
|||||||
return generateAuthResponse(user);
|
return generateAuthResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResponse generateAuthResponse(User user) {
|
private AuthResponse generateAuthResponse(User user) throws NoExpiryDurationException, InvalidRoleException {
|
||||||
String accessToken = jwtService.generateAccessToken(user);
|
String accessToken = jwtService.generateAccessToken(user);
|
||||||
String refreshToken = jwtService.generateRefreshToken(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;
|
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.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.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.Claims;
|
||||||
import io.jsonwebtoken.JwtBuilder;
|
import io.jsonwebtoken.JwtBuilder;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
|
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.function.Function;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class JwtService {
|
public class JwtService {
|
||||||
|
|
||||||
private final JWTSecretProvider secretProvider;
|
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.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()
|
JwtBuilder builder = Jwts.builder()
|
||||||
.subject(user.getEmail())
|
.subject(user.getEmail())
|
||||||
.claim("name", user.getName())
|
.claim("name", user.getName())
|
||||||
.claim("id", user.getId())
|
.claim("id", user.getId())
|
||||||
.claim("role", user.getRole())
|
.claim("roles", roles)
|
||||||
|
.claim("authorities", authorities)
|
||||||
.claim("type", "refresh");
|
.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 issuedAt = new Date();
|
||||||
Date expiryDate = Date.from(Instant.now().plus(refreshExpiry));
|
Date expiryDate = Date.from(Instant.now().plus(refreshExpiry));
|
||||||
@ -41,14 +59,21 @@ public class JwtService {
|
|||||||
.compact();
|
.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()
|
JwtBuilder builder = Jwts.builder()
|
||||||
.subject(user.getEmail())
|
.subject(user.getEmail())
|
||||||
.claim("name", user.getName())
|
.claim("name", user.getName())
|
||||||
.claim("id", user.getId())
|
.claim("id", user.getId())
|
||||||
.claim("role", user.getRole())
|
.claim("roles", roles)
|
||||||
|
.claim("authorities", authorities)
|
||||||
.claim("type", "access");
|
.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 issuedAt = new Date();
|
||||||
Date expiryDate = Date.from(Instant.now().plus(accessExpiry));
|
Date expiryDate = Date.from(Instant.now().plus(accessExpiry));
|
||||||
@ -59,6 +84,14 @@ public class JwtService {
|
|||||||
.compact();
|
.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) {
|
public String extractEmail(String token) {
|
||||||
return getClaims(token).getSubject();
|
return getClaims(token).getSubject();
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,30 @@
|
|||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET}
|
secret: ${JWT_SECRET}
|
||||||
expiry:
|
authorization:
|
||||||
access:
|
roles:
|
||||||
USER: 1d
|
- name: ROLE_USER
|
||||||
MODER: 12h
|
access-expiry: '1d'
|
||||||
ADMIN: 30m
|
refresh-expiry: '90d'
|
||||||
refresh:
|
privileges:
|
||||||
USER: 90d
|
- READ_PRIVILEGE
|
||||||
MODER: 7d
|
- name: ROLE_ADMIN
|
||||||
ADMIN: 2h
|
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