Ape logic, ape logic, ape logic

See the Github repository for the complete sample code for this article. Q only introduces the most critical code blocks in this article.

https://github.com/yuanluoji/purestart-springboot-jwt
Copy the code

There is no further ado about what JWT is. Generally speaking, it has three parts, Header, Payload, and Signature. Each part has some subdivided attributes. This principle can be scanned at a glance, but it is not helpful for us to use it.

Using JWT can make the background service completely stateless, so that the login process does not need session interaction, so that the server has a powerful horizontal expansion capability, the previous NGINx does not need to configure ip_hash such a headache.

I have found many examples of JWT code to be very complex and vague. Especially with SpringBoot integration, due to the participation of SpringSecurity, this process is more complex.

This article will focus on integrating with SpringBoot to put JWT into practice.

First, let’s take a look at what JWT’s outerwear looks like. That’s the long list down here.

eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJ5dWFubHVvamkiLCJpYXQiOjE2MDI0OTA0NDEsImV4cCI6MTYwMjQ5MjI0MX0.Qrz30s56F--cQu_fs0LWQhiZtco LbdAuQK6dIVk4b_aSZ5is8nTs1bR7mh0qefZdiFvFk4N__sg0UouKbhH8_gCopy the code

For sensitive students, the last step is encoded in Base64. Using the official HTML page to decode it, you can see that it is only encoded, and the content is not encrypted. We can easily obtain the word Yuanluoji from Playload.

1. JWT use

With JWT, we expect to get login authentication, replace cookies with it, and simulate sessions with it. The general usage process is as follows:

  1. The front-end submits the user name and password to any server
  2. Server validates user name and password (spring securityorshiro)
  3. If the validation is successful, a token is generated using JWT’s API
  4. This token will be returned to the front end, which will store it (cookie, context, etc.) and send it to the server on each subsequent request as an HTTP header
  5. The server can verify the validity of the token because it has an expiration date and tamper-proof mechanism, so the token needs to be sent in its entirety

In Java, there are two popular packages. One is the official auth0, but it seems that because the use of more complex so use less; The other one is JJWT. It can be introduced directly through poM.

<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-api</artifactId>
	<version>0.11.2</version>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-impl</artifactId>
	<version>0.11.2</version>
	<scope>runtime</scope>
</dependency>
<dependency>
	<groupId>io.jsonwebtoken</groupId>
	<artifactId>jjwt-jackson</artifactId> <! -- or jjwt-gson if Gson is preferred -->
	<version>0.11.2</version>
	<scope>runtime</scope>
</dependency>
Copy the code

Let’s take a look at how Jwt is used. The first is the token issuing code, as follows:

public String generateToken(Map<String, Object> claims, String subject) {
    Date now = Calendar.getInstance().getTime();
    return Jwts.builder()
            .setClaims(claims)
            .setSubject(subject)
            .setIssuedAt(now)
            .setExpiration(new Date(System.currentTimeMillis() + expire * 1000))
            .signWith(SignatureAlgorithm.HS512, secret)
            .compact();
}
Copy the code

There are several important parameters that need to be explained:

  • subjectThe issued principal, such as the user name. It’s also in claims.
  • claimsSome additional information, which is in playload. Since it’s a HashMap, you can dump all the information you need into it
  • expirationIssue date and expiration date can be used during validation
  • secretAn encrypted key used to authenticate previously signed content

Let’s also look at its validation code.

public Claims getClaims(String token){
    Claims claims = Jwts
            .parser()
            .setSigningKey(secret)
            .parseClaimsJws(token)
            .getBody();
    return claims;
}
Copy the code

As you can see, we also passed in a Secret, and if the secret is tampered with, the code will throw a SignatureException.

That’s what JWT is all about. Remember these two methods, our integration validation, are based on these two methods.

Integrate it into the SpringBoot project

In the SpringBoot system, the most used authentication framework is its own Spring Security. In fact, JWT itself is not difficult, the difficulty is the integration with Spring Security, that is, more knowledge of Spring Security.

As shown in the figure above, we unpack the process of using Jwt into two parts. The first part is login, which is done using the normal Controller. The second part is JWT validation, which we solve using interceptors.

2. Configure security

We use WebSecurityConfigurerAdapter to complete Spring Security configuration. The main code is divided into three parts.

First, the source of user data

