Spring Security + JWT, realize the right control of separating and adapting the restful API before and after

Keywords: Java Spring JSON github

Preface

Spring security adopts the form based authentication form by default, which is related to the forms such as login authorization and authentication from the user with session identification. At present, many applications are developed based on the restful API style of SpringBoot, which can not be directly satisfied by using the default spring security. Solution: instead of using the default login authentication method, the system implements login and authentication by overwriting and adding Filter, and uses JWT (JSON WEB TOKEN) as the authentication mechanism.

Principle:

  1. First time using username+password to request login interface
  2. After verification, generate to the server and return a customized jwt to the client
  3. Every subsequent HTTP request of clinet carries header=Authorization, value=jwt
  4. When the server receives the request, it will obtain the authentication information in jwt and generate a UsernamePasswordAuthenticationToken in the spring security authentication system (this Token is different from the jwt returned to the client)

Implementation steps:

  • Jwtloginfilter implementation usernamepasswordauthenticationfilter
  • JWTAuthenticationFilter extends BasicAuthenticationFilter (token validation filter)
  • JWTUtils (tool class to operate jwt)

Source address: https://github.com/YoungerJam/SpringSecurityWithJwtDemo

A key

// JWTLoginFilter.java

package com.SpringSecurityWithJwt.demo.filter;

import com.SpringSecurityWithJwt.demo.entity.User;
import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

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

/**
 * Login filter client sends POST request to this authentication
 * After the verification is successful, a token will be generated and returned to the client
 *
 * @Author: Jam
 * @Date: 2020/3/12 17:24
 */
public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {


    private AuthenticationManager authenticationManager;

    /**
     * Set the path of the login request to / auth (default / login)
     * @param authenticationManager Authentication management
     */
    public JWTLoginFilter(AuthenticationManager authenticationManager) {
        this.authenticationManager = authenticationManager;
        super.setFilterProcessesUrl("/auth");
    }

    /**
     * Method of key rewriting, method name direct translation attempt authentication
     * @param request Request body with user name + password
     * @param response
     * @return Authentication information
     * @throws AuthenticationException
     */
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        try {
            User user = new User();
            user.setUsername(request.getParameter("username"));
            user.setPassword(request.getParameter("password"));
            return authenticationManager.authenticate(new UsernamePasswordAuthenticationToken(
                    user.getUsername(),
                    user.getPassword(),
                    new ArrayList<>()
            ));
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * Overwrite the method of successful authentication. If the account password verification is successful, this method will be called
     * And generate the corresponding token to return to the client
     * @param request 
     * @param response The response body returned to the client (a token authentication token will be added to the header, which is required for subsequent access)
     * @param chain
     * @param auth
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain,Authentication auth)throws  IOException, ServletException{
        Collection<? extends GrantedAuthority> authorities=auth.getAuthorities();
        String username=((org.springframework.security.core.userdetails.User) auth.getPrincipal()).getUsername();
        String role="";
        for(GrantedAuthority authority:authorities){
            role=authority.getAuthority();
        }
        System.out.println(username+" "+role);
        String token=JwtUtils.createToken(username,role);
        response.addHeader(JwtUtils.TOKEN_HEADER, JwtUtils.TOKEN_PREFIX+token);
    }
}

// JWTAuthenticationFilter.java

package com.SpringSecurityWithJwt.demo.filter;

import com.SpringSecurityWithJwt.demo.utils.JwtUtils;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;

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

