To prepare

Project GitHub: github.com/Smith-Cruis…

I’ve written two articles about security frameworks before, so you can take a look at them to lay the groundwork.

Shiro+JWT+Spring Boot Restful Tutorial

Spring Boot+Spring Security+Thymeleaf

You need to understand at least the basic configuration of Spring Security and the JWT mechanism before you start.

Some of the Maven configuration and Controller writing here do not say, you can see the source code.

In this project, the JWT key is the user’s own login password, so that each token’s key is different and relatively secure.

Transformation ideas

We usually use Spring Security will use UsernamePasswordAuthenticationFilter and UsernamePasswordAuthenticationToken these two classes, However, these two classes are intended to solve the problem of form login and are not very friendly to Token authentication like JWT. So we’ll develop our own Filter and AuthenticationToken to replace Spring Security’s built-in classes.

The default Spring Security authentication user uses the ProviderManager class. Meanwhile ProviderManager invokes AuthenticationUserDetailsService this interface in populated UserDetails loadUserDetails throws token (T) UsernameNotFoundException to retrieve the user information from the database, this method requires the user to inherit implementation). Because of considering the own way is not very good support JWT, such as UsernamePasswordAuthenticationToken assignment by have a username and password in the field, but JWT is attached in the header of the request, There is no such thing as username and password when there is only one token.

So I have to, for example, the user’s method is not realized in AuthenticationUserDetailsService, but this may not be able to perfect follow Spring Security official design, please correct me if there is a better way.

transform

transformAuthentication

Authentication is an interface that is officially provided by the Security and is the core of the call Authentication stored in SecurityContextHolder.

Here are three methods

GetCredentials () was originally used to get the password, but now we’re going to use it to store the tokens passed by the front end

GetPrincipal () was originally used to hold user information, but we’ll keep it. For example, some key information such as user username, ID and so on is stored in the Controller

GetDetails () returns some miscellaneous details, such as the client IP, but since this is basically a restful request, this is not important, so it is emasculated :happy:

The Authentication interface is provided by default

public interface Authentication extends Principal.Serializable {

	Collection<? extends GrantedAuthority> getAuthorities();

	Object getCredentials(a);

	Object getDetails(a);

	Object getPrincipal(a);

	boolean isAuthenticated(a);

	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}
Copy the code

JWTAuthenticationToken

Let’s write our own Authentication, noting the difference between the two constructors. A class of AbstractAuthenticationToken is official Authentication.

public class JWTAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    private final Object principal;
    private final Object credentials;

    /** * setAuthenticated(false) *@paramToken JWT key */
    public JWTAuthenticationToken(String token) {
        super(null);
        this.principal = null;
        this.credentials = token;
        setAuthenticated(false);
    }

    JWTAuthenticationToken = JWTAuthenticationToken = JWTAuthenticationToken = JWTAuthenticationToken * setAuthenticated(true) * because it has been identified successfully@paramToken JWT key *@paramUserInfo User information, such as username, ID, and so on *@param*/ the authority that has been granted to the authorities
    public JWTAuthenticationToken(String token, Object userInfo, Collection<? extends GrantedAuthority> authorities) {
        super(authorities);
        this.principal = userInfo;
        this.credentials = token;
        setAuthenticated(true);
    }

    @Override
    public Object getCredentials(a) {
        return credentials;
    }

    @Override
    public Object getPrincipal(a) {
        returnprincipal; }}Copy the code

Transform the AuthenticationManager

This parameter is used to determine whether the user token is valid

JWTAuthenticationManager

@Component
public class JWTAuthenticationManager implements AuthenticationManager {

    @Autowired
    private UserService userService;

    /** * Perform token authentication *@paramAuthentication JWTAuthenticationToken * to be authenticated@returnThe authenticated JWTAuthenticationToken is used by the Controller@throwsAuthenticationException throws */ if authentication fails
    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String token = authentication.getCredentials().toString();
        String username = JWTUtil.getUsername(token);

        UserEntity userEntity = userService.getUser(username);
        if (userEntity == null) {
            throw new UsernameNotFoundException("This user does not exist");
        }

        /* * The official recommendation is that three exceptions must be handled in this method, BadCredentialsException = BadCredentialsException = DisabledException = BadCredentialsException You can customize this to suit your business needs * see the AuthenticationManager JavaDoc */ for details
        boolean isAuthenticatedSuccess = JWTUtil.verify(token, username, userEntity.getPassword());
        if (! isAuthenticatedSuccess) {
            throw new BadCredentialsException("Incorrect user name or password");
        }

        JWTAuthenticationToken authenticatedAuth = new JWTAuthenticationToken(
                token, userEntity, AuthorityUtils.commaSeparatedStringToAuthorityList(userEntity.getRole())
        );
        returnauthenticatedAuth; }}Copy the code

Develop your own Filter

Then we will use our own filters, considering that token is added in the header, this like those BasicAuthentication certification, so we inherit BasicAuthenticationFilter rewrite the core method reform.

JWTAuthenticationFilter

public class JWTAuthenticationFilter extends BasicAuthenticationFilter {

