Skip to content
JackSparrow414
Go back

Using Shiro: Integrating JWT for More Capable Tokens

Table of contents

Open Table of contents

Article body

If I am going to do this, I might as well do it thoroughly!

In the previous article, I implemented token validation with my own very, very simple token. After learning about JWT, which is also widely used, I decided to integrate it with Shiro.

JWT provides solutions to the unresolved problems listed at the end of the previous article.

Approach:

Replace my custom token from the previous article with JWT. The overall approach stays the same.

Step 1: write a JWT utility class to handle token encryption, decryption, expiry checks, and related tasks.

/**
 * JWT token utility for generation and validation
 *
 * @Date 2019-05-25
 * @Description: TODO
 */
@ConfigurationProperties(prefix = "dhb.jwt")
@Component
public class JwtUtil {
    private Logger logger = LoggerFactory.getLogger(getClass());
    private String secret;
    private Long expire;
    private String header;

    /**
     *
     * Generate a JWT token
     * @param userId
     * @return
     */
    public String generateToken(Long userId) {
        Date nowDate = new Date();
        Date expireDate = new Date(nowDate.getTime() + expire * 1000);
        return Jwts.builder()
                .setHeaderParam("typ", "JWT")
                .setSubject(userId + "")
                .setIssuedAt(nowDate)
                .setExpiration(expireDate)
                .signWith(SignatureAlgorithm.HS256, secret)
                .compact();

    }

    /**
     *
     * Parse a JWT token
     * @param token
     * @return
     */
    public Claims parseToken(String token) {
        try {

            return Jwts.parser()
                    .setSigningKey(secret)
                    .parseClaimsJws(token)
                    .getBody();
        } catch (Exception e) {
            logger.info("Error parsing token");
            return null;
        }
    }

    /**
     *
     * Check whether the token has expired
     * @param expiprationTime
     * @return
     */
    public boolean isTokenExpired(Date expiprationTime){
        return expiprationTime.before(new Date());
    }
    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public Long getExpire() {
        return expire;
    }

    public void setExpire(Long expire) {
        this.expire = expire;
    }

    public String getHeader() {
        return header;
    }

    public void setHeader(String header) {
        this.header = header;
    }
}

JWT generation and parsing are handled by jws.builder and jes.parse respectively. Consult the official documentation or search online for the specific methods; I will not explain them in detail here. Note the ConfigeratureProperties annotation on JwtUtil. It maps properties from application.yml to this class, analogous to loading a properties file in a traditional SSM project. Since Spring Boot favors classes for configuration, adding this annotation makes the class a configuration-properties holder.

# JWT configuration
dhb:
  jwt:
    # Encryption secret
    secret: f4e2e52034348f86b67cde581c0f9eb5
    # Token validity: 7 days, in seconds
    expire: 604800
    header: authorization

This is part of application.yml. The prefix on the class corresponds to the path here.

Step 2:

Handle interception in AuthFilter (previously named shiroFilter, renamed to avoid a naming conflict). The logic is the same as in the previous article, except that the logic previously in onAccessDenied() now lives in isaAssessAllowed(). If isAssessAllowed returns false, onAccessDenied can simply return false.

@Component("authFilter")
public class AuthFilter extends FormAuthenticationFilter {

    @Autowired
    JwtUtil jwtUtil;

