Provide Best Programming Tutorials

SpringBoot Integrate With JWT And Apache Shiro

RESTful API Authentication Method

Generally speaking, RESTful API get Authentication and Authorization to make sure the safety of the API.

Authentication vs Authorization

Authentication means the identity of the user and Authorization means what operation rights the user has.

Ways Of Authentication

  • Basic Authentication This means put the username and password directly into the HTTP request header. This is the simplest way but not recommended.

  • TOKEN Authentication This is most commonly used way that put JWT TOKEN directly into the HTTP request header. This is the recommended way.

  • OAuth2.0 This is the most secure way but also the most complicated way. If not necessary don’t consider this way.

    Normally we just use JWT for Authentication in our projects.

What Is JWT

JSON Web Token (JWT, sometimes pronounced) is a JSON-based open standard (RFC 7519) for creating access tokens that assert some number of claims. For example, a server could generate a token that has the claim “logged in as admin” and provide that to a client. The client could then use that token to prove that it is logged in as admin. The tokens are signed by one party’s private key (usually the server’s), so that both parties (the other already being, by some suitable and trustworthy means, in possession of the corresponding public key) are able to verify that the token is legitimate. The tokens are designed to be compact, URL-safe, and usable especially in a web-browser single-sign-on (SSO) context. JWT claims can be typically used to pass the identity of authenticated users between an identity provider and a service provider, or any other type of claims as required by business processes. Official Web Site: https://jwt.io/ JWT is combined with three parts, all these parts together formed JWS string like this:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ.

Three Parts Of JWT

  • Header
  • Payload
  • Signature

Header

Header has two parts:

  • Declarition Type usually JWT
  • Declaring an encrypted algorithm Typical cryptographic algorithms used are HMAC with SHA-256 (HS256) and RSA signature with SHA-256 (RS256). JWA (JSON Web Algorithms) RFC 7518 introduces many more for both authentication and encryption.

The header usually looks like this:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

then encryt the header turn it into this:

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9

Payload

The payload contains three parts:

  • Registered Claim
  • Public Claim
  • Private Claim

Registered Claim

code name description
iss Issuer Identifies principal that issued the JWT.
sub Subject Identifies the subject of the JWT.
aud Audience Identifies the recipients that the JWT is intended for. Each principal intended to process the JWT must identify itself with a value in the audience claim. If the principal processing the claim does not identify itself with a value in the aud claim when this claim is present, then the JWT must be rejected.
exp Expiration Time Identifies the expiration time on and after which the JWT must not be accepted for processing. The value must be a NumericDate[10]: either an integer or decimal, representing seconds past 1970-01-01 00:00:00Z.
nbf Not Before Identifies the time on which the JWT will start to be accepted for processing. The value must be a NumericDate.
iat Issued at Identifies the time at which the JWT was issued. The value must be a NumericDate.
jti JWT ID Case sensitive unique identifier of the token even among different issuers.

Public Claim

Generally speaking, the public claim can contain any information but it is not recommended to add sensitive information here since it can be easily decrypted.

Private Claim

Private Claim is the claimed by both client side and server side. Also, sensitive information is not recommended claimed here.

A classical payload looks like this:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

After based64 encrypt we get the second part of JWT:

eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9

Signature

Signature contains three parts:

  • header (after encrypted)
  • payload (after encrypted)
  • secret

The third part of JWT is the combination of the component i mentioned before:

header(after encrypted)+payload(after encrypted)+secret

After encrypt get finally got our JWT string:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

Notice that secret is kept on the server side and should never be leaked.

How Could We Use JWT In Our Application

Generally speaking we add these information in our HTTP request header:

fetch('api/user/1', {
  headers: {
    'Authorization': 'Bearer ' + token
  }
})

The server is responsible for analysis the HTTP header for Authentication and Authorization as demonstrate below:

Security Related Issue

The JWT protocol itself does not have a secure transport function, so it must rely on the secure channel of SSL/TLS, so the recommendations are as follows:

  1. Sensitive information should not be stored in the payload portion of jwt, as this part is the part that the client can decrypt.
  2. Protect the secret private key, which is very important.
  3. If you can, please use the HTTPS protocol