@Autowired
public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
    auth.userDetailsService(userDetailsService)
            .passwordEncoder(passwordEncoder());
}
Copy the code

This code configures the user data source and the digest algorithm for the password. BCrypt with high security factor is used here. In other words, what we store in MySQL is the BCrypt digest password, and SpringSecurity uses this algorithm to calculate the digest of the password we entered for comparison.

@Bean
public PasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
 }
Copy the code

We simulated a real user data source, the following is JwtUserDetailsServiceImpl code. The password for all users is 123456.

JwtUserDetailsServiceImpl.java

@Service
public class JwtUserDetailsServiceImpl implements UserDetailsService {
    /** ** already generated in WebSecurityConfig */
    @Autowired
    PasswordEncoder passwordEncoder;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return new User(username, mockPassword(), getAuthorities());
    }

    private String mockPassword(a) {
        return passwordEncoder.encode("123456");
    }

    private Collection<GrantedAuthority> getAuthorities(a) {
        List<GrantedAuthority> authList = new ArrayList<GrantedAuthority>();
        authList.add(new SimpleGrantedAuthority("ROLE_USER"));
        authList.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
        returnauthList; }}Copy the code

Second, whitelist configuration

We want links that don’t go to SpringSecurity’s interceptors, such as swagger or the login method, to be ignored in the global configuration.

Overriding the configure method can ignore some links.

String[] SWAGGER_WHITELIST = {
        "/swagger-ui.html"."/swagger-ui/**"."/swagger-resources/**"."/v2/api-docs"."/v3/api-docs"."/webjars/**"
};

@Override
public void configure(WebSecurity web) throws Exception {
    web.ignoring()
            .antMatchers(SWAGGER_WHITELIST)
            .antMatchers("/login**"); }Copy the code

Third, configure filters

Of course, there is also a configure method, this time with HttpSecurity. We are here to add a custom JwtRequestFilter until UsernamePasswordAuthenticationFilter.

Filters are generally in the chain of responsibility mode, so there will be order problems.

@Override
protected void configure(HttpSecurity httpSecurity) throws Exception {
    httpSecurity.cors()
            .and().csrf().disable()
            .authorizeRequests()
            .antMatchers(SWAGGER_WHITELIST).authenticated()
            .anyRequest().authenticated()
            .and().sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS)
            .and().addFilterBefore(new JwtRequestFilter(), UsernamePasswordAuthenticationFilter.class);
}
Copy the code

Be careful here. In the addFilterBefore method, we simply new a custom filter. After testing, if we do not do this and leave the custom filter to Spring to manage, the whitelist we configured above will be invalid. This is a bit of a pit.

At this point, our configuration for SpringSecurity is complete. Let’s take a look at the actual login and authentication code.

3. Login

Login is a simple Controller. I use the authenticate method of AuthenticationManager to verify the user’s name and password. After validation, JWT’s methods are called to generate a token return.

As you can see, the login code is very simple.

RestController
@CrossOrigin
public class LoginController {

    @Autowired
    private AuthenticationManager authenticationManager;

    @Autowired
    private JwtTools jwtTools;

    @RequestMapping(value = "/login", method = RequestMethod.POST)
    publicResponseEntity<? > login(@RequestBody JwtRequest jwtRequest)
            throws Exception {

        Authentication authentication = authenticate(jwtRequest.getUsername(), jwtRequest.getPassword());


        User user = User.class.cast(authentication.getPrincipal());
        final String token = jwtTools.generateToken(new HashMap<>(), user.getUsername());

        return ResponseEntity.ok(new JwtResponse(token));
    }

    private Authentication authenticate(String username, String password) throws Exception {
        Objects.requireNonNull(username);
        Objects.requireNonNull(password);
        return authenticationManager.authenticate(newUsernamePasswordAuthenticationToken(username, password)); }}Copy the code

Invoking the login method with Swagger returns the token we want when the password is entered as 123456. Otherwise the 403 status code is returned.

4. Verify

The validation code is mainly in the filter, we inherit OncePerRequestFilter, can write logic in its doFilterInternal method.