/**
 * token Check filter
 * http requests with token will be token verified in this class
 * jwt The mechanism of token test is provided
 *
 * @Author: Jam
 * @Date: 2020/3/12 18:00
 */

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    public JWTAuthenticationFilter(AuthenticationManager authenticationManager, AuthenticationEntryPoint authenticationEntryPoint) {
        super(authenticationManager, authenticationEntryPoint);
    }

    /**
     * Overwriting method
     * @param request Request body with token
     * @param response
     * @param chain
     * @throws IOException
     * @throws ServletException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                    HttpServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        String header=request.getHeader(JwtUtils.TOKEN_HEADER);
        //Filtering without token (possibly unauthorized access)
        if(header==null||!header.startsWith(JwtUtils.TOKEN_PREFIX)){
            chain.doFilter(request,response);
            return;
        }
        UsernamePasswordAuthenticationToken authentication = getAuthentication(header);
        //Set the authentication information of the user, and generate the UsernamePasswordAuthenticationToken from jwtToken
        SecurityContextHolder.getContext().setAuthentication(authentication);
        chain.doFilter(request, response);
    }

    private UsernamePasswordAuthenticationToken getAuthentication(String header) {
        String token = header.replace(JwtUtils.TOKEN_PREFIX, "");
        String username = JwtUtils.getUsername(token);
        String role = JwtUtils.getUserRole(token);
        System.out.println("To grant authorization:"+username+" "+role);
        if (username != null) {
            return new UsernamePasswordAuthenticationToken(username, null,
                    Collections.singleton(new SimpleGrantedAuthority(role))
            );
        }
        return null;
    }

}

// JwtUtils.java

package com.SpringSecurityWithJwt.demo.utils;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;

import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * @Author: Jam
 * @Date: 2020/3/13 0:36
 */
public class JwtUtils {
    public static final String TOKEN_HEADER="Authorization";
    public static final String TOKEN_PREFIX="Bearer ";
    private static final String SECRET="MyJwtSecret";
    private static final long EXPIRATION=7200;
    private static final String ROLE_CLAIMS="rol";

    public static String createToken(String username,String role){
        Map<String,Object> map=new HashMap<>();
        map.put(ROLE_CLAIMS,role);
        return Jwts.builder()
                .signWith(SignatureAlgorithm.HS512,SECRET)
                .setClaims(map)
                .setSubject(username)
                .setExpiration(new Date(System.currentTimeMillis()+EXPIRATION*1000))
                .compact();
    }
    public static String getUsername(String token){
        return getTokenBody(token).getSubject();
    }
    public static String getUserRole(String token){
        return (String) getTokenBody(token).get(ROLE_CLAIMS);
    }
   
    //Parsing jwt
    private static Claims getTokenBody(String token){
        return Jwts.parser()
                .setSigningKey(SECRET)
                .parseClaimsJws(token)
                .getBody();
    }
}

//JWTAuthenticationEntryPoint exception handling class

package com.SpringSecurityWithJwt.demo.exception;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;

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

/**
 * @Author: Jam
 * @Date: 2020/3/13 15:09
 */
public class JWTAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.setCharacterEncoding("UTF-8");
        response.setContentType("application/json; charset=utf-8");
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.getWriter().write(new ObjectMapper().writeValueAsString("No access "+authException.getMessage()));
    }
}

// SecurityConfig.java

package com.SpringSecurityWithJwt.demo.config;

import com.SpringSecurityWithJwt.demo.exception.JWTAuthenticationEntryPoint;
import com.SpringSecurityWithJwt.demo.filter.JWTAuthenticationFilter;
import com.SpringSecurityWithJwt.demo.filter.JWTLoginFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
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 javax.sql.DataSource;

/**
 * @Author: Jam
 * @Date: 2020/3/9 16:31
 */
@Configuration
@EnableGlobalMethodSecurity(prePostEnabled = true)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private final DataSource dataSource;

    private final BCryptPasswordEncoder bCryptPasswordEncoder;

    @Autowired
    public SecurityConfig(DataSource dataSource, BCryptPasswordEncoder bCryptPasswordEncoder) {
        this.dataSource = dataSource;
        this.bCryptPasswordEncoder = bCryptPasswordEncoder;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.cors().and().csrf().disable().authorizeRequests()
                .antMatchers(HttpMethod.GET, "/user/query").authenticated()
                .antMatchers(HttpMethod.POST, "/user/register").permitAll()
                .antMatchers(HttpMethod.POST, "/user/update").hasRole("USER")
                .anyRequest().authenticated()
                .and()
                .addFilter(new JWTLoginFilter(authenticationManager()))
                .addFilter(new JWTAuthenticationFilter(authenticationManager()))
                .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                .and()
                .exceptionHandling().authenticationEntryPoint(new JWTAuthenticationEntryPoint());
    }

    /**
    * Here I use jdbc database table authentication
    **/
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth
                .jdbcAuthentication()
                .dataSource(dataSource)
                .usersByUsernameQuery(
                        "select username,password,true from t_user where username=?"
                )
                .authoritiesByUsernameQuery(
                        "select username,'ROLE_USER' from t_user where username=?"

                )
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}