Integrate With SpringBoot

Since we want to achieve complete front-end & back-end separation, so it is impossible to use session, cookie way for authentication thus JWT is used.

Program Logic

  1. Send POST request to /login if successed return a TOKEN if failed return UnauthorizedException

  2. After the user accesses each URL request that requires permission, the Authorization field must be added to the header, such as Authorization: token, and token is the key.

  3. Back-end will authenticate the request otherwise 401

Our security framework is :

  • Apche Shiro

  • java-jwt

Add Maven Dependency

<dependencies>

       <dependency>
           <groupId>org.slf4j</groupId>
           <artifactId>slf4j-api</artifactId>
           <version>1.7.16</version>
       </dependency>
       <dependency>
           <groupId>org.apache.shiro</groupId>
           <artifactId>shiro-spring</artifactId>
           <version>1.3.2</version>
       </dependency>
       <dependency>
           <groupId>com.auth0</groupId>
           <artifactId>java-jwt</artifactId>
           <version>3.2.0</version>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-web</artifactId>
           <version>1.5.8.RELEASE</version>
       </dependency>

   </dependencies>

Build Mock Datasource

Inorder to focus on how to ues JWT not the DB I fake a Datasource as below:

| username | password | role  | permission |
| -------- | -------- | ----- | ---------- |
| smith    | smith123 | user  | view       |
| danny    | danny123 | admin | view,edit  |

Then next build a UserService to mock DB query and put the result into the UserBean.

UserService.java

@Component
public class UserService {

    public UserBean getUser(String username) {
        // If no such user return null
        if (! DataSource.getData().containsKey(username))
            return null;

        UserBean user = new UserBean();
        Map<String, String> detail = DataSource.getData().get(username);

        user.setUsername(username);
        user.setPassword(detail.get("password"));
        user.setRole(detail.get("role"));
        user.setPermission(detail.get("permission"));
        return user;
    }
}

UserBean.java

public class UserBean {
    private String username;

    private String password;

    private String role;

    private String permission;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getRole() {
        return role;
    }

    public void setRole(String role) {
        this.role = role;
    }

    public String getPermission() {
        return permission;
    }

    public void setPermission(String permission) {
        this.permission = permission;
    }
}

Config JWT

We build a simple JWT enrypt tool and make user password as encrypt password thus make sure even if you token is stolen by others they cannot hack it.And besides, we put username in our TOKEN ,then TOKEN will be expired 5 minutes later.

public class JWTUtil {

    // Expired 5 minutes later
    private static final long EXPIRE_TIME = 5*60*1000;

    /**
     * Verify TOKEN
     * @param token 
     * @param secret User password
     * @return 
     */
    public static boolean verify(String token, String username, String secret) {
        try {
            Algorithm algorithm = Algorithm.HMAC256(secret);
            JWTVerifier verifier = JWT.require(algorithm)
                    .withClaim("username", username)
                    .build();
            DecodedJWT jwt = verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * Get username from TOKEN
     * @return token contains username information
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * generate signature, the signature will be expired 5 minutes later
     * @param username 
     * @param secret 
     * @return Encryted token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis()+EXPIRE_TIME);
            Algorithm algorithm = Algorithm.HMAC256(secret);
           
            return JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

ResponseBean.java

public class ResponseBean {
    
    // http status code
    private int code;

    // return message
    private String msg;

    // return data
    private Object data;

    public ResponseBean(int code, String msg, Object data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public Object getData() {
        return data;
    }

    public void setData(Object data) {
        this.data = data;
    }
}

Custom Exception

public class UnauthorizedException extends RuntimeException {
    public UnauthorizedException(String msg) {
        super(msg);
    }

    public UnauthorizedException() {
        super();
    }
}

URL

URL Function
/login Login
/article Everyone can visit but different role will get different content
/require_auth Login user can visit
/require_role admin role can visit
/require_permission view and edit role can visit

Controller

@RestController
public class WebController {

    private static final Logger LOGGER = LogManager.getLogger(WebController.class);

    private UserService userService;

    @Autowired
    public void setService(UserService userService) {
        this.userService = userService;
    }

    @PostMapping("/login")
    public ResponseBean login(@RequestParam("username") String username,
                              @RequestParam("password") String password) {
        UserBean userBean = userService.getUser(username);
        if (userBean.getPassword().equals(password)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, password));
        } else {
            throw new UnauthorizedException();
        }
    }

    @GetMapping("/article")
    public ResponseBean article() {
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated()) {
            return new ResponseBean(200, "You are already logged in", null);
        } else {
            return new ResponseBean(200, "You are guest", null);
        }
    }

