亚洲国产日韩欧美一区二区三区,精品亚洲国产成人av在线,国产99视频精品免视看7,99国产精品久久久久久久成人热,欧美日韩亚洲国产综合乱

Home Java javaTutorial Understanding Spring Security and OAuth

Understanding Spring Security and OAuth

Jan 14, 2025 pm 04:05 PM

In this article we will explore Spring security and will build a authentication system with OAuth 2.0.

Spring Security is a powerful, highly customizable framework for implementing robust authentication and access control mechanisms in Java-based applications. It is a core component of the Spring ecosystem, widely used to secure web applications, REST APIs, and other backend services. With Spring Security, you gain a solid foundation to build and enforce secure practices in your application.


How Spring Security Works

Before diving into how Spring Security operates, it's crucial to understand the request-handling lifecycle in a Java-based web server. Spring Security seamlessly integrates into this lifecycle to secure incoming requests.


Request-Handling Lifecycle with Spring Security

The lifecycle of handling an HTTP request in a Spring-based application with Spring Security involves several stages, each playing a critical role in processing, validating, and securing the request.


1. Client Request

The lifecycle begins when a client (e.g., browser, mobile app, or API tool like Postman) sends an HTTP request to the server.

Example:

GET /api/admin/dashboard HTTP/1.1


2. Servlet Container

The servlet container (e.g., Tomcat) receives the request and delegates it to the DispatcherServlet, the front controller in a Spring application. This is where the application’s processing pipeline starts.


3. Spring Security Filter Chain

Before the DispatcherServlet processes the request, Spring Security's Filter Chain intercepts it. The filter chain is a sequence of filters, each responsible for handling specific security tasks. These filters ensure the request meets authentication and authorization requirements before it reaches the application logic.

Key Filters in the Chain:

  1. Authentication Filters:

    These filters verify if the request contains valid credentials, such as a username/password, a JWT, or session cookies.

  2. Authorization Filters:

    After authentication, these filters ensure the authenticated user has the necessary roles or permissions to access the requested resource.

  3. Other Filters:

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

4. Security Context

If authentication is successful, Spring Security creates an Authentication object and stores it in the SecurityContext. This object, often stored in a thread-local storage, is accessible throughout the request lifecycle.

The Authentication Object:

  • Principal: Represents the authenticated user (e.g., username).

  • Credentials: Includes authentication details like JWT tokens or passwords.

  • Authorities: Contains roles and permissions assigned to the user.

Example Flow in the Filter Chain:

  • A request passes through the authentication filters.

  • If the credentials are valid, the Authentication object is created and added to the SecurityContext.

  • If the credentials are invalid, the ExceptionTranslationFilter sends a 401 Unauthorized response to the client.


5. DispatcherServlet

Once the request successfully passes through the Spring Security Filter Chain, the DispatcherServlet takes over:

  1. Handler Mapping:

    It maps the incoming request to the appropriate controller method based on the URL and HTTP method.

  2. Controller Invocation:

    The mapped controller processes the request and returns the appropriate response, often with help from other Spring components like services and repositories.

How Spring Security Fits Into the Lifecycle

Spring Security integrates itself into this lifecycle through its filters, intercepting requests at the earliest stage. By the time a request reaches the application logic, it has already been authenticated and authorized, ensuring only legitimate traffic gets processed by the core application.


Spring Security’s design ensures that authentication, authorization, and other security measures are handled declaratively, giving developers the flexibility to customize or extend its behavior as needed. It not only enforces best practices but also simplifies the implementation of complex security requirements in modern applications.

Understanding Spring Security and OAuth

Spring Security Components: Beyond the Filter Chain

Having explored the Filter Chain in Spring Security, let’s delve into some other key components that play a pivotal role in the authentication and authorization process.

AuthenticationManager

AuthenticationManager is an interface that defines a single method , authenticate(Authentication authentication) , which is used to verify the credentials of a user and determine if they are valid. You can think of?AuthenticationManager?as a coordinator where you can register multiple providers, and based on the request type, it will deliver an authentication request to the correct provider.

AuthenticationProvider

