GitHub source address: github.com/yifanzheng/…

An overview of the

Spring Security is a powerful and highly customizable authentication and access control framework within the Spring Family bucket. As with all Spring projects, Spring Security can be easily extended to meet custom requirements.

Because Spring Security is so powerful and difficult to get started with compared to other technologies, many developers who are new to Spring Security have a hard time using it through documentation or videos.

During my internship in the company, A project I came into contact with used Spring Security, a powerful Security verification framework, to complete the user login module, which WAS also a module I was in charge of myself. At that time, I was not familiar with Spring Security, so it could be said that it was the first time for me to contact with it. I looked up a lot of information about Spring Security, which was vaguely understood. It took me nearly a week to finish it under the guidance of my tutor.

Spring Security is really difficult for beginners to get started with. Therefore, I learned this part of knowledge in my spare time, and realized a simple project, mainly using Spring Boot technology to integrate Spring Security and Spring Data Jpa technology. The implementation of this project is relatively simple, and there are still many areas to be optimized. I hope friends who are interested can improve it together. I look forward to your PR.

The project download

  • Git clone github.com/yifanzheng/… .

  • Configure the Maven repository and open the project using the IntelliJ IDEA tool.

  • Change the database information to your own in the application.properties configuration file.

Access control

This Demo permission control uses RBAC idea. Simply put, a user has several roles, with which the user forms a many-to-many relationship.

model

Data table design

The user table and user role table are many-to-many relationships. Because this is simpler, the table design is a little redundant. Friends can redesign according to the actual situation.

Data interaction

User login -> back end verifies login and returns token -> Front end requests back end data with token -> back end returns data.

Project core class description

WebCorsConfiguration

WebCorsConfiguration is used to solve the cross-domain problem of HTTP requests. It is important to note that the client cannot obtain Token information without exposing the Authorization header field to the client.

/** * WebCorsConfiguration Cross-domain configuration **@author star
 */
@Configuration
public class WebCorsConfiguration implements WebMvcConfigurer {

    /** * Set swagger to the default home page */
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("redirect:/swagger-ui.html");
        registry.setOrder(Ordered.HIGHEST_PRECEDENCE);
        WebMvcConfigurer.super.addViewControllers(registry);
    }

    @Bean
    public CorsFilter corsFilter(a) {
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        CorsConfiguration config = new CorsConfiguration();
        config.setAllowCredentials(true);
        config.setAllowedOrigins(Collections.singletonList("*"));
        config.setAllowedMethods(Collections.singletonList("*"));
        config.setAllowedHeaders(Collections.singletonList("*"));
        // Expose other attributes in the header to the client application
        config.setExposedHeaders(Arrays.asList(
                "Authorization"."X-Total-Count"."Link"."Access-Control-Allow-Origin"."Access-Control-Allow-Credentials"
        ));
        source.registerCorsConfiguration("/ * *", config);
        return newCorsFilter(source); }}Copy the code

WebSecurityConfig

WebSecurityConfig configuration class inherits the Spring Security WebSecurityConfigurerAdapter classes. WebSecurityConfigurerAdapter class provides a default security configuration, and allows other classes by covering the ways to extend it and custom security configuration.

The following information is configured:

  • Ignore some resource paths that are accessible without validation;

  • Set CustomAuthenticationProvider custom authentication component, is used to validate the user login information (username and password);

  • In The Spring Security mechanism, configure resource paths that require authentication to access, resource paths that do not require authentication to access, and specify that certain resources can only be accessed by specific roles.

  • Configure the processing class when the request permission authentication is abnormal.

  • Add the custom JwtAuthenticationFilter and JwtAuthorizationFilter to the Spring Security mechanism.

/** * Web security configuration **@author star
 **/
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Import(SecurityProblemSupport.class)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CorsFilter corsFilter;

    @Autowired
    private UserService userService;

    /** * Encrypt the login password using the encryption method recommended by Spring Security */
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder(a){
        return new BCryptPasswordEncoder();
    }


     /** * The resource path configured in this method does not enter Spring Security for verification */
    @Override
    public void configure(WebSecurity web) {
        web.ignoring()
                .antMatchers(HttpMethod.OPTIONS, "/ * *")
                .antMatchers("/app/**/*.{js,html}")
                .antMatchers("/v2/api-docs/**")
                .antMatchers("/webjars/springfox-swagger-ui/**")
                .antMatchers("/swagger-resources/**")
                .antMatchers("/i18n/**")
                .antMatchers("/content/**")
                .antMatchers("/swagger-ui.html")
                .antMatchers("/test/**");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder authenticationManagerBuilder) {
        // Set up a custom authentication component that validates user login information (username and password) from the database
        CustomAuthenticationProvider authenticationProvider = new CustomAuthenticationProvider(bCryptPasswordEncoder());
        authenticationManagerBuilder.authenticationProvider(authenticationProvider);
    }

    /** * Define security policies and set HTTP access rules */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(corsFilter, UsernamePasswordAuthenticationFilter.class)
                .exceptionHandling()
                // Send a 401 response when the user has no access to the resource
                .authenticationEntryPoint(new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED))
                // Send a 403 response when the user does not have permission to access the resource
                .accessDeniedHandler(new AccessDeniedHandlerImpl())
             .and()
                / / disable CSRF
                .csrf().disable()
                .headers().frameOptions().disable()
             .and()
                .authorizeRequests()
                 // Resources in the specified path must be authenticated before they can be accessed
                .antMatchers("/").permitAll()
                .antMatchers(HttpMethod.POST, SecurityConstants.AUTH_LOGIN_URL).permitAll()
                .antMatchers("/api/users/register").permitAll()
                // Only administrators are allowed access
                .antMatchers("/api/users/detail").hasRole("ADMIN")
                // Other requests need validation
                .anyRequest().authenticated()
             .and()
                // Add a user login authentication filter to pass login requests to the filter for processing
                .addFilter(new JwtAuthenticationFilter(authenticationManager()))
                // No session required (no session created)
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
             .and()
               .apply(securityConfigurationAdapter());
        super.configure(http);
    }

    private JwtConfigurer securityConfigurationAdapter(a) throws Exception{
        return new JwtConfigurer(newJwtAuthorizationFilter(authenticationManager())); }}Copy the code

CustomAuthenticationProvider

CustomAuthenticationProvider custom user authentication component class, it is used to validate the user login information is correct. You need to configure it into the Spring Sercurity mechanism to use it.

/ * * * * * CustomAuthenticationProvider custom user authentication component < p > * to provide the user login password authentication function. Obtain the user information from the database based on the user name and verify the password. If the authentication succeeds, the user is granted the corresponding permission. * *@author star
 */
public class CustomAuthenticationProvider implements AuthenticationProvider {

    private final UserService userService;

    private BCryptPasswordEncoder bCryptPasswordEncoder;

    public CustomAuthenticationProvider(BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
        this.userService = SpringSecurityContextHelper.getBean(UserService.class);
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws BadCredentialsException, UsernameNotFoundException {
        // Get the username and password in the authentication information (that is, the username and password in the login request)
        String userName = authentication.getName();
        String password = authentication.getCredentials().toString();
        // Get user information based on the login name
        User user = userService.getUserByName(userName);
        Verify that the login password is correct. If yes, the user is granted corresponding permissions and user authentication information is generated
        if(user ! =null && this.bCryptPasswordEncoder.matches(password, user.getPassword())) {
            List<String> roles = userService.listUserRoles(userName);
            // If the user role is empty, the ROLE_USER permission is granted by default
            if (CollectionUtils.isEmpty(roles)) {
                roles = Collections.singletonList(UserRoleConstants.ROLE_USER);
            }
            // Set permissions
            List<GrantedAuthority> authorities = roles.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            // Generate authentication information
            return new UsernamePasswordAuthenticationToken(userName, password, authorities);
        }
        // Throw an exception if validation fails
        throw new BadCredentialsException("The userName or password error.");

    }

    @Override
    public boolean supports(Class
        aClass) {
        returnaClass.equals(UsernamePasswordAuthenticationToken.class); }}Copy the code

JwtAuthenticationFilter

JwtAuthenticationFilter user login authentication filter, mainly cooperate with CustomAuthenticationProvider to authenticate the user login request, check the login name and password. If the authentication succeeds, the token is generated and returned.

/** * JwtAuthenticationFilter ** <p> *@linkSecurityConstants#AUTH_LOGIN_URL} user request for login. * Verify by checking the username and password parameters in the request and calling Spring's authentication manager. * If the username and password are correct, the filter creates a token and returns it in the Authorization header. * Authorization: "Bearer + specific token values "</p> * *@author star
 **/
public class JwtAuthenticationFilter extends UsernamePasswordAuthenticationFilter {

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

    private final AuthenticationManager authenticationManager;

    private final ThreadLocal<Boolean> rememberMeLocal = new ThreadLocal<>();