test

  1. Open interface / user/register

    The open interface can be called normally. Note that only the path under the post method is set for post request during configuration. Get / user / register belongs to configuration. anyRequest().authenticated()
    So if the path is requested by get in error, the unauthorized access information will be returned
  2. Login interface / auth

    Because no successful return body information is overwritten, the body in Reponse is empty, which can be added by yourself if necessary. Focus on the Authorization header in reponse Header, which is the jwt token returned by the server.
  3. GET /user/query and POST /user/update interface tests

    When the token is not carried, the response is as above. Add the authorization returned when you just log in to the Header


    After the test, the permission control functions normally. When you return to idea, you can see some permission related information in the process of processing (I wrote sout in two filters)

Analyse

In the process of implementation, through inheriting UsernamePasswordAuthenticationFilter, the attemptauthentication and successfulAuthentication methods are overridden to see the class in the source code
(capture key parts)

The principle of accepting post requests only under the / login path is that post requests cannot be changed, and the path can be set as super.setFilterProcessesUrl("/auth") in the above code at will;


Here we can see that in the source code implementation level, the internal use is UsernamePasswordAuthenticationToken, which is also why we need to parse jwt to generate apat when carrying jwt to request again, so we don't need to transfer to the attempt method, but we can still have the authorization identity.

//Key statements in the source code are intercepted

	private AuthenticationSuccessHandler successHandler = new SavedRequestAwareAuthenticationSuccessHandler();
	private AuthenticationFailureHandler failureHandler = new SimpleUrlAuthenticationFailureHandler();
protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
SecurityContextHolder.getContext().setAuthentication(authResult);
successHandler.onAuthenticationSuccess(request, response, authResult);
	}

Verify successful processing method, in abstractauthenticate In Filter, if you are interested, you can call it out by yourself. The default is to call the Handler class to process the follow-up. If you choose not to overwrite this method, you can write a user-defined login success / failure process to implement the Hadler class.

Let's take a look at bsiacauthenticationfitler, the parent class of JWTAuthentication
After clicking -- there's a lot of notes, take a look at the key points
We just need to focus on two key points
1. Who intercepts and processes the request with token (as shown in the figure above), so we can extend it
2. How to handle this token and complete the identification of user rights.


SecurityContextHolder.getContext().setAuthentication(authResult);
This statement is the key to setting authentication information. Let's take a look at the data structure of the frequently mentioned UsernamePasswordAuthenticationToken
This class has principal field
Output the UPAT generated after verifying the account password

org.springframework.security.core.userdetails.User@a0ccb02b: 
Username: 771007760;
Password: [PROTECTED]; 
Enabled: true; 
AccountNonExpired: true; 
credentialsNonExpired: true;
AccountNonLocked: true; 
Granted Authorities: ROLE_USER

It contains almost all the information of users. A constructor of this class (the constructor called above in JWTAuthenticationFilter)


The JwtToken we carry is parsed and generated back to UPAT. Just write username, null and role to complete the authentication.
Output the UPAT generated by token parsing

   System.out.println(u.getPrincipal()+" "+u.getAuthorities());

771007760 [ROLE_USER]
There is no other information, because at this time, the user name + role is qualified for authentication

There is no way to mark all references after referring to multiple documents and implementation schemes, so thank you all the big guys here.

Source address: https://github.com/YoungerJam/SpringSecurityWithJwtDemo

Published 6 original articles, won praise 0, visited 1338
Private letter follow

Posted by brandonr on Fri, 13 Mar 2020 04:00:05 -0700