An AuthenticationProvider is an interface that defines a contract for authenticating users based on their credentials. It represents a specific authentication mechanism, such as username/password, OAuth, or LDAP. Multiple AuthenticationProvider implementations can coexist, allowing the application to support various authentication strategies.

Core Concepts:

  1. Authentication Object:

    The AuthenticationProvider processes an Authentication object, which encapsulates the user’s credentials (e.g., username and password).

  2. authenticate Method:

    Each AuthenticationProvider implements the authenticate(Authentication authentication) method, where the actual authentication logic resides. This method:

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
  1. supports Method: The supports(Class authentication) method indicates whether the AuthenticationProvider can handle the given type of Authentication. This allows Spring Security to determine the correct provider to handle specific authentication requests.

Example:

  • A database-backed AuthenticationProvider validates usernames and passwords.

  • An OAuth-based AuthenticationProvider validates tokens issued by an external identity provider.

UserDetailsService

UserDetailsService?is described as a core interface that loads user-specific data in the Spring documentation It contains a single method loadUserByUsername which accepts username as parameter and returns the ==User== identity object . Basically we create and implementation class of UserDetailsService in which we override the loadUserByUsername method.

* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

Now how all these three works together is AuthenticationManager will ask AuthenticationProvider to carry on the authentication according to the type of Provider specified and the UserDetailsService implementation will help the AuthenticationProvider in proving the userdetails .

Now before moving to the configuration and all stuff here's a concise flow of Spring Security for JWT-based authentication:

1. User Request

  • The user sends a request to the authenticated endpoint with their credentials (username and password) or a JWT token (in the header) and the request is passed to the Authentication Filter

  • AuthenticationFilter (e.g., UsernamePasswordAuthenticationFilter):

    • Handles user authentication based on credentials submitted (typically in the form of a username and password). This is where the UsernamePasswordAuthenticationFilter comes into play.
    • It listens for the request , extracts the username and password, and passes them to the AuthenticationManager.
    • But we are not passing the username and password , we are giving only the token hence there should be a filter before this AuthenticationFilter which will tell the Authentication Process that the user is authenticated and no need to check for Username and password and this is done by creating a JWTFilter

2. JWTFilter

This custom filter extends OncePerRequestFilter and is placed before the UsernamePasswordAuthenticationFilter , and what it do is it extracts the token from request and validates it.

If the token is valid it creates a UsernamePasswordAuthenticationToken and sets that token into the Security Context which tells the spring security that the request is authenticated and when this request passes to the UsernamePasswordAuthenticationFilter it just passes as it has the UsernamePasswordAuthenticationToken

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

this UsernamePasswordAuthenticationToken is generated with the help of the AuthenticationManager and AuthenticationProvider if we have passed username and password instead of the token after authenticating the username and password with the help of out UserDetails class.

3. AuthenticationManager

  • AuthenticationManager: This receives the authentication request and delegates it to the appropriate AuthenticationProvider which we configure.
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.

4. AuthenticationProvider

  • UserDetailsService: The AuthenticationProvider uses UserDetailsService to load user details based on the username. And we provide this with a implementation of UserDetailsService

  • Credential Validation: It compares the provided password with the one stored in the user details (typically using a PasswordEncoder).

package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}

Now all these different filters and beans are needed to be configured so that Spring security knows what to do hence we create a configuration class where we specify all the configuration.

@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}

till now we have understand and configured our authentication with the help of spring security now it’s time to test it.

We will create a simple app with two controllers AuthController (handles login and register) and ProductController (dummy protected controller)

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.
* Validates the user’s credentials.

* Returns an authenticated `Authentication` object upon success.

* Throws an `AuthenticationException` if authentication fails.
package com.oauth.backend.services;

import com.oauth.backend.entities.User;
import com.oauth.backend.repositories.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;


@Component
public class CustomUserDetailsService implements UserDetailsService {
    private final UserRepository userRepository;

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

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {

        User user = userRepository.findByUsername(username);
        if(user==null){
            throw new UsernameNotFoundException(username);
        }
        return new UserDetailsImpl(user);
    }
    public UserDetails loadUserByEmail(String email) throws UsernameNotFoundException {
        User user = userRepository.findByEmail(email);
        if(user==null){
            throw new UsernameNotFoundException(email);
        }
        return new UserDetailsImpl(user);
    }
}
@Component
public class JWTFilter extends OncePerRequestFilter {