    /** * Use our own JWTAuthenticationManager@paramAuthenticationManager Our own developed JWTAuthenticationManager */
    public JWTAuthenticationFilter(AuthenticationManager authenticationManager) {
        super(authenticationManager);
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException {
        String header = request.getHeader("Authorization");
        if (header == null| |! header.toLowerCase().startsWith("bearer ")) {
            chain.doFilter(request, response);
            return;
        }

        try {
            String token = header.split("") [1];
            JWTAuthenticationToken JWToken = new JWTAuthenticationToken(token);
            If authentication fails, the AuthenticationManager will throw an exception that we catch
            Authentication authResult = getAuthenticationManager().authenticate(JWToken);
            // Write the authenticated Authentication to SecurityContextHolder for later use
            SecurityContextHolder.getContext().setAuthentication(authResult);
        } catch (AuthenticationException failed) {
            SecurityContextHolder.clearContext();
            // Authentication failed
            response.sendError(HttpServletResponse.SC_UNAUTHORIZED, failed.getMessage());
            return; } chain.doFilter(request, response); }}Copy the code

configuration

SecurityConfig

// Enable method annotation
@EnableGlobalMethodSecurity(securedEnabled = true, prePostEnabled = true)
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private JWTAuthenticationManager jwtAuthenticationManager;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // restful This function is disabled because it is innate to defend against CSRF attacks
        http.csrf().disable()
                // By default, all requests are allowed through, and then we use method annotations to granular control permissions
                .authorizeRequests().anyRequest().permitAll()
                .and()
                / / to add our own filters, note because we don't have open formLogin (), so UsernamePasswordAuthenticationFilter not be invoked
                .addFilterAt(new JWTAuthenticationFilter(jwtAuthenticationManager), UsernamePasswordAuthenticationFilter.class)
                // The front-end separation is inherently stateless, so we don't need cookies and sessions. All information is stored in a token..sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); }}Copy the code

There are a lot of tricks for method annotation authentication. Check out this simple tutorial on Spring Boot+Spring Security+Thymeleaf

Uniform Global Exception

A restful final exception throw must be in a uniform format, so as to facilitate the front end of the call.

We normally use RestControllerAdvice to unify exceptions, but it can only manage our own exceptions, not the framework’s own exceptions, such as 404, so we need to modify the ErrorController

ExceptionController

@RestControllerAdvice
public class ExceptionController {

    // Catch all exceptions thrown by the controller itself
    @ExceptionHandler(Exception.class)
    public ResponseEntity<ResponseBean> globalException(Exception ex) {
        return new ResponseEntity<>(
                new ResponseBean(
                        HttpStatus.INTERNAL_SERVER_ERROR.value(), ex.getMessage(), null), HttpStatus.INTERNAL_SERVER_ERROR ); }}Copy the code

CustomErrorController

If you want to implement the ErrorController interface directly, there are many existing methods that do not work well, so we choose AbstractErrorController

@RestController
public class CustomErrorController extends AbstractErrorController {

    // Exception path url
    private final String PATH = "/error";

    public CustomErrorController(ErrorAttributes errorAttributes) {
        super(errorAttributes);
    }

    @RequestMapping("/error")
    public ResponseEntity<ResponseBean> error(HttpServletRequest request) {
        // Obtain the exception information in the request, there are many, such as time, path, you can traverse the map to see
        Map<String, Object> attributes = getErrorAttributes(request, true);
        // Select only the return message field
        return new ResponseEntity<>(
                new ResponseBean(
                       getStatus(request).value() , (String) attributes.get("message"), null), getStatus(request)
        );
    }

    @Override
    public String getErrorPath(a) {
        returnPATH; }}Copy the code

test

Write a controller, and you can also use my controller to get user information. I recommend using @authenticationPrincipal!

@RestController
public class MainController {

    @Autowired
    private UserService userService;

    // Log in to obtain the token
    @PostMapping("login")
    public ResponseEntity<ResponseBean> login(@RequestParam String username, @RequestParam String password) {
        UserEntity userEntity = userService.getUser(username);
        if (userEntity==null| |! userEntity.getPassword().equals(password)) {return new ResponseEntity<>(new ResponseBean(HttpStatus.BAD_REQUEST.value(), "login fail".null), HttpStatus.BAD_REQUEST);
        }

        / / JWT's signature
        String token = JWTUtil.sign(username, password);
        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "login success", token), HttpStatus.OK);
    }

    // Anyone can access the method to determine whether the user is valid
    @GetMapping("everyone")
    public ResponseEntity<ResponseBean> everyone(a) {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        if (authentication.isAuthenticated()) {
            // Login user
            return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are already login", authentication.getPrincipal()), HttpStatus.OK);
        } else {
            return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are anonymous".null), HttpStatus.OK); }}@GetMapping("user")
    @PreAuthorize("hasAuthority('ROLE_USER')")
    public ResponseEntity<ResponseBean> user(@AuthenticationPrincipal UserEntity userEntity) {
        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are user", userEntity), HttpStatus.OK);
    }

    @GetMapping("admin")
    @PreAuthorize("hasAuthority('ROLE_ADMIN')")
    public ResponseEntity<ResponseBean> admin(@AuthenticationPrincipal UserEntity userEntity) {
        return new ResponseEntity<>(new ResponseBean(HttpStatus.OK.value(), "You are admin", userEntity), HttpStatus.OK); }}Copy the code

other

Here are some quick answers to common questions.

Verifying whether the Token is valid is too resource-intensive per request to the database

It is impossible for us to get data from the database every time to judge whether the token is legitimate, which is a waste of resources and affects efficiency.

We can use the cache in JWTAuthenticationManager.

When the user accesses the database for the first time, we query the database to determine whether the token is valid. If the token is valid, we put it into the cache (the expiration time of the cache is the same as the expiration time of the token). After that, each request first searches in the cache.

How to solve the JWT expiration problem

Write a method in JWTAuthenticationManager that raises a specific exception, such as ReAuthenticateException, when the token is about to expire, Then we catch the exception separately in JWTAuthenticationFilter, return a specific HTTP status code, and then the front end separately access GET /re_authentication to GET a new token to replace the original one. The old token is also removed from the cache.