    @GetMapping("/require_auth")
    @RequiresAuthentication
    public ResponseBean requireAuth() {
        return new ResponseBean(200, "You are authenticated", null);
    }

    @GetMapping("/require_role")
    @RequiresRoles("admin")
    public ResponseBean requireRole() {
        return new ResponseBean(200, "You are visiting require_role", null);
    }

    @GetMapping("/require_permission")
    @RequiresPermissions(logical = Logical.AND, value = {"view", "edit"})
    public ResponseBean requirePermission() {
        return new ResponseBean(200, "You are visiting permission require edit,view", null);
    }

    @RequestMapping(path = "/401")
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    public ResponseBean unauthorized() {
        return new ResponseBean(401, "Unauthorized", null);
    }
}

Deal With The Exception

As mentioned before that RESTFUL need uniform style so we need to deal with the Spring Boot exception.

We can use @RestControllerAdvice to do this.

@RestControllerAdvice
public class ExceptionController {

    // Catch Shiro Exception
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(ShiroException.class)
    public ResponseBean handle401(ShiroException e) {
        return new ResponseBean(401, e.getMessage(), null);
    }

    // Catch UnauthorizedException
    @ResponseStatus(HttpStatus.UNAUTHORIZED)
    @ExceptionHandler(UnauthorizedException.class)
    public ResponseBean handle401() {
        return new ResponseBean(401, "Unauthorized", null);
    }

    // Catch Other Exception
    @ExceptionHandler(Exception.class)
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ResponseBean globalException(HttpServletRequest request, Throwable ex) {
        return new ResponseBean(getStatus(request).value(), ex.getMessage(), null);
    }

    private HttpStatus getStatus(HttpServletRequest request) {
        Integer statusCode = (Integer) request.getAttribute("javax.servlet.error.status_code");
        if (statusCode == null) {
            return HttpStatus.INTERNAL_SERVER_ERROR;
        }
        return HttpStatus.valueOf(statusCode);
    }
}

Config Shiro

Achive JWTToken

public class JWTToken implements AuthenticationToken {

    // TOKEN
    private String token;

    public JWTToken(String token) {
        this.token = token;
    }

    @Override
    public Object getPrincipal() {
        return token;
    }

    @Override
    public Object getCredentials() {
        return token;
    }
}

Realm

@Service
public class MyRealm extends AuthorizingRealm {

    private static final Logger LOGGER = LogManager.getLogger(MyRealm.class);

    private UserService userService;

    @Autowired
    public void setUserService(UserService userService) {
        this.userService = userService;
    }
    
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

   
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        UserBean user = userService.getUser(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.addRole(user.getRole());
        Set<String> permission = new HashSet<>(Arrays.asList(user.getPermission().split(",")));
        simpleAuthorizationInfo.addStringPermissions(permission);
        return simpleAuthorizationInfo;
    }

   
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserBean userBean = userService.getUser(username);
        if (userBean == null) {
            throw new AuthenticationException("User didn't existed!");
        }

        if (! JWTUtil.verify(token, username, userBean.getPassword())) {
            throw new AuthenticationException("Username or password error");
        }

        return new SimpleAuthenticationInfo(token, token, "my_realm");
    }
}

Define Filter

