Blog Projesi REST — part 2

Feyyaz Beğen
7 min readJan 29, 2023

--

Merhaba bugün sizlere backend tarafında yaptığım blog projesinin ikinci kısmını anlatacağım.

Projenin son hali buradadır. https://github.com/feyyazbegen/BlogRestService

JWT kısmı bayağı bir önem arz ettiğinden makalemin bu kısmını buraya ayıracağım.

1- JWT Nasıl Çalışır?

  1. Burada son kullanıcı, kullanıcı adını ve şifresini girer.
  2. Tarayıcı bu isteği sunucuya gönderir.
  3. Sunucuya gönderme işlemi başarılı olduysa token üretilir. Burada token’in expires dediğimiz son kullanma tarihi vardır.
  4. Bu adımda token bilgisini client’e verdik. Tokeni artık client saklayacak.
  5. Bu adımda, o kullanıcının artık rolü her ne ise, mesela admin, manager, editor olabilir veya kullanıcının kendisine ait sepete attığı ürünler sayfası da olabilir ona özel gitmesi gereken endpoint’tir.

Normalde token 3 başlıktan oluşur.

  1. Header : Algoritma tipini saklar.
  2. Payload : Kullanıcı bilgilerini saklar. (claims)
  3. Signature : Kimlik doğrulaması içindir.

Geri kalan adımlarda client, server ve tekrar client’e dönerek tokeni bize döndürür.

2- Filtreleme İşlemi Nasıl Olur ?

Şekil-1

Servlet : Client tabanlı web uygulamalarının haberleşmesini yönetmek için kullanılan sınıf, arayüz ve paket topluluğudur. Java’da WEB geliştirmenin kaynağıdır.

Filter tarafına bakacak olursak; Servlet’e ulaşmadan önce araya çeşitli filter’ler konulabilir ve bunlar zincirlenebilir.

Şekil-2

Spring Security mimarisi bu ortama uygun olacak şekilde FilterChainProxy sınıfını eklemiştir. Birden fazla filtreye ihtiyaç olma durumunda da bu sınıfı ana sınıf kabul edip alt sınıflar oluşturarak filter zinciri oluşturabiliriz. Bu sınıf ApplicationContext içinde Bean olarak gelir.

Şimdi JWT’yi bu filtre zincirine dahil edelim.

Öncelikle pom.xml dosyamıza iki adet dependency ekleyelim.

<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>

<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
<version>6.0.1</version>
</dependency>

Şimdi security ve config adında iki paket oluşturalım. Security altında JwtUserDetails sınıfını oluşturalım. Bu sınıf UserDetails arayüzünü implement edecek. Bu arayüz Spring Security’nin sağladığı arayüzdür. Authentication(kimlik doğrulama) tarafından kullanacağımız sınıftır. Entity paketi altındaki User sınıfı buradan bağımsızdır.

@Getter
@Setter
public class JwtUserDetails implements UserDetails {

public Long id;
private String username;
private String password;
private Collection<? extends GrantedAuthority> authorities;

private JwtUserDetails(Long id, String username, String password, Collection<? extends GrantedAuthority> authorities) {
this.id = id;
this.username = username;
this.password = password;
this.authorities = authorities;
}
public static JwtUserDetails create(User user) {
List<GrantedAuthority> authoritiesList = new ArrayList<>();
Set<Role> roles = user.getRoles();
for (Role role : roles){
authoritiesList.add(new SimpleGrantedAuthority(role.getName()));
}
return new JwtUserDetails(user.getId(), user.getUserName(), user.getPassword(), authoritiesList);
}

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

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

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

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

Buradaki create metodu User alıp JwtUserDetails dönmektedir. Kullanıcının birden fazla yetkisi olabileceğinden foreach döngüsü içerisinde yetkileri aldık. Sonra da id, username, password ve yetkiler listesini döndük.

Şimdi JwtUserDetails için UserDetailService oluşturalım. UserDetailsServiceImlp bu arayüz servisini implement etsin. loadUserByUsername metodunu override ettirecektir.

@Service
public class UserDetailsServiceImpl implements UserDetailsService {

private final UserRepository userRepository;

public UserDetailsServiceImpl(UserRepository userRepository) {
this.userRepository = userRepository;
}

@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userRepository.findByUserName(username);
return JwtUserDetails.create(user);
}

public UserDetails loadUserById(Long id) {
User user = userRepository.findById(id).get();
return JwtUserDetails.create(user);
}
}
public interface UserRepository extends JpaRepository<User,Long> {
User findByUserName(String name);
}

İhtiyaç dahilinde loadUserById metodunu da ben oluşturdum.

Şimdi Security paket altında JwtTokenProvider sınıfı oluşturalım.

@Component
public class JwtTokenProvider {

@Value("${app.secret}")
private String APP_SECRET;

@Value("${expires.in}")
private long EXPIRES_IN;

public String generateJwtToken(Authentication auth) {
JwtUserDetails userDetails = (JwtUserDetails) auth.getPrincipal();
Date expireDate = new Date(new Date().getTime() + EXPIRES_IN);
return Jwts.builder().setSubject(Long.toString(userDetails.getId()))
.setIssuedAt(new Date()).setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, APP_SECRET).compact();
}

public String generateJwtTokenByUserId(Long userId) {
Date expireDate = new Date(new Date().getTime() + EXPIRES_IN);
return Jwts.builder().setSubject(Long.toString(userId))
.setIssuedAt(new Date()).setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, APP_SECRET).compact();
}

Long getUserIdFromJwt(String token) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();
return Long.parseLong(claims.getSubject());
}

boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (SignatureException e) {
return false;
} catch (MalformedJwtException e) {
return false;
} catch (ExpiredJwtException e) {
return false;
} catch (UnsupportedJwtException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
}
}

private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getExpiration();
return expiration.before(new Date());
}
}

Burada APP_SECRET ve EXPIRES_IN tanımladık. Bunlar sırasıyla uygulamada kullanacağımız key ve son kullanma tarihidir. Value olarak tanımladığımız application.proporties dosyası içinde şu şekildedir.

app.secret = loginexample 
expires.in = 604800

app.secret : algoritma bu key değerini kullanarak token üretecek.

expires.in : saniye cinsinden 7 günlük son kullanma tarihi verdik.

Teker teker medotları açıklayacak olursak;

public String generateJwtToken(Authentication auth) {
JwtUserDetails userDetails = (JwtUserDetails) auth.getPrincipal();
Date expireDate = new Date(new Date().getTime() + EXPIRES_IN);
return Jwts.builder().setSubject(Long.toString(userDetails.getId()))
.setIssuedAt(new Date()).setExpiration(expireDate)
.signWith(SignatureAlgorithm.HS512, APP_SECRET).compact();
}

.getPrincipal() kısmı kimlik doğrulaması yapacağımız USER.

Date expireDate = new Date() kısmı şuandan expires_in yani 7 gün sonraya kadar generate et demek.

Jwts : jasonwebtoken depenceny ile hazır geldi.

.setSubject : User tarafıdır. User’ın id sini kullandık.

.setUssuedAt : key’in oluşturulma tarihi

Daha sonra kullanılacak algoritma ve key’i tanımlayıp bu metodu oluşturduk.

Long getUserIdFromJwt(String token) {
Claims claims = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody();
return Long.parseLong(claims.getSubject());
}

Bu metotda ise APP_SECRET key’ine göre bu tokeni geri çözüp elimizde IssuedAt, Subject gibi body’ler bulunduruyor. Bir nevi önceki metodu tersten çözüyoruz gibi düşünebiliriz.

boolean validateToken(String token) {
try {
Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token);
return !isTokenExpired(token);
} catch (SignatureException e) {
return false;
} catch (MalformedJwtException e) {
return false;
} catch (ExpiredJwtException e) {
return false;
} catch (UnsupportedJwtException e) {
return false;
} catch (IllegalArgumentException e) {
return false;
}
}

private boolean isTokenExpired(String token) {
Date expiration = Jwts.parser().setSigningKey(APP_SECRET).parseClaimsJws(token).getBody().getExpiration();
return expiration.before(new Date());
}

Burası da tokeni doğrulama yaptığımız yerdir.

Şimdi Securiy paketinde JwtAuthenticationFilter sınıfımızı oluşturalım.

Bu sınıf OncePerRequestFilter sınıfını extends edecek. doFilterInternal metodunu override edecektir. Burada kendimize göre filtreleme işlemi yapacağız.

public class JwtAuthenticationFilter extends OncePerRequestFilter {

@Autowired
JwtTokenProvider jwtTokenProvider;

@Autowired
UserDetailsServiceImpl userDetailsService;

@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
try {
String jwtToken = extractJwtFromRequest(request);
if(StringUtils.hasText(jwtToken) && jwtTokenProvider.validateToken(jwtToken)) {
Long id = jwtTokenProvider.getUserIdFromJwt(jwtToken);
UserDetails user = userDetailsService.loadUserById(id);
if(user != null) {
UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities());
auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(auth);
}
}
} catch(Exception e) {
return;
}
filterChain.doFilter(request, response);
}

private String extractJwtFromRequest(HttpServletRequest request) {
String bearer = request.getHeader("Authorization");
if(StringUtils.hasText(bearer) && bearer.startsWith("Bearer ")) {
return bearer.substring("Bearer".length() + 1);
}
return null;
}
}