    private final JWTService jwtService;
    private final UserDetailsService userDetailsService;
    public JWTFilter(JWTService jwtService,UserDetailsService userDetailsService) {
        this.jwtService = jwtService;
        this.userDetailsService = userDetailsService;
    }
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        final String authHeader = request.getHeader("Authorization");

        if(authHeader == null || !authHeader.startsWith("Bearer")) {
            filterChain.doFilter(request,response);
            return;
        }

        final String jwt = authHeader.substring(7);
        final String userName = jwtService.extractUserName(jwt);

        Authentication authentication
                = SecurityContextHolder.getContext().getAuthentication();

        if(userName !=null  && authentication == null) {
            //Authenticate
            UserDetails userDetails
                    = userDetailsService.loadUserByUsername(userName);

            if(jwtService.isTokenValid(jwt,userDetails)) {
                UsernamePasswordAuthenticationToken authenticationToken
                        = new UsernamePasswordAuthenticationToken(
                        userDetails,
                        null,
                        userDetails.getAuthorities()
                );

                SecurityContextHolder.getContext()
                        .setAuthentication(authenticationToken);
            }
        }

        filterChain.doFilter(request,response);
    }



}
@Bean  
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{  
return config.getAuthenticationManager();  
}
@Bean  
public AuthenticationProvider authenticationProvider(){  
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();  
authenticationProvider.setUserDetailsService(userDetailsServiceImpl);  
authenticationProvider.setPasswordEncoder(passwordEncoder);  
return authenticationProvider;  
}

till now we have implemented a registration , login and verification but what if i also want to add Login With Google/Github fuctionality then we can do it with the help of OAuth2.0

OAuth 2.0

OAuth 2.0 is a protocol made for authorization through which enables users to grant third party applications access to resources stored on other platforms (e.g Google Drive , Github) without sharing the credentials of those platforms.

It is mostly used in enabling social logins like “Login with google” , “Login with github ” .

Platforms like Google , Facebook , Github provides Authorization server which implements OAuth 2.0 protocol for this social sign in or authorizing access .

Key Concepts of OAuth 2.0

  • Resource Owner

  • Client

  • Authorization Server

  • Resource Server

  • Access Token

  • Scopes

  • Grants

Now we will look into each concept one by one

Resource Owner

Resource owner is the user who wants to authorize the third party application (Your Application).

Client

It is your (third party) application which wants to access the data or resource from the resource server.

Resource Server

It is the server where the user’s data is stored and is to be accessed by the third party application.

Authorization Server

The server that authenticates the resource owner and issues access tokens to the client (e.g., Google Accounts).

Access Token

A credential issued by the authorization server to the client, allowing it to access the resource server on behalf of the user. It is generally short lived that is expires very soon so a refresh token is also provided in order to refresh this access token so that user doesn’t need to authorize again.

Scopes

Specific permissions granted by the user, defining what the client can and cannot do with the user's data. For example for authorization we only need user info like profile , name etc. but for file access different scope is required.

Grants

It refers to the methods by which Client application can obtain the access token from the Authorization Server. A grant defines the process and conditions under which the client application is authorized to access a resource owner's protected data.

It is secure as we do not need to expose our client secret and other credentials to the browser

There are two mostly used Grant types provided by OAuth 2.0

  1. Authorization Code Grant

    It is the most used type of grant/method , most secure and is for server-side applications

    In this a authorization code is given by the client to the backend and the backend gives the access token to the client.

    Process:

    1. The client redirects the user to the authorization server.
    2. The user logs in and consents.
    3. The authorization server issues an authorization code.
    4. The client exchanges the authorization code with the backend for an access token.
  2. Implicit Grant

    Used by single-page apps (SPAs) or applications without a backend. In this the access token is directly generated and issued in the browser itself.

    Process:

    1. The client redirects the user to the authorization server.
    2. The user logs in and consents.
    3. The authorization server directly issues an access token.