All the request will forward to Filter,we extends BasicHttpAuthenticationFilter to override some methods

execution flow :preHandle->isAccessAllowed->isLoginAttempt->executeLogin

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());
    @Override
    protected boolean isLoginAttempt(ServletRequest request, ServletResponse response) {
        HttpServletRequest req = (HttpServletRequest) request;
        String authorization = req.getHeader("Authorization");
        return authorization != null;
    }

    
    @Override
    protected boolean executeLogin(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String authorization = httpServletRequest.getHeader("Authorization");

        JWTToken token = new JWTToken(authorization);
        getSubject(request, response).login(token);
        return true;
    }

   
    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        if (isLoginAttempt(request, response)) {
            try {
                executeLogin(request, response);
            } catch (Exception e) {
                response401(request, response);
            }
        }
        return true;
    }

    
    @Override
    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        HttpServletResponse httpServletResponse = (HttpServletResponse) response;
        httpServletResponse.setHeader("Access-control-Allow-Origin", httpServletRequest.getHeader("Origin"));
        httpServletResponse.setHeader("Access-Control-Allow-Methods", "GET,POST,OPTIONS,PUT,DELETE");
        httpServletResponse.setHeader("Access-Control-Allow-Headers", httpServletRequest.getHeader("Access-Control-Request-Headers"));
        
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

    /**
     * Illege request foward to /401
     */
    private void response401(ServletRequest req, ServletResponse resp) {
        try {
            HttpServletResponse httpServletResponse = (HttpServletResponse) resp;
            httpServletResponse.sendRedirect("/401");
        } catch (IOException e) {
            LOGGER.error(e.getMessage());
        }
    }
}

Config Shiro

@Configuration
public class ShiroConfig {

    @Bean("securityManager")
    public DefaultWebSecurityManager getManager(MyRealm realm) {
        DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
        
        manager.setRealm(realm);

       
        DefaultSubjectDAO subjectDAO = new DefaultSubjectDAO();
        DefaultSessionStorageEvaluator defaultSessionStorageEvaluator = new DefaultSessionStorageEvaluator();
        defaultSessionStorageEvaluator.setSessionStorageEnabled(false);
        subjectDAO.setSessionStorageEvaluator(defaultSessionStorageEvaluator);
        manager.setSubjectDAO(subjectDAO);

        return manager;
    }

    @Bean("shiroFilter")
    public ShiroFilterFactoryBean factory(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean factoryBean = new ShiroFilterFactoryBean();

        // define your filter and name it as jwt
        Map<String, Filter> filterMap = new HashMap<>();
        filterMap.put("jwt", new JWTFilter());
        factoryBean.setFilters(filterMap);

        factoryBean.setSecurityManager(securityManager);
        factoryBean.setUnauthorizedUrl("/401");

        /*
         * difine custom URL rule
         * http://shiro.apache.org/web.html#urls-
         */
        Map<String, String> filterRuleMap = new HashMap<>();
        // All the request forword to JWT Filter
        filterRuleMap.put("/**", "jwt");
        // 401 and 404 page does not forward to our filter
        filterRuleMap.put("/401", "anon");
        factoryBean.setFilterChainDefinitionMap(filterRuleMap);
        return factoryBean;
    }

   
    @Bean
    @DependsOn("lifecycleBeanPostProcessor")
    public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
        return new LifecycleBeanPostProcessor();
    }

    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(DefaultWebSecurityManager securityManager) {
        AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor();
        advisor.setSecurityManager(securityManager);
        return advisor;
    }
}

Run It

Get TOKEN by sending POST request

Add TOKEN in the HTTP request header and get the result

If without TOKEN or the wrong TOKEN you will get the following error:

Source Code:

Github Address

This Post Has 3 Comments

  1. Very Nice article. Just what i was looking for. But i have one question in this. How to implement logout? If i just say SecurityUtils.getSubject().logout(); it does not work because the JWT token might be still valid.

  2. Thanks a lot.
    This was very helpful!
    ~ Starred your repo ~

Leave a Reply

Close Menu