    public JwtAuthenticationFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        // Specify the login URL to authenticate
        super.setFilterProcessesUrl(SecurityConstants.AUTH_LOGIN_URL);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            // Get user login information, JSON deserialize to UserDTO object
            UserLoginDTO loginUser = new ObjectMapper().readValue(request.getInputStream(), UserLoginDTO.class);
            rememberMeLocal.set(loginUser.getRememberMe());
            // Generate authentication information based on user name and password
            Authentication authentication = new UsernamePasswordAuthenticationToken(loginUser.getUserName(), loginUser.getPassword(), new ArrayList<>());
            / / return after the Authentication through our custom here {@ see CustomAuthenticationProvider} for validation
            return this.authenticationManager.authenticate(authentication);
        } catch (IOException e) {
            e.printStackTrace();
            return null; }}/** * if the validation is successful, the token is generated and */ is returned
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authentication) {
        try {
            // Get user information
            String username = null;
            // Get the identity information
            Object principal = authentication.getPrincipal();
            if (principal instanceof UserDetails) {
                UserDetails user = (UserDetails) principal;
                username = user.getUsername();
            } else if (principal instanceof String) {
                username = (String) principal;
            }
            // Obtain user authentication permission
            Collection<? extends GrantedAuthority> authorities = authentication.getAuthorities();
            // Obtain user role permissions
            List<String> roles = authorities.stream()
                    .map(GrantedAuthority::getAuthority)
                    .collect(Collectors.toList());
            boolean isRemember = this.rememberMeLocal.get();
            / / token is generated
            String token = JwtUtils.generateToken(username, roles, isRemember);
            // Returns the token added to the Response Header
            response.addHeader(SecurityConstants.TOKEN_HEADER, token);
        } finally {
            // Clear variables
            this.rememberMeLocal.remove(); }}/** * If the authentication fails, an error message */ is returned
    @Override
    protected void unsuccessfulAuthentication(HttpServletRequest request, HttpServletResponse response, AuthenticationException authenticationException) throws IOException {
        logger.warn(authenticationException.getMessage());

        if (authenticationException instanceof UsernameNotFoundException) {
            response.sendError(HttpServletResponse.SC_NOT_FOUND, authenticationException.getMessage());
            return; } response.sendError(HttpServletResponse.SC_UNAUTHORIZED, authenticationException.getMessage()); }}Copy the code

This filter inherited UsernamePasswordAuthenticationFilter class, and rewrite the three methods:

  • AttemptAuthentication: This method is used to validate user login information.

  • SuccessfulAuthentication: this method is called after the user authentication is successful;

  • UnsuccessfulAuthentication: this method is called after the user authentication failed.

At the same time, through the super setFilterProcessesUrl (SecurityConstants. AUTH_LOGIN_URL) method to specify the need to verify the login request.

AttemptAuthentication method, which retrievesthe user name and password from the attemptAuthentication request when a login request enters this filter, And use the authenticationManager. Authenticate (authenticate) authentication of user information, When performing this method will enter CustomAuthenticationProvider components and call the authenticate (Authentication Authentication) method for validation. If successful, an Authentication object is returned that contains complete information about the user, such as role permissions, and the successfulAuthentication method is invoked. If validation fails, you will go to call unsuccessfulAuthentication method.

At this point, the whole validation process is over.

JwtAuthorizationFilter

JwtAuthorizationFilter User request authorization filter is used to obtain token information from user requests, authenticate it, and load the user authentication information associated with the token and add it to the Spring Security context.

/** * JwtAuthorizationFilter user request authorization filter ** <p> * Provides request authorization function. Used to process all HTTP requests and check for the presence of an Authorization header with the correct token. * If the token is valid, the filter adds the authentication data to the Spring Security context and authorizes the request to access the resource. </p> * *@author star
 */
public class JwtAuthorizationFilter extends BasicAuthenticationFilter {

    private UserService userService;

    public JwtAuthorizationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
        this.userService = SpringSecurityContextHelper.getBean(UserService.class);
    }

    @Override
    protected void doFilterInternal(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response, @NotNull FilterChain filterChain) throws ServletException, IOException {
        // Get the token from the HTTP request
        String token = this.getTokenFromHttpRequest(request);
        // Verify the token is valid
        if (StringUtils.isNotEmpty(token) && JwtUtils.validateToken(token)) {
            // Obtain the authentication information
            Authentication authentication = this.getAuthentication(token);
            // Store the authentication information in the Spring security context
            SecurityContextHolder.getContext().setAuthentication(authentication);
        }
        // Release request
        filterChain.doFilter(request, response);

    }

    /** * Retrieve token ** from HTTP request@paramRequest Indicates the HTTP request *@returnReturns the token * /
    private String getTokenFromHttpRequest(HttpServletRequest request) {
        String authorization = request.getHeader(SecurityConstants.TOKEN_HEADER);
        if (authorization == null| |! authorization.startsWith(SecurityConstants.TOKEN_PREFIX)) {return null;
        }
        // Get the token from the request header
        return authorization.replace(SecurityConstants.TOKEN_PREFIX, "");
    }

    private Authentication getAuthentication(String token) {
        // Get the user name from the token information
        String userName = JwtUtils.getUserName(token);
        if (StringUtils.isNotEmpty(userName)) {
            // Obtain user permissions from the database to ensure timely access
            List<String> roles = userService.listUserRoles(userName);
            // If the user role is empty, the ROLE_USER permission is granted by default
            if (CollectionUtils.isEmpty(roles)) {
                roles = Collections.singletonList(UserRoleConstants.ROLE_USER);
            }
            // Set permissions
            List<GrantedAuthority> authorities = roles.stream()
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
            // Authentication information
            return new UsernamePasswordAuthenticationToken(userName, null, authorities);
        }
        return null; }}Copy the code

All user requests pass through this filter, and when they enter the filter they go through the following steps:

  • First, the token information is retrieved from the request and the validity of the token is checked.

  • If the token is valid, the token is parsed to get the user name, which is then used to get user role information from the database, and authentication is set up in the context of Spring Security.

  • If the token is invalid or the request does not contain token information, the request is allowed.

In particular, the user’s role information is retrieved from the database. Instead, the user role can be resolved from the token information to avoid direct access to the database.

However, it is also helpful to get user information directly from the database. For example, if the user role has changed, access using this token may be prohibited.

JwtUtils

The JwtUtils utility class is used to generate tokens and verify the tokens sent in user requests after a user logs in successfully.

/** * Jwt utility class for generating, parsing, and validating tokens **@author star
 **/