We will implement both separately for complete understanding , but first we will implement the Authorization Code Grant for that we will need

  1. Authorization Server

    It can be either of a platform (like google , github) or you can create your own also using KeyCloak or can also build your own adhering to OAuth 2.0 standards ( we may do this in next blog ?)

  2. Spring Boot Application

    This will be our main backend application/service which will handle all the operations like code exchange, verification , saving user details and assigning JWT tokens

  3. React Application (Frontend)

    This will be our client which will redirect the user to Authorization Server for authorization.

So in our implementation what we will be doing is the frontend(web/app) will redirect our user to google login with redirect uri to our backend endpoint which will take control further we will talki about it later and along with the redirect_url we also be passing the client id of our app all these will be sent in the query parameters.

No when the user will successfully login in the google the authrization server(google’s) will redirect our request to the backend enpoint and there what we will be doing is exchanging Authorization code with authorization server to get access token and refresh token and then we can handle the auth as we want and finally we will send a response back to our frontend which will have a cookie and redirect to our dashboard or may be a protected page.

Now we will look into the code , but make sure that you add the url of your backend endpoint in authorized redirect urls in Google console dashboard for OAuth client.

* **CsrfFilter**: Validates CSRF tokens to prevent Cross-Site Request Forgery attacks.

* **CorsFilter**: Manages Cross-Origin Resource Sharing (CORS) rules for secure API access from different domains.

* **ExceptionTranslationFilter**: Handles security-related exceptions (e.g., invalid credentials) and sends appropriate responses to the client.

and that’s it this will work fine and for testing you can made a simple frontend application which will nothing but having a context and yout know login and registration functions.

Thanks for reading till this long , and if you have any suggestion , please drop it in the comments

The above is the detailed content of Understanding Spring Security and OAuth. For more information, please follow other related articles on the PHP Chinese website!

Statement of this Website
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn

Hot AI Tools

Undress AI Tool

Undress AI Tool

Undress images for free

Undresser.AI Undress

Undresser.AI Undress

AI-powered app for creating realistic nude photos

AI Clothes Remover

AI Clothes Remover

Online AI tool for removing clothes from photos.

Clothoff.io

Clothoff.io

AI clothes remover

Video Face Swap

Video Face Swap

Swap faces in any video effortlessly with our completely free AI face swap tool!

Hot Tools

Notepad++7.3.1

Notepad++7.3.1

Easy-to-use and free code editor

SublimeText3 Chinese version

SublimeText3 Chinese version

Chinese version, very easy to use

Zend Studio 13.0.1

Zend Studio 13.0.1

Powerful PHP integrated development environment

Dreamweaver CS6

Dreamweaver CS6

Visual web development tools

SublimeText3 Mac version

SublimeText3 Mac version

God-level code editing software (SublimeText3)

Hot Topics

PHP Tutorial
1488
72
Differences Between Callable and Runnable in Java Differences Between Callable and Runnable in Java Jul 04, 2025 am 02:50 AM

There are three main differences between Callable and Runnable in Java. First, the callable method can return the result, suitable for tasks that need to return values, such as Callable; while the run() method of Runnable has no return value, suitable for tasks that do not need to return, such as logging. Second, Callable allows to throw checked exceptions to facilitate error transmission; while Runnable must handle exceptions internally. Third, Runnable can be directly passed to Thread or ExecutorService, while Callable can only be submitted to ExecutorService and returns the Future object to

Asynchronous Programming Techniques in Modern Java Asynchronous Programming Techniques in Modern Java Jul 07, 2025 am 02:24 AM

Java supports asynchronous programming including the use of CompletableFuture, responsive streams (such as ProjectReactor), and virtual threads in Java19. 1.CompletableFuture improves code readability and maintenance through chain calls, and supports task orchestration and exception handling; 2. ProjectReactor provides Mono and Flux types to implement responsive programming, with backpressure mechanism and rich operators; 3. Virtual threads reduce concurrency costs, are suitable for I/O-intensive tasks, and are lighter and easier to expand than traditional platform threads. Each method has applicable scenarios, and appropriate tools should be selected according to your needs and mixed models should be avoided to maintain simplicity

