Use Privilege-role system instead of enum Role one

This commit is contained in:
2025-05-27 00:45:51 +05:00
parent 0f89a1baa7
commit 96ca31e536
22 changed files with 572 additions and 104 deletions

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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));
}
}

View File

@ -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();
}
}

View File

@ -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());
}
}

View File

@ -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());
}
}

View File

@ -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) {
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -1,9 +0,0 @@
package com.backend.user.service.anyame.exception;
public class EmailAlreadyExistsException extends Exception {
public EmailAlreadyExistsException() {
super("email already exists");
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
package com.backend.user.service.anyame.exception;
public class NoExpiryDurationException extends Exception {
}

View File

@ -0,0 +1,9 @@
package com.backend.user.service.anyame.exception;
public class UserAlreadyExistsException extends Exception {
public UserAlreadyExistsException() {
super("user already exists");
}
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -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