extractJwtFromRequest metodunda; Bearer için istek atarken Authorization headeri altında gönderiyoruz. Dolu ve bearer ile mi başlıyor kontrolünden sonra return ediyoruz. UsernamePasswordAuthenticationToken Security ile gelen sınıftır.

Şimdi de Security paketi altındaki nihayet son sınıfımız JwtAuthenticationEntryPoint ekleyelim. Bu sınıf yetkisiz işlemlerde işimize yarayacak. Yine override edilecek metot gelecektir.

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response,
AuthenticationException authException) throws IOException, ServletException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authException.getMessage());
}
}

Artık Config paketine geldik. Buraya kadar yaptıklarımızı tek bir çatı altına toplayalım. Burada SecurityConfig sınıfı oluşturalım. Configuration ve EnableWebSecurity anotasyonlarını verelim.

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private final UserDetailsServiceImpl userDetailsService;
private final JwtAuthenticationEntryPoint handler;

public SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtAuthenticationEntryPoint handler) {
this.userDetailsService = userDetailsService;
this.handler = handler;
}
@Bean
public JwtAuthenticationFilter jwtAuthenticationFilter() {
return new JwtAuthenticationFilter();
}

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

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception{
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}

@Bean
public CorsFilter corsFilter() {
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
CorsConfiguration config = new CorsConfiguration();
config.setAllowCredentials(true);
config.addAllowedOrigin("*");
config.addAllowedHeader("*");
config.addAllowedMethod("OPTIONS");
config.addAllowedMethod("HEAD");
config.addAllowedMethod("GET");
config.addAllowedMethod("PUT");
config.addAllowedMethod("POST");
config.addAllowedMethod("DELETE");
config.addAllowedMethod("PATCH");
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.cors()
.and().csrf().disable().exceptionHandling().authenticationEntryPoint(handler)
.and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()
.authorizeRequests()
.antMatchers("/auth/**")
.permitAll().antMatchers("/admin/**").hasAnyAuthority("ADMIN")
.anyRequest().authenticated();

httpSecurity.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
}
@Bean
public BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}

CorsFilter : Farklı orijinden gelen istekler için bütün metotları tanımladık.

Asıl dikkat edilmesi gereken ve sınıf içindeki her şeyin kullanıldığı metot configure’dir.

.cors() : CorsFilter’i ekler. Bu isimde BEAN varsa onu kullanır.

.crsf() : Genel yapı olarak sitenin açığından faydalanarak siteye sanki o kullanıcıymış gibi erişerek işlem yapasını sağlar. Normalde dissable edilmez. Biz postman tarafında deneyeceğimiz için dissable ettik.

.authorizeRequests() : bize requestleeri nasıl authrize edeyim diye sorar

.andMatchers(“/auth/**”).permitAll() : /auth isteğiyle başlayan her yere her kullanıcı girebilir.

.andMatchers(“/admin/**”).hasAnyAuthority(“ADMIN”) : /admin isteğiyle başlayan her yere sadece admin girebilir.

.addFilterBefore() : Oluşturduğumuz ara filtreyi ekledik.

Şimdi de kodu çalıştıracağımız Controller paketi altında AuthController sınıfı oluşturalım. RestController anotasyonunu verelim.

@RestController
@RequestMapping("/auth")
public class AuthController {

private final AuthenticationManager authenticationManager;
private final JwtTokenProvider jwtTokenProvider;
private final UserService userService;

public AuthController(AuthenticationManager authenticationManager, JwtTokenProvider jwtTokenProvider, UserService userService) {
this.authenticationManager = authenticationManager;
this.jwtTokenProvider = jwtTokenProvider;
this.userService = userService;
}

@PostMapping("/login")
public String login(@RequestBody AuthRequest request){
UsernamePasswordAuthenticationToken authtoken = new UsernamePasswordAuthenticationToken(request.getUserName(),request.getPassword());
Authentication auth = authenticationManager.authenticate(authtoken);
SecurityContextHolder.getContext().setAuthentication(auth);
String jwtToken = jwtTokenProvider.generateJwtToken(auth);
return "Bearer " + jwtToken;
}

@PostMapping("/register")
public ResponseEntity<String> register(@RequestBody CreateUserRequest request){
if(userService.findByUserName(request.getUserName()) !=null){
return new ResponseEntity<>("Username already exist.", HttpStatus.BAD_REQUEST);
}
userService.createUser(request);
return new ResponseEntity<>("User successfully registered.",HttpStatus.OK);
}
}

Login olduğumuzda bize token dönecek. Register tarafında da aynı kullanıcı var mı diye sorguladık ve yeni kullanıcı kaydı oluşturduk.

Çıktı için bir örnek verecek olursak;

JWT tarafı bir hayli uzun ve yorucudur. Buraya kadar okuduysanız teşekkür ederim ❤ Bir sonraki yazımda görüşmek üzere…

--

--