This part of the logic is encoded according to the diagram above. You can see that JWT is a very small part of it.

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
        throws ServletException, IOException {
    final String token = request.getHeader("Authorization");

    Claims claims = null;
    try {
        claims = getJwt().getClaims(token);
    } catch (Exception ex) {
        log.error("JWT Token error: {} , cause: {}", token, ex.getMessage());
    }

    if (claims == null) {
        chain.doFilter(request, response);
        return;
    }

    if (SecurityContextHolder.getContext().getAuthentication() == null) {
        boolean ok = getJwt().validateTokenExpiration(claims);
        if (ok) {
            UserDetails userDetails = getUserDetailsService().loadUserByUsername(claims.getSubject());
            UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(
                    userDetails,
                    null,
                    userDetails.getAuthorities());
            authToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
            SecurityContextHolder.getContext().setAuthentication(authToken);
        }
    }
    chain.doFilter(request, response);
}
Copy the code

In this way, JWT and SpringBoot are perfectly integrated. In the Swagger configuration, we found another problem: the need to enter the token in the header. There’s nothing to be said for that.

On this issue, Small Q also explained in the last article. We have two configurations for swagger’s permission token entry.

Here’s a picture of the configuration. You can see this article to complete the configuration.

5. Is it safe?

As you can see from our initial screenshot, this long list of JWT messages can be seen in plaintext on the client side. It’s easy to suspect that the contents can be tampered with.

We randomly copied some of it and decoded it using Base64. Turns out it was plaintext.

In fact, if you tamper with the plaintext and then re-use Base64 encoding to insert it, it will not pass verification. This is what our Serect key does.

In Java, this key, which needs to be Base64, can be generated using JDK utility classes.

String key = new String (Base64. GetEncoder (.) encode (" lk234jlk80234lsd can connect christoph isofios23u8432ndsdfsokjjjsklfjslk % ^ & ^ & % $# $$% # 83 12=12y3uiuy&^".getBytes()));Copy the code

This key can be very complex, and unless it is compromised, our JWT information is hard to tamper with. So, even if you see plaintext, you can’t change it, so it’s safe.

6. Problems in JWT events

As we can see from the above description, there is no problem with using JWT for login. Its implementation is simple, easy to expand, can replace cookie and session to complete login, permission related functions. There are many more benefits. First, clients can be processed in a unified way, such as Android, IOS, Web, etc. In addition, JWT supports cross-domain. This is relative to cookies, because JWT can put information in the header or pass it as a parameter.

There are problems, too. Let’s start with performance:

  • Take up bandwidth. Because you need to pass that token every time. Normally, we just store the user ID in playLoad, but if you have more attributes like permissions and so on, this string will get really big. If the requests are high, the overhead can be daunting
  • Stateless means that the server does not save the login information, which requires that the user information be pulled at every request. Generally this information is not suitable to pull from the database, you need a redis-like cache front. But whatever you do, it’s not as fast as a session. So you might want to think about in-heap caching, which mimics the session thing.

Other usage issues.

  • Token cancellation. If it is simply stateless, logging out of JWT tokens becomes very difficult. If tokens are compromised, you don’t even have any way to prevent these insecure requests. This is because the expiration date is written into the token and you cannot change it.
  • Token for renewal. Another problem is the renewal of the lease of the token. For example, if your token agreement expires after half an hour, then even if you perform the operation in the 29th minute, the token will still expire at the agreed time. This will look particularly weird to the user.

To solve these problems, we need to converge the service stateless property. After the tokens are generated, a copy (redis, noSQL, etc.) is saved on the server, and then the tokens are refreshed or deregistered.

But if you don’t care about these issues, assuming that the token is secure and won’t leak, you can issue a token with a long timeout and generate one for each login, nullifying the previous token. Thus, those old tokens existed, but no one knows about them anymore. These tokens then become ghosts.

conclusion

This article gives a brief introduction to JWT and then looks at an integration case using the SpringBoot service as an example. Finally, the pros and cons of JWT are discussed. It can be seen that for a common application, JWT can achieve real service coupling, holding the token pass, can shuttle between different systems.

Again, see the Github repository for the full sample code for this article.

https://github.com/yuanluoji/purestart-springboot-jwt
Copy the code

If it helps you, please don’t forget to like me. Your support is the motivation for my creation, and I will share more high-quality content with you in the future.

Many people pretend to be decadent. I advise you not to fall for it. Don’t give up every thought that you want to learn, because it could be your future self asking for help. I am small Q, and progress together with you. It’s not hard to give up, but it must be cool to persist.

This article is written by XjjDog. For information about Springboot, projects and private projects, please follow the public account “Ape Logic”.