Implementation of Login Authentication Based on JWT by Constructing SpringSecurity

  • 2021-08-17 00:03:10
  • OfStack

Directory target function point preparation
Introducing Maven dependency
Configuring the DAO data layer
Create an JWT tool class
Login
LoginFilter
LoginSuccessHandler
LoginFailureHandler
Validation
JwtAuthenticationFilter
AuthenticationEntryPoint
Centralized configuration

Recently, the login verification part of the project adopts JWT verification. And since Spring Boot framework is adopted, Spring Security is naturally used in this part of authentication and privilege management. Here, record the specific implementation under 1.
Before adopting JWT scheme in the project, it is necessary to understand its characteristics and applicable scenarios. After all, there is no silver bullet in software engineering. There is only a suitable scene, and there is no plan for ten thousand essential oils.

In short, JWT can carry non-sensitive information and can not be tampered with. Three questions of network authentication can be completed by verifying whether it has been tampered with and reading the information content: "Who are you", "What permissions do you have" and "Are you impersonating?".

For the sake of security, using it requires Https protocol, and 1 care must be taken to prevent the key used for encryption from being leaked.

Under the authentication mode of JWT, the server does not store the user status information, which cannot be discarded within the validity period. After the validity period expires, a new one needs to be recreated to replace it.
Therefore, it is not suitable for long-term state maintenance, scenes that require users to kick offline, and scenes that require frequent modification of user information. Because to solve these problems, it is always necessary to query the database or cache, or encrypt and decrypt repeatedly. It is not sweet to twist the melon, so it is better to use Session directly. However, as a short-term switching between services, it is very suitable, such as OAuth.

Target function point

Log in by filling in your username and password.

After successful verification, the server generates JWT authentication token and returns it to the client. Returns an error message after validation fails. The client carries JWT in each request to access the interface within the permission.

Each request verifies token validity and permissions and throws a 401 unauthorized error when there is no valid token.
When it is found that the expiration date of token with the request is approaching, return a specific status code and re-request a new token.

Preparatory work

Introducing Maven dependency

For the implementation of this login authentication, three packages, Spring Security, jackson and java-jwt, need to be introduced.


<dependency>
 <groupId>org.springframework.boot</groupId>
 <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
 <groupId>com.fasterxml.jackson.core</groupId>
 <artifactId>jackson-core</artifactId>
 <version>2.12.1</version>
</dependency>
<dependency>
 <groupId>com.auth0</groupId>
 <artifactId>java-jwt</artifactId>
 <version>3.12.1</version>
</dependency>

Configuring the DAO data layer

Before authenticating a user, it is natural to create a user entity object and get the user's service class. The difference is that the two classes need to implement the Spring Security interface to integrate them into the validation framework.

User

The user entity class needs to implement "UserDetails" interface, which requires three methods: getUsername, getPassword and getAuthorities to obtain user name, password and authority. And isAccountNonExpired, isAccountNonLocked, isCredentialsNonExpired and isEnabled, which are four methods to judge whether they are valid users, all return true first because they have nothing to do with authentication. The diagram here is convenient, and lombok is used.


@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

UserService

The user service class needs to implement the "UserDetailsService" interface, which is very simple and only needs to implement one method, loadUserByUsername (String username). Here, MyBatis is used to connect to the database to obtain user information.


@Service
public class UserService implements UserDetailsService {
 
 @Autowired
 UserMapper userMapper;

 @Override
 @Transactional
 public User loadUserByUsername(String username) {
  return userMapper.getByUsername(username);
 }

 ...
}

Create an JWT tool class

This tool class is mainly responsible for generating, verifying and taking values from token.


@Component
public class JwtTokenProvider {

 private static final long JWT_EXPIRATION = 5 * 60 * 1000L; // 5 Minutes expired 

 public static final String TOKEN_PREFIX = "Bearer "; // token  The beginning string of 

 private String jwtSecret = "XXX  Key, you can't tell anyone if you are killed ";

 ...
}

Generating an JWT: The user information is obtained from the authenticated object, and then the token is generated with the specified encryption method and expiration time. Here, only the user name is simply added to token:


public String generateToken(Authentication authentication) {
 User userPrincipal = (User) authentication.getPrincipal(); //  Getting User Objects 
 Date expireDate = new Date(System.currentTimeMillis() + JWT_EXPIRATION); //  Set expiration time 
 try {
  Algorithm algorithm = Algorithm.HMAC256(jwtSecret); //  Specify the encryption method 
  return JWT.create().withExpiresAt(expireDate).withClaim("username", userPrincipal.getUsername()) 
    .sign(algorithm); //  Issue  JWT
 } catch (JWTCreationException jwtCreationException) {
  return null;
 }
}

Verify JWT: Specify and sign the same encryption method, and verify whether this token is signed by this server, tampered with, or expired.