Best Practices for Using Enums in Java Best Practices for Using Enums in Java Jul 07, 2025 am 02:35 AM

In Java, enums are suitable for representing fixed constant sets. Best practices include: 1. Use enum to represent fixed state or options to improve type safety and readability; 2. Add properties and methods to enums to enhance flexibility, such as defining fields, constructors, helper methods, etc.; 3. Use EnumMap and EnumSet to improve performance and type safety because they are more efficient based on arrays; 4. Avoid abuse of enums, such as dynamic values, frequent changes or complex logic scenarios, which should be replaced by other methods. Correct use of enum can improve code quality and reduce errors, but you need to pay attention to its applicable boundaries.

Understanding Java NIO and Its Advantages Understanding Java NIO and Its Advantages Jul 08, 2025 am 02:55 AM

JavaNIO is a new IOAPI introduced by Java 1.4. 1) is aimed at buffers and channels, 2) contains Buffer, Channel and Selector core components, 3) supports non-blocking mode, and 4) handles concurrent connections more efficiently than traditional IO. Its advantages are reflected in: 1) Non-blocking IO reduces thread overhead, 2) Buffer improves data transmission efficiency, 3) Selector realizes multiplexing, and 4) Memory mapping speeds up file reading and writing. Note when using: 1) The flip/clear operation of the Buffer is easy to be confused, 2) Incomplete data needs to be processed manually without blocking, 3) Selector registration must be canceled in time, 4) NIO is not suitable for all scenarios.

How Java ClassLoaders Work Internally How Java ClassLoaders Work Internally Jul 06, 2025 am 02:53 AM

Java's class loading mechanism is implemented through ClassLoader, and its core workflow is divided into three stages: loading, linking and initialization. During the loading phase, ClassLoader dynamically reads the bytecode of the class and creates Class objects; links include verifying the correctness of the class, allocating memory to static variables, and parsing symbol references; initialization performs static code blocks and static variable assignments. Class loading adopts the parent delegation model, and prioritizes the parent class loader to find classes, and try Bootstrap, Extension, and ApplicationClassLoader in turn to ensure that the core class library is safe and avoids duplicate loading. Developers can customize ClassLoader, such as URLClassL

Exploring Different Synchronization Mechanisms in Java Exploring Different Synchronization Mechanisms in Java Jul 04, 2025 am 02:53 AM

Javaprovidesmultiplesynchronizationtoolsforthreadsafety.1.synchronizedblocksensuremutualexclusionbylockingmethodsorspecificcodesections.2.ReentrantLockoffersadvancedcontrol,includingtryLockandfairnesspolicies.3.Conditionvariablesallowthreadstowaitfor

Handling Common Java Exceptions Effectively Handling Common Java Exceptions Effectively Jul 05, 2025 am 02:35 AM

The key to Java exception handling is to distinguish between checked and unchecked exceptions and use try-catch, finally and logging reasonably. 1. Checked exceptions such as IOException need to be forced to handle, which is suitable for expected external problems; 2. Unchecked exceptions such as NullPointerException are usually caused by program logic errors and are runtime errors; 3. When catching exceptions, they should be specific and clear to avoid general capture of Exception; 4. It is recommended to use try-with-resources to automatically close resources to reduce manual cleaning of code; 5. In exception handling, detailed information should be recorded in combination with log frameworks to facilitate later

How does a HashMap work internally in Java? How does a HashMap work internally in Java? Jul 15, 2025 am 03:10 AM

HashMap implements key-value pair storage through hash tables in Java, and its core lies in quickly positioning data locations. 1. First use the hashCode() method of the key to generate a hash value and convert it into an array index through bit operations; 2. Different objects may generate the same hash value, resulting in conflicts. At this time, the node is mounted in the form of a linked list. After JDK8, the linked list is too long (default length 8) and it will be converted to a red and black tree to improve efficiency; 3. When using a custom class as a key, the equals() and hashCode() methods must be rewritten; 4. HashMap dynamically expands capacity. When the number of elements exceeds the capacity and multiplies by the load factor (default 0.75), expand and rehash; 5. HashMap is not thread-safe, and Concu should be used in multithreaded

See all articles