    /**
     * Check whether the token is missing or expired
     *
     * @param request
     * @param response
     * @param mappedValue
     * @return
     */
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
            String token = getRequestToken((HttpServletRequest) request);
            if (ObjectUtils.isNull(token)){
                return false;
            }
            if (StringUtils.isBlank(token)) {
                throw new CustomException(jwtUtil.getHeader()+"不能为空", HttpStatus.SC_UNAUTHORIZED);
            }
            Claims claims = jwtUtil.parseToken(token);
            if (ObjectUtils.isNull(claims) || jwtUtil.isTokenExpired(claims.getExpiration())) {
                throw new CustomException(jwtUtil.getHeader()+"token过期",HttpStatus.SC_UNAUTHORIZED);
            }
        return true;
    }

    /**
     * Called next if the method above returns false; not called if it returns true
     * Check whether this is the login URL, then whether the request is POST
     *
     * @param request
     * @param response
     * @return
     * @throws Exception
     */
    @Override
    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws Exception {
        if (isLoginRequest(request, response)) {
            if (isLoginSubmission(request, response)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Read the token from the request header first; if absent, try a request parameter
     *
     * @param request
     * @return
     */
    private String getRequestToken(HttpServletRequest request) {
        String token = request.getHeader(jwtUtil.getHeader());
        if (StringUtils.isBlank(token)) {
            token = request.getParameter(jwtUtil.getHeader());
        }
        return token;
    }
}

Step 3:

After login authentication succeeds, return the generated encrypted token to the frontend. The relevant code is:

 @PostMapping("login")
    public Map login(@RequestBody String info){
        User user = JSON.parseObject(info,User.class);
        Subject subject = SecurityUtils.getSubject();
        UsernamePasswordToken token = new
              UsernamePasswordToken(user.getName(),user.getPassword());
        token.setRememberMe(true);
        subject.login(token);
        // Token to return
        String tokenBack = jwtUtil.generateToken(userId);

tokenBack is the encrypted JWT ultimately returned to the frontend.

Test result:

{
    "token": "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdWIiOiI0MTQiLCJpYXQiOjE1NTg3ODk1NTAsImV4cCI6MTU1OTM5NDM1MH0.MzFzdOJCLbrG8c3j7WuJzj9NIPjrtLUL7n_AF913tS4"
}

Include this token in every subsequent request to validate that request. This completes the Shiro–JWT integration. I recommend JWT for its greater security.

Minor issues encountered:

  1. Initially, jwtUtil in AuthFilter was always null during request validation. I first suspected the properties had not been mapped, but debugging showed they had. That meant JwtUtil was not being injected into AuthFilter. I found the explanation online: when you construct an instance with new, Spring cannot inject dependencies into it through @Autowired. Other objects referenced inside that manually created instance are not injected either.

Shiro configuration injecting the AuthFilter bean as a parameter and adding it to the filters map

My original call was filter.put(“auth”,new AuthFilter()). Because I constructed AuthFilter directly, its JwtUtil could not be injected even with @Autowired. After changing it to the form shown in the screenshot, I added @Component to AuthFilter. Thanks to this blogger for the explanation.

  1. After resolving that issue, I thought it would be helpful to see which beans were in the Spring container after startup. That would make diagnosis faster than repeatedly searching online as I did today. Spring Boot does provide such a feature: Actuator. I have not studied it closely yet; here is the official documentation.

For now, the project’s security module—Shiro integrated with Spring Boot and JWT—is sufficient. I can finally move on to other problems.

 This series does not include complete code for these examples. However, I have integrated Shiro into a project with both a frontend and backend; the code is here if needed.

December 1, 2021 update

A few additional notes on JWT:

 This article focuses on applying JWT to a practical scenario with the io.jsonwebtoken library. For a broader introduction to JWT and other libraries such as Nimbus, see Getting Started with JWT.


Share this post:

Continue this series

Using Shiro

  1. Using Shiro: A Basic Login Flow
  2. Using Shiro Remember Me and Automatic Login: Fixing a deleteMe Cookie
  3. Using Shiro: Basic Session Management
  4. Using Shiro: Password Hashing and Salting, with Authentication Troubleshooting
  5. Using Shiro: A Primer on Tokens and Why to Use Them
  6. Using Shiro with Tokens
  7. Using Shiro: Integrating JWT for More Capable TokensYou are here
  8. Using Shiro: Complete Spring Boot Integration Code

Comments

Questions, corrections, and experiences are welcome. Sign in with GitHub to comment; both language versions share this discussion.

Comments are available on the live site only.