There are a number of login strategies for projects with a separate front and back end, but JWT is one of the most popular solutions. In this article, I will share with you how to use Spring Security and JWT together to achieve a separate front and back end login solution.

1 Stateless login

1.1 What is stateful?

Stateful service, that is, the server needs to record the client information of each Session to identify the client identity and process requests according to the user identity. A typical design is Session in Tomcat. For example, login: After the user logs in, we save the user’s information in the server session, and give the user a cookie value, record the corresponding session, and then the next request, the user brings the cookie value (this step is automatically completed by the browser), we can identify the corresponding session, so as to find the user’s information. This method is the most convenient at present, but it also has some disadvantages as follows:

  • The server stores a large amount of data, increasing the pressure on the server
  • The server saves user status and does not support clustered deployment

1.2 What is stateless

Each service in a microservice cluster uses RESTful interfaces to provide external services. One of the most important specifications of RESTful style is the statelessness of services, i.e. :

  • The server does not save any client requester information
  • Each request from the client must contain self-description information, which identifies the client

So what are the benefits of this statelessness?

  • Client requests do not depend on server information, and multiple requests do not have to access the same server
  • The cluster and state of the server are transparent to the client
  • Servers can be migrated and scaled at will (clustering deployment is convenient)
  • Reduce the storage pressure on the server

1.3. How to achieve statelessness

The flow of stateless login:

  • First, the client sends the account name/password to the server for authentication
  • After the authentication succeeds, the server encrypts and encodes the user information into a token and returns it to the client
  • Each time the client sends a request, it must carry an authentication token
  • The server decrypts the token sent by the client, checks whether it is valid, and obtains user login information

1.4 JWT

1.4.1 profile

JWT, or Json Web Token, is a jSON-style lightweight authorization and identity authentication specification that can implement stateless and distributed Authorization for Web applications:

JWT as a specification, there is no bound together, and a certain language commonly used Java implementation is making on the open source project JJWT, address is as follows: https://github.com/jwtk/jjwt

1.4.2 JWT data format

JWT contains three parts of data:

  • Header: A Header usually has two parts of information:

    • Declare type, in this case JWT
    • Encryption algorithm, custom

    We base64URL-encode the header (decidable) to get the first part of the data.

  • The official document, RFC7519, gives seven sample messages:

    • Iss (issuer) : indicates the issuer
    • Exp (expiration Time) : indicates the expiration time of the token
    • Sub (subject) : indicates the topic
    • Aud (audience) : Audience
    • NBF (Not Before) : indicates the effective time
    • Iat (Issued At) : time of issue
    • Jti (JWT ID) : indicates the ID

    This part will also be Base64Url encoded to get the second part of the data.

  • Signature: indicates the authentication information of the entire data. Generally, the encryption algorithm configured in the Header is generated based on the data obtained in the previous two steps and the secret key of the service (the secret key is stored on the server and cannot be disclosed to the client). Used to verify data integrity and reliability.

The generated data format is shown below:

Note that the data is separated into three parts by a., which corresponds to the three parts mentioned above. In addition, the data is not wrapped, and the image is wrapped for presentation purposes only.

1.4.3 JWT Interaction process

Flow chart:

Step translation:

  1. The application or client requests authorization from the authorization server
  2. Once the authorization is obtained, the authorization server returns the access token to the application
  3. Applications use access tokens to access protected resources (such as apis)

Since the jWT-issued token already contains the user’s identity information and is carried with each request, the service does not need to save the user information or even query the database, which fully conforms to the stateless specification of RESTful.

1.5 Problems existing in JWT

Having said that, JWT is not perfect, and some of the problems that arise from maintaining login state on the client side still exist. Here are some examples:

  1. The traditional cookie+ Session scheme naturally supports renewal, but JWT is difficult to solve the problem perfectly because the server does not save the user state. If redis is introduced, the problem can be solved, but JWT also becomes nonexistent.
  2. Logout problem: Because the server no longer saves user information, it is generally possible to implement logout by modifying secret. After modifying secret, the authentication of the issued tokens that have not expired will fail, and then the logout will be realized. After all, it is not as convenient as traditional logout.
  3. After the password is reset, the original token can still access the system. In this case, you need to forcibly change the secret.
  4. Based on the second and third points, different users are generally advised to obtain different secret.

2 of actual combat

So with that said, how does this thing actually work?

2.1 Environment Construction

Create a Spring Boot project. Add the Spring Security dependency and JJWT dependency.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>
Copy the code

Then create a simple User object in the project that implements the UserDetails interface as follows:

public class User implements UserDetails {
    private String username;
    private String password;
    private List<GrantedAuthority> authorities;
    public String getUsername(a) {
        return username;
    }
    @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;
    }
    / / omit getter/setter
}
Copy the code

This is our user object, put it in standby, and create a HelloController with the following content:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(a) {
        return "hello jwt !";
    }
    @GetMapping("/admin")
    public String admin(a) {
        return "hello admin !"; }}Copy the code

HelloController is simple. There are two interfaces, designed so that/Hello can be accessed by users with the user role and /admin can be accessed by users with the admin role.

2.2 JWT Filter Configuration

The next two jWT-related filter configurations are provided:

  1. One is the filter of user login. In the filter of user login, verify whether the user login is successful. If the login is successful, a token is generated and returned to the client.
  2. The second filter validates the token when other requests are sent and, if successful, allows the request to continue.

Let’s look at the two filters separately, starting with the first one:

public class JwtLoginFilter extends AbstractAuthenticationProcessingFilter {
    protected JwtLoginFilter(String defaultFilterProcessesUrl, AuthenticationManager authenticationManager) {
        super(new AntPathRequestMatcher(defaultFilterProcessesUrl));
        setAuthenticationManager(authenticationManager);
    }
    @Override
    public Authentication attemptAuthentication(HttpServletRequest req, HttpServletResponse resp) throws AuthenticationException, IOException, ServletException {
        User user = new ObjectMapper().readValue(req.getInputStream(), User.class);
        return getAuthenticationManager().authenticate(new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()));
    }
    @Override
    protected void successfulAuthentication(HttpServletRequest req, HttpServletResponse resp, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        Collection<? extends GrantedAuthority> authorities = authResult.getAuthorities();
        StringBuffer as = new StringBuffer();
        for (GrantedAuthority authority : authorities) {
            as.append(authority.getAuthority())
                    .append(",");
        }
        String jwt = Jwts.builder()
                .claim("authorities", as)// Configure user roles
                .setSubject(authResult.getName())
                .setExpiration(new Date(System.currentTimeMillis() + 10 * 60 * 1000))
                .signWith(SignatureAlgorithm.HS512,"sang@123")
                .compact();
        resp.setContentType("application/json; charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write(new ObjectMapper().writeValueAsString(jwt));
        out.flush();
        out.close();
    }
    protected void unsuccessfulAuthentication(HttpServletRequest req, HttpServletResponse resp, AuthenticationException failed) throws IOException, ServletException {
        resp.setContentType("application/json; charset=utf-8");
        PrintWriter out = resp.getWriter();
        out.write("Login failed!"); out.flush(); out.close(); }}Copy the code

Here are a few things I can say about this class:

  1. Custom JwtLoginFilter inherited from AbstractAuthenticationProcessingFilter, and realize the three methods by default.
  2. AttemptAuthentication method, from the user name password login parameters are extracted, and then call the AuthenticationManager. The authenticate () method to automatically check.
  3. The second step, if successful, leads to the successfulAuthentication callback, which in the successfulAuthentication method iterates through the user role and uses one.Then Jwts is used to generate tokens. In order of code, four parameters are configured in the generation process, namely user role, subject, expiration time, encryption algorithm and key, and the generated tokens are written out to the client.
  4. Step 2 check failure will come unsuccessfulAuthentication method, in this method returns an error message to the client.

Let’s look at the second token check filter:

public class JwtFilter extends GenericFilterBean {
    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) servletRequest;
        String jwtToken = req.getHeader("authorization");
        System.out.println(jwtToken);
        Claims claims = Jwts.parser().setSigningKey("sang@123").parseClaimsJws(jwtToken.replace("Bearer".""))
                .getBody();
        String username = claims.getSubject();// Get the current login user name
        List<GrantedAuthority> authorities = AuthorityUtils.commaSeparatedStringToAuthorityList((String) claims.get("authorities"));
        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, null, authorities); SecurityContextHolder.getContext().setAuthentication(token); filterChain.doFilter(req,servletResponse); }}Copy the code

Here are a few things I can say about this filter:

  1. First, the authorization field is extracted from the request header, and the value corresponding to this field is the token of the user.
  2. To extract the token string is converted to a Claims object, and then Claims the object from the current user name and user roles, create a UsernamePasswordAuthenticationToken in the current Context, The filter chain is then executed to continue the request.

After that, the two JWT-related filters are configured.

2.3 Spring Security Configuration

Let’s configure Spring Security as follows:

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Bean
    PasswordEncoder passwordEncoder(a) {
        return NoOpPasswordEncoder.getInstance();
    }
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("admin")
                .password("123").roles("admin")
                .and()
                .withUser("sang")
                .password("456")
                .roles("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/hello").hasRole("user")
                .antMatchers("/admin").hasRole("admin")
                .antMatchers(HttpMethod.POST, "/login").permitAll()
                .anyRequest().authenticated()
                .and()
                .addFilterBefore(new JwtLoginFilter("/login",authenticationManager()),UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(newJwtFilter(),UsernamePasswordAuthenticationFilter.class) .csrf().disable(); }}Copy the code
  1. For simplicity, I didn’t encrypt the password here, so I configured an instance of NoOpPasswordEncoder.
  2. For simplicity, instead of connecting to the database, I simply configured two users in memory with different roles.
  3. When configuring path rules,/helloThe interface must have the user role to access,/adminInterface must have admin role to access, POST request and is/loginInterfaces can be directly passed. Other interfaces can be accessed only after authentication.
  4. Finally, configure two custom filters and disable CSRF protection.

2.4 test

Once that’s done, our environment is fully set up. Let’s start the project and test it in POSTMAN as follows:

The string returned after a successful login is the token transcoded by base64URL. There are three parts in the string. We can decode the string before the first., i.e. Header, as follows:

Payload = payload; payload = payload;

As you can see, we set the information, and since Base64 is not an encryption scheme, just an encoding scheme, it is not recommended to put sensitive user information into tokens.

Note that the protocol has been selected as Bearer Token, and the Token values are as follows:

You can see that the access succeeds.

conclusion

This is a simple use of JWT in conjunction with Spring Security, and to be honest, I still recommend using password mode in OAuth2 for similar requirements if the instance allows.

I don’t know if you understand? If you don’t understand, Songo also has a video tutorial on this topic, as follows:

How do I get this video tutorial? Very simple, forward this article to a wechat group of more than 100 people (QQ group does not count, songge is the group leader of the wechat group does not count, the group should be Java direction), or multiple wechat groups, as long as the cumulative number of 100 people can be added songge wechat, send the screenshot to Songge can get information.

Pay attention to the public account [Jiangnan little Rain], focus on Spring Boot+ micro service and front and back end separation and other full stack technology, regular video tutorial sharing, after attention to reply to Java, get Songko for you carefully prepared Java dry goods!