From connections: www.callicoder.com/spring-boot… More articles by Rajeev Kumar Singh: www.callicoder.com/

Making: github.com/callicoder/…

1. Security architecture overview

  • Implement registration API by filling in name, username, email and password
  • Implement login API by filling in user name (or email) and password.
    • After the user information is authenticated successfully, the server generates a JWT authentication token and returns it to the client.
    • Clients access specific resources by passing a JWT token in the Authorization request header.
  • Configure Spring Security to restrict access to specific resources, such as:
    • Login, registration and all static resources are completely open
    • Other specific resources are available only to authenticated users
  • Configure Spring Security to throw a 401 unauthorized error when accessing a specific resource without valid JWT token authentication
  • Configure role-based authorization to protect server resources.

2. Configure Spring Security and JWT

code

SecurityConfig

package com.example.polls.config;

import com.example.polls.security.CustomUserDetailsService;
import com.example.polls.security.JwtAuthenticationEntryPoint;
import com.example.polls.security.JwtAuthenticationFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.BeanIds;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;


@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(
        securedEnabled = true,
        jsr250Enabled = true,
        prePostEnabled = true
)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    CustomUserDetailsService customUserDetailsService;

    @Autowired
    private JwtAuthenticationEntryPoint unauthorizedHandler;

    @Bean
    public JwtAuthenticationFilter jwtAuthenticationFilter(a) {
        return new JwtAuthenticationFilter();
    }

    @Override
    public void configure(AuthenticationManagerBuilder authenticationManagerBuilder) throws Exception {
        authenticationManagerBuilder
                .userDetailsService(customUserDetailsService)
                .passwordEncoder(passwordEncoder());
    }

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

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .cors()
                    .and()
                .csrf()
                    .disable()
                .exceptionHandling()
                    .authenticationEntryPoint(unauthorizedHandler)
                    .and()
                .sessionManagement()
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    .and()
                .authorizeRequests()
                    .antMatchers("/"."/favicon.ico"."/**/*.png"."/**/*.gif"."/**/*.svg"."/**/*.jpg"."/**/*.html"."/**/*.css"."/**/*.js")
                        .permitAll()
                    .antMatchers("/api/auth/**")
                        .permitAll()
                    .antMatchers("/api/user/checkUsernameAvailability"."/api/user/checkEmailAvailability")
                        .permitAll()
                    .antMatchers(HttpMethod.GET, "/api/polls/**"."/api/users/**")
                        .permitAll()
                    .anyRequest()
                        .authenticated();

        // Add our custom JWT security filterhttp.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class); }}Copy the code
Code reading
1. @EnableWebSecurity
  • Open the web security
2. @EnableGlobalMethodSecurity
  • Enable method-level protection based on assertions. There are three types:
    • SecuredEnabled, which enables @Secured assertion, protects controller/ Service methods:
        @Secured("ROLE_ADMIN")
        public User getAllUsers(a) {}
    
        @Secured({"ROLE_USER"."ROLE_ADMIN"})
        public User getUser(Long id) {}
    
        @Secured("IS_AUTHENTICATED_ANONYMOUSLY")
        public boolean isUsernameAvailable(a) {}
    Copy the code
    • Jsr250Enabled, enable @rolesAllowed assertion:
        @RolesAllowed("ROLE_ADMIN")
        public Poll createPoll(a) {} 
    Copy the code
    • PrePostEnabled, which enables more complex permission control expressions controlled by @preauthorize and @postauthorize:
        @PreAuthorize("isAnonymous()")
        public boolean isUsernameAvailable(a) {}
    
        @PreAuthorize("hasRole('USER')")
        public Poll createPoll(a) {}    
    Copy the code
3. WebSecurityConfigurerAdapter
  • Provides a default security configuration, allowing other classes to inherit and customize the security configuration by overriding methods.
  • SecurityConfig inherits from this class and overrides some methods to customize the security configuration.
4. CustomUserDetailsService
  • The interface UserDetailsService is implemented.
  • User authentication is implemented by overriding the loadUserByUsername method of the interface.
5. JwtAuthenticationEntryPoint
  • Implement the AuthenticationEntryPoint interface.
  • Returns a 401 unauthorized error for users accessing a particular resource without authentication.
6. JwtAuthenticationFilter
  • Get the JWT token from the Authorization request header of all requests
  • Authentication token
  • Load token-related user information
  • Set up user information in Spring Security’s Security container. Spring Security uses this user information to perform authorization authentication. We can manipulate our business logic in the Controller by retrieving user information from the security container.
7. AuthenticationManagerBuilder and the AuthenticationManager
  • AuthenticationManager is the main interface for implementing user authentication in Spring Security
  • AuthenticationManagerBuilder responsible for generating the AuthenticationManager
  • Can be established through AuthenticationManagerBuilder certification based on memory, LDAP authentication, JDBC certification, or add your own custom authentication.
  • In this example, we provide the customUserDetailsService and passwordEncoder to build the AuthenticationManager.
  • Authenticate the user in the login API through the configured AuthenticationManager.
8. HttpSecurity configurations
  • The HttpSecurity configuration includes security configuration functions such as CSRF, session management, and resource protection policies based on different conditions.
  • In this example, we develop static resources and some public apis, while other resources are restricted to authenticated users.
  • We joined in the configuration JWTAuthenticationFilter JWTAuthenticationEntryPoint and custom.

3. Create custom Spring Security Classes, Filters, and Annotations

3.1. Customize Spring Security AuthenticationEntryPoint

  • Define JwtAuthenticationEntryPoint AuthenticationEntryPoint interface and its method of commence.
  • The commence method is triggered when an unauthenticated user view accesses a resource that needs to be authenticated.
  • In this example, we simply return the 401 error with the exception information.
JwtAuthenticationEntryPoint
package com.example.polls.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@Component
public class JwtAuthenticationEntryPoint implements AuthenticationEntryPoint {

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationEntryPoint.class);
    @Override
    public void commence(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        logger.error("Responding with unauthorized error. Message - {}", e.getMessage()); httpServletResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED, e.getMessage()); }}Copy the code

3.2. Customize Spring Security UserDetails

  • The UserPrincipal implements the UserDetails interface.
  • The custom UserDetailsService interface returns this object.
  • Spring Security uses this information for authentication and authorization operations.
UserPrincipal
package com.example.polls.security;

import com.example.polls.model.User;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Collectors;

public class UserPrincipal implements UserDetails {
    private Long id;

    private String name;

    private String username;

    @JsonIgnore
    private String email;

    @JsonIgnore
    private String password;

    private Collection<? extends GrantedAuthority> authorities;

    public UserPrincipal(Long id, String name, String username, String email, String password, Collection<? extends GrantedAuthority> authorities) {
        this.id = id;
        this.name = name;
        this.username = username;
        this.email = email;
        this.password = password;
        this.authorities = authorities;
    }

    public static UserPrincipal create(User user) {
        List<GrantedAuthority> authorities = user.getRoles().stream().map(role ->
                new SimpleGrantedAuthority(role.getName().name())
        ).collect(Collectors.toList());

        return new UserPrincipal(
                user.getId(),
                user.getName(),
                user.getUsername(),
                user.getEmail(),
                user.getPassword(),
                authorities
        );
    }

    public Long getId(a) {
        return id;
    }

    public String getName(a) {
        return name;
    }

    public String getEmail(a) {
        return email;
    }

    @Override
    public String getUsername(a) {
        return username;
    }

    @Override
    public String getPassword(a) {
        return password;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return authorities;
    }

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

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

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

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

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null|| getClass() ! = o.getClass())return false;
        UserPrincipal that = (UserPrincipal) o;
        return Objects.equals(id, that.id);
    }

    @Override
    public int hashCode(a) {

        returnObjects.hash(id); }}Copy the code

3.3. Customize Spring Security UserDetailsService

  • Load user data by user name
CustomUserDetailsService
package com.example.polls.security;

import com.example.polls.exception.ResourceNotFoundException;
import com.example.polls.model.User;
import com.example.polls.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
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.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class CustomUserDetailsService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    @Transactional
    public UserDetails loadUserByUsername(String usernameOrEmail)
            throws UsernameNotFoundException {
        User user = userRepository.findByUsernameOrEmail(usernameOrEmail, usernameOrEmail)
                .orElseThrow(() ->
                        new UsernameNotFoundException("User not found with username or email : " + usernameOrEmail)
        );

        return UserPrincipal.create(user);
    }

    @Transactional
    public UserDetails loadUserById(Long id) {
        User user = userRepository.findById(id).orElseThrow(
            () -> new ResourceNotFoundException("User"."id", id)
        );

        returnUserPrincipal.create(user); }}Copy the code

3.4. Generate and validate JWT utility classes

  • The JwtTokenProvider generates the JWT after a successful user login and is responsible for verifying the JWT in the Authorization request header.
  • Define the JWT key and expiration time in application.properties
## jwt Properties
app.jwtSecret= JWTSuperSecretKey
app.jwtExpirationInMs = 604800000
Copy the code
JwtTokenProvider
package com.example.polls.security;

import io.jsonwebtoken.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

import java.util.Date;

@Component
public class JwtTokenProvider {

    private static final Logger logger = LoggerFactory.getLogger(JwtTokenProvider.class);

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

    @Value("${app.jwtExpirationInMs}")
    private int jwtExpirationInMs;

    public String generateToken(Authentication authentication) {

        UserPrincipal userPrincipal = (UserPrincipal) authentication.getPrincipal();

        Date now = new Date();
        Date expiryDate = new Date(now.getTime() + jwtExpirationInMs);

        return Jwts.builder()
                .setSubject(Long.toString(userPrincipal.getId()))
                .setIssuedAt(new Date())
                .setExpiration(expiryDate)
                .signWith(SignatureAlgorithm.HS512, jwtSecret)
                .compact();
    }

    public Long getUserIdFromJWT(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(jwtSecret)
                .parseClaimsJws(token)
                .getBody();

        return Long.parseLong(claims.getSubject());
    }

    public boolean validateToken(String authToken) {
        try {
            Jwts.parser().setSigningKey(jwtSecret).parseClaimsJws(authToken);
            return true;
        } catch (SignatureException ex) {
            logger.error("Invalid JWT signature");
        } catch (MalformedJwtException ex) {
            logger.error("Invalid JWT token");
        } catch (ExpiredJwtException ex) {
            logger.error("Expired JWT token");
        } catch (UnsupportedJwtException ex) {
            logger.error("Unsupported JWT token");
        } catch (IllegalArgumentException ex) {
            logger.error("JWT claims string is empty.");
        }
        return false; }}Copy the code

3.5. Customize Spring Security AuthenticationFilter

  • The JWTAuthenticationFilter retrieves the JWT token from the request, validates it, loads token-related user information, and passes it to Spring Security.
JwtAuthenticationFilter
package com.example.polls.security;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class JwtAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private JwtTokenProvider tokenProvider;

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    private static final Logger logger = LoggerFactory.getLogger(JwtAuthenticationFilter.class);

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        try {
            String jwt = getJwtFromRequest(request);

            if (StringUtils.hasText(jwt) && tokenProvider.validateToken(jwt)) {
                Long userId = tokenProvider.getUserIdFromJWT(jwt);

                // Both user names and role information can be encoded into JWT Claims
                // UserDetails information is then created by parsing the CLAIMS object of JWT
                // Avoid repeated queries to the database

                UserDetails userDetails = customUserDetailsService.loadUserById(userId);
                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
                authentication.setDetails(newWebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(authentication); }}catch (Exception ex) {
            logger.error("Could not set user authentication in security context", ex);
        }

        filterChain.doFilter(request, response);
    }

    private String getJwtFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7, bearerToken.length());
        }
        return null; }}Copy the code

3.6. Custom assertions get the current user

  • Spring Security provides the @AuthenticationPrincipal assertion to get the currently authenticated user in the Controller
  • The CurrentUser assertion encapsulates the AuthenticationPrincipal
CurrentUser
package com.example.polls.security;

import org.springframework.security.core.annotation.AuthenticationPrincipal;
import java.lang.annotation.*;

@Target({ElementType.PARAMETER, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal
public @interface CurrentUser {

}
Copy the code
  • By creating meta-assertions, you can avoid over-binding Spring Security assertions in your project and reduce your dependency on Spring Security. When you need to remove Spring Security, you can simply modify the CurrentUser assertion.

4. Login to the registration API

4.1. The request Payloads

LoginRequest
import javax.validation.constraints.NotBlank;

import lombok.Data;

@Data
public class LoginRequest {
	@NotBlank
	private String usernameOrEmail;
	@NotBlank
	private String password;
}
Copy the code
SignUpRequest
import javax.validation.constraints.*;

import lombok.Data;

@Data
public class SignUpRequest {
    @NotBlank
    @Size(min = 4, max = 40)
    private String name;

    @NotBlank
    @Size(min = 3, max = 15)
    private String username;

    @NotBlank
    @Size(max = 40)
    @Email
    private String email;

    @NotBlank
    @Size(min = 6, max = 20)
    private String password;
    
    // TODO:Mobile phone no.
}

Copy the code

4.2. The response of Payloads

JwtAuthenticationResponse
import lombok.Data;

@Data
public class JwtAuthenticationResponse {
	private String accessToken;
	private String tokenType = "Bearer";

	public JwtAuthenticationResponse(String accessToken) {
		this.accessToken = accessToken; }}Copy the code
ApiResponse
import lombok.Data;

@Data
public class ApiResponse {
    private Boolean success;
    private String message;

    public ApiResponse(Boolean success, String message) {
        this.success = success;
        this.message = message; }}Copy the code

4.3. User-defined service exception

AppException
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public class AppException extends RuntimeException {
    public AppException(String message) {
        super(message);
    }

    public AppException(String message, Throwable cause) {
        super(message, cause); }}Copy the code
BadRequestException
package com.example.polls.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.BAD_REQUEST)
public class BadRequestException extends RuntimeException {

    public BadRequestException(String message) {
        super(message);
    }

    public BadRequestException(String message, Throwable cause) {
        super(message, cause); }}Copy the code
ResourceNotFoundException
package com.example.polls.exception;

import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;

@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
    private String resourceName;
    private String fieldName;
    private Object fieldValue;

    public ResourceNotFoundException( String resourceName, String fieldName, Object fieldValue) {
        super(String.format("%s not found with %s : '%s'", resourceName, fieldName, fieldValue));
        this.resourceName = resourceName;
        this.fieldName = fieldName;
        this.fieldValue = fieldValue;
    }

    public String getResourceName(a) {
        return resourceName;
    }

    public String getFieldName(a) {
        return fieldName;
    }

    public Object getFieldValue(a) {
        returnfieldValue; }}Copy the code

4.4. Certification Controller

AuthController
package com.example.polls.controller;

import com.example.polls.exception.AppException;
import com.example.polls.model.Role;
import com.example.polls.model.RoleName;
import com.example.polls.model.User;
import com.example.polls.payload.ApiResponse;
import com.example.polls.payload.JwtAuthenticationResponse;
import com.example.polls.payload.LoginRequest;
import com.example.polls.payload.SignUpRequest;
import com.example.polls.repository.RoleRepository;
import com.example.polls.repository.UserRepository;
import com.example.polls.security.JwtTokenProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;

import javax.validation.Valid;
import java.net.URI;
import java.util.Collections;

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

    @Autowired
    AuthenticationManager authenticationManager;

    @Autowired
    UserRepository userRepository;

    @Autowired
    RoleRepository roleRepository;

    @Autowired
    PasswordEncoder passwordEncoder;

    @Autowired
    JwtTokenProvider tokenProvider;

    @PostMapping("/signin")
    publicResponseEntity<? > authenticateUser(@Valid @RequestBody LoginRequest loginRequest) {

        Authentication authentication = authenticationManager.authenticate(
                new UsernamePasswordAuthenticationToken(
                        loginRequest.getUsernameOrEmail(),
                        loginRequest.getPassword()
                )
        );

        SecurityContextHolder.getContext().setAuthentication(authentication);

        String jwt = tokenProvider.generateToken(authentication);
        return ResponseEntity.ok(new JwtAuthenticationResponse(jwt));
    }

    @PostMapping("/signup")
    publicResponseEntity<? > registerUser(@Valid @RequestBody SignUpRequest signUpRequest) {
        if(userRepository.existsByUsername(signUpRequest.getUsername())) {
            return new ResponseEntity(new ApiResponse(false."Username is already taken!"),
                    HttpStatus.BAD_REQUEST);
        }

        if(userRepository.existsByEmail(signUpRequest.getEmail())) {
            return new ResponseEntity(new ApiResponse(false."Email Address already in use!"),
                    HttpStatus.BAD_REQUEST);
        }

        // Creating user's account
        User user = new User(signUpRequest.getName(), signUpRequest.getUsername(),
                signUpRequest.getEmail(), signUpRequest.getPassword());

        user.setPassword(passwordEncoder.encode(user.getPassword()));

        Role userRole = roleRepository.findByName(RoleName.ROLE_USER)
                .orElseThrow(() -> new AppException("User Role not set."));

        user.setRoles(Collections.singleton(userRole));

        User result = userRepository.save(user);

        URI location = ServletUriComponentsBuilder
                .fromCurrentContextPath().path("/users/{username}")
                .buildAndExpand(result.getUsername()).toUri();

        return ResponseEntity.created(location).body(new ApiResponse(true."User registered successfully")); }}Copy the code

5. Read more

JWT Security Authentication and Refresh Token