public final class JwtUtils {

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

    private static final byte[] secretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);

    private JwtUtils(a) {
        throw new IllegalStateException("Cannot create instance of static util class");
    }

    /** * Generate token ** based on user name@paramUserName userName *@paramRoles User roles *@paramDo you remember me@returnReturns the generated token */
    public static String generateToken(String userName, List<String> roles, boolean isRemember) {
        byte[] jwtSecretKey = DatatypeConverter.parseBase64Binary(SecurityConstants.JWT_SECRET_KEY);
        // Expiration time
        long expiration = isRemember ? SecurityConstants.EXPIRATION_REMEMBER_TIME : SecurityConstants.EXPIRATION_TIME;
        / / token is generated
        String token = Jwts.builder()
                // Generate visa information
                .setHeaderParam("typ", SecurityConstants.TOKEN_TYPE)
                .signWith(Keys.hmacShaKeyFor(jwtSecretKey), SignatureAlgorithm.HS256)
                .setSubject(userName)
                .claim(SecurityConstants.TOKEN_ROLE_CLAIM, roles)
                .setIssuer(SecurityConstants.TOKEN_ISSUER)
                .setIssuedAt(new Date())
                .setAudience(SecurityConstants.TOKEN_AUDIENCE)
                // Set the valid time
                .setExpiration(new Date(System.currentTimeMillis() + expiration * 1000))
                .compact();
        // JWT is usually preceded by Bearer, with the Authorization being added to the header of the request and subsequently labeled as Bearer
        return SecurityConstants.TOKEN_PREFIX + token;
    }

    /** * validates the token and returns the result ** 

* if parsing fails, the token is invalid */

public static boolean validateToken(String token) { if (StringUtils.isEmpty(token)) { throw new RuntimeException("Miss token"); } try { Jwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token); return true; } catch (ExpiredJwtException e) { logger.warn("Request to parse expired JWT : {} failed : {}", token, e.getMessage()); } catch (UnsupportedJwtException e) { logger.warn("Request to parse unsupported JWT : {} failed : {}", token, e.getMessage()); } catch (MalformedJwtException e) { logger.warn("Request to parse invalid JWT : {} failed : {}", token, e.getMessage()); } catch (IllegalArgumentException e) { logger.warn("Request to parse empty or null JWT : {} failed : {}", token, e.getMessage()); } return false; } public static String getUserName(String token) { returnJwts.parser() .setSigningKey(secretKey) .parseClaimsJws(token) .getBody() .getSubject(); }}Copy the code

Description of the request authentication process

There are two filters in this project, namely JwtAuthenticationFilter and JwtAuthorizationFilter. When a user initiates a request, the JwtAuthorizationFilter is entered first. If the request is a login request, the JwtAuthorizationFilter is entered again. That is, only the specified login request will enter the JwtAuthorizationFilter. Once you pass the filter, you enter the Spring Security mechanism.

Test the API

Registered account

The login

Bring the correct token to access resources that require authentication


Bring an incorrect token to access a resource that requires authentication

Access resources that require authentication without a token

Project adjustment record

  • Add Swagger UI for easy view of project interface.
  • Added global exception catching.
  • Add JPA audit function and improve data sheet audit information.
  • Expose the user login interface (/ API /auth/login) in the Controller layer.
  • Improve the detailed content of the project.

Reference documentation

  • www.callicoder.com/spring-boot-spring-security-jwt-mysql-react-app-part-2/

  • Segmentfault.com/a/119000000…

  • www.springcloud.cc/spring-secu…