Java Spring Boot — Role Based Authorization

Mustafa Enes Tepe
8 min readJun 28, 2022

--

Selamlar. Bu yazı içerisinde, Java Spring Boot üzerinde Rol Tabanlı doğrulamanın nasıl yapabileceğimize değineceğim. Öncelikle rol tabanlı doğrulamayı açıklayacak olur isek; rol tabanlı doğrulama kişilerin sahip olduğu yetkilerce, belirli işlemlere sahip olmasıdır.

Gerek proje içerisindeki bağımlılıklar olsun gerekse takıldığınız yerler olsun, yazılardaki kaymalar olsun, projeme github üzerindeki şu link’ten ulaşabilirsiniz=https://github.com/MET-DEV/Java-SpringBoot-Role-Based-Authorization

Projemiz için veri tabanı bağlantımızı unutmayalım. application.properties dosyamızı düzenleyelim. Ben Auth-Article isimli bir Database açtığım için ismi o şekilde verdim siz de kendinize göre ayarlarsınız.

spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.hibernate.show-sql=true
spring.datasource.url=jdbc:postgresql://localhost:5432/Auth-Article
spring.datasource.username=postgres
spring.datasource.password=12345
spring.jpa.properties.javax.persistence.validation.mode = none

Projemizin yapısına değinerek başlayabiliriz. Dosyalama yapımız aşağıda göründüğü gibi olacak.

Şimdi kodlamaya başlayabiliriz. Bizlere ilk önce bir tane User Modeli lazım. Bunu oluşturarak ilk kodlarımızı yazmaya başlayalım.

@Entity
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
@Table(name = "users")
public class User {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
private int id;


@Column(name = "user_name")
private String username;

@Column(name = "password")
private String password;


@Column(name = "register_date")
private LocalDateTime date;


@Column(name = "first_name")
private String firstName;

@Column(name = "last_name")
private String lastName;

@Column(name = "email")
private String email;

@Column(name = "status")
private boolean isEnabled;

@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY)
@JoinTable(
name = "users_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id")
)
private Set<Role> roles = new HashSet<>();



}

Burada dikkat etmemiz gereken alanlar; userName, password, roles, isEnabled olacak. Çünkü bizler bu alanlar ile giriş yapacağız. Roles kısmında gördüğümüz gibi bir ManyToMany ilişkimiz var ve bu ilişkiden doğan users_roles tablosu var. Bu tablo user’larımızın sahip olduğu rollerin id’sini ve user’ımızın id’sini tutacak. ManyToMany olmasının sebebi ise bir kullanıcı birçok role, bir rol ise birçok kullanıcıya verilebilir. Şimdi ise Altı kızaran Role modelimizi oluşturmamızın zamanı geldi.

@Table(name="roles")
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Setter
public class Role {
@Id
@Column(name = "id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private int id;

private String name;

@ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "roles")
@JsonIgnore
private Set<User> users;
}

Role modelimiz hakkında çok bir şey anlatmaya gerek yok aslında. ManyToMany ilişkimizi belirttik ve JsonIgnore ile rollerin çağrıldığında user’ların da gelmesine gerek olmadığını söyledik.

Şimdi ise UserDetailsDto, LoginDto, RegisterDto’larımızı oluşturacağız.

public class UserDetailsDto implements UserDetails {
private User user;

public UserDetailsDto(User user) {
this.user = user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
Set<Role> roles = user.getRoles();
List<SimpleGrantedAuthority> authorities = new ArrayList<>();

for (Role role : roles) {
authorities.add(new SimpleGrantedAuthority(role.getName()));
}

return authorities;
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getUsername();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return user.isEnabled();
}
}

UserDetailsDto’muzu oluşturduk ve dikkat edeceğiniz üzere UserDetails interface’inden implement’e ettik. Bunun dışında dikkat edeceğimiz diğer unsur ise Bizlere getAuthorities ismi ile rolleri getiren metodumuz. Diğer kısımlarda ise UserDetails ile gelen gerekli metotları Override ettik.

Şimdi LoginDto ve RegisterDto’larımızı oluşturacağız. Bunlar bizlere api dışından bilgi alımında kolaylıklar sağlayacaklar. Gayet basit yapıları var.

@Data
public class LoginDto {
private String username;
private String password;

}
@Getter
@Setter
@AllArgsConstructor
@NoArgsConstructor
public class RegisterDto {
private String firstName;
private String lastName;
private String email;
private String username;
private String password;

public User registerDtoToUser(RegisterDto registerDto){
User user=new User();
user.setEmail(registerDto.getEmail());
user.setFirstName(registerDto.getFirstName());
user.setLastName(registerDto.getLastName());
user.setUsername(registerDto.getUsername());
user.setPassword(registerDto.getPassword());
return user;
}
}

RegisterDto kısmında değineceğim ufak bir kısım var. registerDtoToUser isimli metodumuz bizler için mini bir map işlemi gerçekleştiriyor. Bizler için RegisterDto olarak gelen yapıyı User modeline çeviriyor.

Şimdi Reposityory katmanına geçtik ve hemen UserRepository interface’imizi oluşturabiliriz.

@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query("SELECT u FROM User u WHERE u.username = :username")
public User getUserByUsername(@Param("username") String username);
}

Burada JpaRepository ile CRUD operasyonlarına zaten sahip olduk. Sadece içerisine UserName ile User getiren bir tane metot imzası attık.

Service katmanımıza gelip UserDetailsServiceImpl class’ımızı oluşturabiliriz.

public class UserDetailsServiceImpl implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Autowired
private BCryptPasswordEncoder passwordEncoder;

@Override
public UserDetails loadUserByUsername(String username)
throws UsernameNotFoundException {
User user = userRepository.getUserByUsername(username);

if (user == null) {
throw new UsernameNotFoundException("Could not find user");
}

return new UserDetailsDto(user);
}
public void registerUser(RegisterDto registerDto){
registerDto.setPassword(passwordEncoder.encode(registerDto.getPassword()));
userRepository.save(registerDto.registerDtoToUser(registerDto));
}
}

Evet burada basit bir şekilde register ve kullanıcı adına göre kullanıcıları getirme işlemi var(Zaten yukarıda user repository’de yazdığımız metodu çağırdık). Register kısmında unutmayalım ki önce parolayı şifreledik, sonra userDto içinde yazdığımız mini mapping metodumuzu çağırdık.

Şimdi ise Auth katmanımıza geçiyoruz ve TokenManager Class’ımızı oluşturuyoruz.

@Service
public class TokenManager {

@Autowired
UserRepository userRepository;

private static final int expirationTime = 5 * 60 * 1000;
Key key = Keys.secretKeyFor(SignatureAlgorithm.HS256);
public String generateToken(Authentication authentication) {
final String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
String token= Jwts.builder()
.setSubject(authentication.getName())
.setExpiration(new Date(System.currentTimeMillis()+expirationTime))
.setIssuer("Met")
.claim("roles",authorities)
.setIssuedAt(new Date(System.currentTimeMillis()))
.signWith(key)
.compact();


return token;
}


public boolean tokenValidate(String token) {
if (checkExpirationTime(token) && getUsernameFromToken(token) != null) {
return true;
}
return false;
}


UsernamePasswordAuthenticationToken getAuthentication(final String token, final Authentication existingAuth, final UserDetails userDetails) {

final JwtParser jwtParser = Jwts.parser().setSigningKey(key);

final Jws claimsJws = jwtParser.parseClaimsJws(token);

final Claims claims = (Claims) claimsJws.getBody();

final Collection authorities =
Arrays.stream(claims.get("roles").toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
System.out.println(authorities);

return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}

public String getUsernameFromToken(String token) {
Claims claims = getClaims(token);
return claims.getSubject();
}

public boolean checkExpirationTime(String token) {
Claims claims = getClaims(token);
return claims.getExpiration().after(new Date(System.currentTimeMillis()));
}


public Claims getClaims(String token) {
return Jwts.parser().setSigningKey(key).parseClaimsJws(token).getBody();
}

}

Burada öncelikle UserRepository’mizi çağırıyoruz ve daha sonrasında bir tane expirationTime (Token süremizin üretildikten sonra tüketime kadar olacağı zaman), bir tane de key tanımlıyoruz (Ben SignatureAlgorithm.HS256 seçtim). Daha sonrasında ise bizler için token oluşturacak metodumuzu yazıyoruz (generateToken). Daha sonra bir değişkenin (authorities) içerisine rollerimizi alıp, sonra her bir rolü “,” ile ayırıp String bir halde atıyoruz.

Şimdi Token oluşturma zamanı, subject istiyor ben authentication.getName() verdim. Expiration tarihimizi istiyor orada ise şuanki(yani token’ın üretildiği) zamana üstte belirlediğimiz süreyi ekliyor ve tükenme zamanını belirliyorum. İmzacı olarak ben kendimi yazdım :))). Sonra claims kısmına önce bir name, ki ben bunu roles olarak verdim sonra ise üstte tanımladığım rollerin tutulduğu authorities’i verdim (Burası çok değerli!!!). Sonrasında imzalanma zamanı var, şuanki zamanı verdim, bir de key kısmı var üstte oluşturduğum key’i verdim. Sonrasında ise bunu döndüm. Hemen altında validate metodu var basit bir şekilde zamanı geçmiş mi’yi vs kontrol ediyor.

Aşağıda ise getAuthentication isimli bir metodumuz var. Bu metot ise oluşturulan bir token için claimleri bir dizi haline getiriyor(Yukarıda virgül ile ayırdık burada virgül ile birleştirdik, Sysout token da sorun vardı diye yazdım. Pek bir mantığı yok yani). Altta ise yukarıdan aldığımız bilgileri dönderiyoruz. Diğer metotlar ise basit işlevlere sahip. İşte token’dan claim’leri çekiyor, username’i alıyor, zamanı alıyor falan.

Şimdi JwtTokenFilter class’ımızı oluşturabiliriz. Dışarıdan gelen Token’larımızı filtreleyelim bakalım:)))

@Component
public class JwtTokenFilter extends OncePerRequestFilter {

@Autowired
private TokenManager tokenManager;
@Autowired
private UserDetailsService service;

@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
final String authHeader=req.getHeader("Authorization");
String username=null;
String token=null;

if(authHeader !=null && authHeader.contains("Bearer")) {
token=authHeader.substring(7);
System.out.println(token);
try {
username=tokenManager.getUsernameFromToken(token);

}catch (Exception e) {
System.out.println(e.getMessage());
}
}
if (username != null && token != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
if (tokenManager.tokenValidate(token)) {
UsernamePasswordAuthenticationToken upassToken =
tokenManager.getAuthentication(token, SecurityContextHolder.getContext().getAuthentication(), service.loadUserByUsername(username));

upassToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
SecurityContextHolder.getContext().setAuthentication(upassToken);
}
}
chain.doFilter(req, res);
}

}

Burada OncePerRequestFilter tarafından extend edilince, doFilterInternal metodunu override etme hakkımız oluyor. Bunun içerisinde ise kısaca, gelen request’in header’ı alıyoruz. Sonra bu request’in header’ı boş mu falan diye kontrol ediyoruz bpş değilse Bearer içeriyor mu diye bakıyoruz. İçeriyorsa token’ımızı burada çekip alıyoruz. Sonra ise gerekli kontrolleri yapıyoruz token için.

Artık bizler SecurityConfig sınıfımızı oluşturabiliriz. Burada da artık Güvenlik konfigürasyonumuzu yapıp bitireceğiz :))).

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

@Autowired
private JwtTokenFilter jwtTokenFilter;


private static final String[] AuthList = {
"/login",
"/register"

};

@Bean
public UserDetailsService userDetailsService() {
return new UserDetailsServiceImpl();
}


@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}


@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}

@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers(HttpMethod.GET,"/user").hasAnyAuthority("USER");
http.authorizeRequests().antMatchers(HttpMethod.GET,"/admin").hasAnyAuthority("ADMIN");
http.cors().and().csrf().disable().authorizeRequests()
. antMatchers(AuthList).permitAll()
.anyRequest().authenticated()
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);

http.addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
;
}
}

Burada WebSecurityConfigurerAdapter class’ından extend aldığınıza dikkat edin. JwtTokenFilter’ımızı çağırıyoruz. Sonrasında ise String array’i dönen AuthList isminde bir değişken oluşturdum. Bunun sebebi altta yapacağım güvenlik işlemlerinde kayıt olma ve giriş yapma işlemlerinin muaf olması. Yoksa sisteme giremeyen biri girememeye devam eder, kayıt olmamış biri kayıt olmamaya devam eder :)))). Bazı Bean’ler var bunları burada oluşturdum. Bcyript şifreleme için hatılarsanız UserServiceImpl kısmında kullandık… ileride kullanacağız. configure metoduna gelelim ilk iki http user ve admin diye iki tane path olacak user’a user, admin’e admin ulaşsın demek oluyor (İşte User ve Admin olacak ikisi de belli yerlere erşecek). Her neyse altta ise dikkat etmemiz gereken nokta AuthList’i unutmayın.

Artık son kısmımıza gelerek controller katmanımıza giriyor ve UsersController class’ımızı oluşturuyoruz.

@CrossOrigin(origins = "*", maxAge = 3600)
@RequestMapping("/")
@RestController
public class UsersController {


private AuthenticationManager authenticationManager;
private TokenManager tokenManager;
private UserDetailsServiceImpl userDetailsService;


@Autowired
public UsersController(AuthenticationManager authenticationManager, TokenManager tokenManager) {
this.authenticationManager = authenticationManager;
this.tokenManager = tokenManager;
}

@PostMapping("login")
public ResponseEntity login(@RequestBody LoginDto loginDto){
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginDto.getUsername(),
loginDto.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final String token = tokenManager.generateToken(authentication);
return ResponseEntity.ok(token);

}


@PostMapping("register")
public ResponseEntity register(@RequestBody RegisterDto registerDto){
userDetailsService.registerUser(registerDto);
return ResponseEntity.ok("Kayıt başarılı");
}

@GetMapping("user")
public String homePage(){
return "Hello, this user";
}

@GetMapping("admin")
public String newPage(){
return "Hello, this is admin";
}

}

Burada yaptığımız işlem temel bir şekilde, öncelikle ihtiyaç duyduğumuz class’larımızı getirmek daha sonrasında gerekli endpointlerimizi tanımlamak. login endpoint’imizde login metodumuz var ve bizler için gelen bilgileri kontol edip, doğru olduğu takdirde geriye token döndürüyor. register endpointimiz var register metodumuz var altında. O ise bizlere kayıt imkanı sağlıyor. Sonra user ve admin olmak üzere iki endpointimiz var(Security config’te tanımlamıştık ya, user’a user, admin’e admin diye işte o).

Veeee şimdi test etme zamanı. Projeyi çalıştıralım ve Postman’i açalım bakalım.

Kayıt olmak için gerekli requesti attık ve kayıt olduk.

Şimdi biraz veri tabanı ile biraz oynama zamanı. Çünkü role eklemek için olsun rol atamak için olsun bir endpoint yok. Haliyle bunu bizzat biz yapacağız, veritabanından.

Sırasıyla çalıştıracağımız komutlar

update users set status='true'
insert into roles (name) values ('USER')
insert into roles (name) values ('ADMIN')
insert into users_roles (user_id,role_id) values (1,1)

Yaptığımız işlem, user status’ını true yap, çünkü default hali false gelecek. Sonra Role tablosuna USER ve ADMIN isminde iki kayıt ekle. Daha sonra ise kayıt olduğumuz (haliyle ilk kayıt olduğu için 1'idli) hesaba USER rolü ver. Şimdi ben bu hesapla login olup(username ve password ile), token’ımı alıp request atacağım. user path’ine erişip admin path’ine erişememeyi umuyorum mantıklı olarak.

Token’ı aldık.

user’a ulaştık.

Vee admin’e ulaşamadık.

Buraya kadardı. Kısa anlatmaya çalıştım ama bazı yerler biraz uzattı yazıyı :))).

--

--