public boolean validateToken(String authToken) {
 try {
  Algorithm algorithm = Algorithm.HMAC256(jwtSecret); //  And issuance and maintenance 1 To 
  JWTVerifier verifier = JWT.require(algorithm).build();
  verifier.verify(authToken);
  return true;
 } catch (JWTVerificationException jwtVerificationException) {
  return false;
 }
}

Obtain Load Information: Parse the user name information from the load section of token, which is encoded by md5 and belongs to public information.


public String getUsernameFromJWT(String authToken) {
 try {
  DecodedJWT jwt = JWT.decode(authToken);
  return jwt.getClaim("username").asString();
 } catch (JWTDecodeException jwtDecodeException) {
  return null;
 }
}

Login

The login part needs to create three files: the interceptor responsible for login interface processing, and the processing class for login success or failure.

LoginFilter

Spring Security comes with its own form login by default. The filter responsible for handling this login verification process is called "UsernamePasswordAuthenticationFilter", but it only supports form value transmission. Here, it is inherited by a custom class to enable it to support JSON value transmission and be responsible for login verification interface.
This interceptor is only responsible for taking the value from the request, and the verification work Spring Security will help us handle it.


public class LoginFilter extends UsernamePasswordAuthenticationFilter {

 @Override
 public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
  if (!request.getMethod().equals("POST")) {
   throw new AuthenticationServiceException(" Login interface method is not supported : " + request.getMethod());
  }
  if (request.getContentType().equals(MediaType.APPLICATION_JSON_VALUE)) {
   Map<String, String> loginData = new HashMap<>();
   try {
    loginData = new ObjectMapper().readValue(request.getInputStream(), Map.class);
   } catch (IOException e) {
   }
   String username = loginData.get(getUsernameParameter());
   String password = loginData.get(getPasswordParameter());
   if (username == null) {
    username = "";
   }
   if (password == null) {
    password = "";
   }
   username = username.trim();
   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username,
     password);
   setDetails(request, authRequest);
   return this.getAuthenticationManager().authenticate(authRequest);
  } else {
   return super.attemptAuthentication(request, response);
  }
 }

}

LoginSuccessHandler

Be responsible for generating JWT to the front end after successful login.


@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler {

 @Autowired
 private JwtTokenProvider jwtTokenProvider;

 @Override
 public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
   Authentication authentication) throws IOException, ServletException {

  ResponseData responseData = new ResponseData();
  String token = jwtTokenProvider.generateToken(authentication);
  responseData.setData(JwtTokenProvider.TOKEN_PREFIX + token);
  response.setContentType("application/json;charset=utf-8");
  ObjectMapper mapper = new ObjectMapper();
  mapper.writeValue(response.getWriter(), responseData);
 }

}

LoginFailureHandler

After validation fails, an error message is returned.


@Component
public class LoginFailureHandler implements AuthenticationFailureHandler {

 @Override
 public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
   AuthenticationException exception) throws IOException, ServletException {
  response.setContentType("application/json;charset=utf-8");
  ResponseData respBean = setResponseData(exception);
  ObjectMapper mapper = new ObjectMapper();
  mapper.writeValue(response.getWriter(), respBean);
 }

 private ResponseData setResponseData(AuthenticationException exception) {
  if (exception instanceof LockedException) {
   return ResponseData.build(" User is locked ");
  } else if (exception instanceof CredentialsExpiredException) {
   return ResponseData.build(" Password has expired ");
  } else if (exception instanceof AccountExpiredException) {
   return ResponseData.build(" User name has expired ");
  } else if (exception instanceof DisabledException) {
   return ResponseData.build(" Account unavailable ");
  } else if (exception instanceof BadCredentialsException) {
   return ResponseData.build(" Validation failed ");
  }
  return ResponseData.build(" Login failed, please contact administrator ");
 }

}

Validation

After successful login, the front end carries the signed JWT every time it initiates the request, so that the server can recognize that this is the logged-in user.
Also, if the JWT is not carried, or the token is out of date, or illegal, a separate processing class returns an error message.

JwtAuthenticationFilter

Be responsible for parsing JWT in the request header in each request, obtaining user information from it, generating verification objects and passing them to the next filter.


@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

0

AuthenticationEntryPoint

This class is relatively simple, except that after the validation fails, it returns a 401 response and logs an error message.


@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

1

Centralized configuration

The function of Spring Security is realized through a series of filter chains, while configuring the whole Spring Security only needs to be configured in one class.
Now let's create this class, inherit from "WebSecurityConfigurerAdapter", and configure the various files prepared above, 11.
First, through annotations, set up the Spring Security function that turns on the global, and through dependency injection, introduce the class you just created.


@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

2

Then, the user gets the service class and encryption method, and configures it to Spring Security, so that it knows how to verify the login.


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

Finally, put the JWT filter into the filter chain, and replace the default "UsernamePasswordAuthenticationFilter" with a custom login filter to complete the function.


@Data
public class User implements UserDetails {

 private static final long serialVersionUID = 1L;

 private String username;

 private String password;

 private Collection<? extends GrantedAuthority> authorities;

 ...
}

4

Related articles: