SpringBoot+Shiro+JWT for login and rights control

Authentication and Authorization

authentication
The main thing to do to certify in the program is to find out who the visitor is and has he registered with our system? Is he a user in our system? If so, is this user blacklisted by us? This is what certification does. For example: To enter a scenic spot, you have to buy a ticket first. If you don't have a ticket in your hand, your Uncle/Aunt will stop you from going in. If you have a ticket in your hand, your Uncle will also check to see if your ticket was bought today and if it expires. If you have a ticket and everything is OK, you can go in. This is a certification process!

authorization
The primary role of authorization is to verify that you have access to a resource! We all know that in a MySQL database, root users can add, delete, and alter data in each database at will, but ordinary users may only have the right to view it, not the right to delete and modify it. There is a very common requirement in programs as well: User A can access and modify a page, while User B may only have the right to access this page, not modify it!

A brief introduction to Shiro and JWT

Shiro is a lightweight security framework that allows you to quickly implement rights management using annotations. He is primarily used for authorization.
JWT(JSON Web Token), consisting of a request header, a request body, and a signature, is mainly used for authentication.

An Overall Idea for Achieving Authentication and Authorization

Client accesses the server and the server authenticates the request, mainly including whether the username and password are correct. If the authentication is successful, a certificate token will be issued to the client. Later, the client should carry the token when it visits the server again. If it is not carried or the token is tampered with, the authentication will fail! If token authentication succeeds and the client accesses the resource, then the server will also authenticate if the user has the right to access the resource at this time. If this user does not have access to this resource, access will also fail!

Database Table Design

Authentication and validation of privileges use the RBAC model with a total of five tables, user, user_role, role, role_permission and permission,
Where user_role and role_permission is an intermediate table, and the user table and role represent a one-to-many relationship.
The role and permission tables are also one-to-many relationships.

code implementation

The first is the login code, which is the code used to authenticate. When the user authenticates successfully, a token is returned to the client. Clients with failed authentication will not be able to get token!

Sign in

@RestController
@Slf4j
public class LoginController {
    @Resource
    private UserService userService;

    @PostMapping("/login")
    public ResponseBean login(@RequestBody JSONObject requestJson) {
        log.info("User Login");
        String username = requestJson.getString("username");
        String password = requestJson.getString("password");

        if (StringUtils.isBlank(username) || StringUtils.isBlank(password)) {
            throw new RuntimeException("User name and password cannot be empty!");
        }

        // Find the user information from the database based on the user name
        UserDTO userDTO = userService.getByUseName(username);

        // Get salt
        String salt = userDTO.getSalt();
        // Encrypt the original password (using username + salt as salt)
        String encodedPassword = ShiroKit.md5(password, username + salt);
        if (null != userDTO && userDTO.getPassword().equals(encodedPassword)) {
            return new ResponseBean(200, "Login success", JWTUtil.sign(username, encodedPassword));
        } else {
            throw new UnauthorizedException("Wrong username or password!");
        }
    }

}

JWT generates token (some tool classes with validation)

public class JWTUtil {
    final static Logger logger = LogManager.getLogger(JWTUtil.class);

    /**
     * Expiration time 30 minutes
     */
    private static final long EXPIRE_TIME = 30 * 60 * 1000;

    /**
     * Verify that token is correct
     *
     * @param token  secret key
     * @param secret User's password
     * @return Is it correct
     */
    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();
            verifier.verify(token);
            return true;
        } catch (Exception exception) {
            return false;
        }
    }

    /**
     * Getting information from a token does not require secret decryption
     *
     * @return token User name included in
     */
    public static String getUsername(String token) {
        try {
            DecodedJWT jwt = JWT.decode(token);
            return jwt.getClaim("username").asString();
        } catch (JWTDecodeException e) {
            return null;
        }
    }

    /**
     * Generate signature, expire in 30 minutes
     *
     * @param username User name
     * @param secret   User's password
     * @return Encrypted token
     */
    public static String sign(String username, String secret) {
        try {
            Date date = new Date(System.currentTimeMillis() + EXPIRE_TIME);
            //Use the user's own password as the encryption key
            Algorithm algorithm = Algorithm.HMAC256(secret);
            // Attached username information
            //  Builder pattern
            String jwtString = JWT.create()
                    .withClaim("username", username)
                    .withExpiresAt(date)
                    .sign(algorithm);
            logger.debug(String.format("JWT:%s", jwtString));
            return jwtString;
        } catch (UnsupportedEncodingException e) {
            return null;
        }
    }
}

Filter

Exclude login requests, and then other requests must go through the authentication process before requesting:

public class JWTFilter extends BasicHttpAuthenticationFilter {

    private Logger LOGGER = LoggerFactory.getLogger(this.getClass());

    /**
     * Determine if the user wants to log in.
     * true:Is to sign in
     * Just check if the header contains an Authorization field
     */
    @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);
        // Submit to realm for login, if wrong he will throw an exception and be caught
        getSubject(request, response).login(token);
        // Logon success if no exception is thrown, returning true
        return true;
    }

    /**
     * Here we detail why all of the eventual returns are true, that is, access is allowed
     * For example, we provide an address GET/article
     * Logged-in users and tourists see different things
     * If false is returned here, the request is directly intercepted and the user cannot see anything
     * So we're going to return true here, and in Controller you can tell if the user is logged in by subject.isAuthenticated().
     * If some resources are accessible only to the logged-in user, we just need to add the @RequiresAuthentication comment to the method
     * However, one drawback of this approach is that it is not possible to filter authentication separately for requests such as GET,POST, etc. (because we override the official method), but it actually has little impact on the application.
     */
    @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;
    }

    /**
     * Provide support across domains
     */
    @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"));
        // Cross-domain will first send an option request, where we directly return the option request to its normal state
        if (httpServletRequest.getMethod().equals(RequestMethod.OPTIONS.name())) {
            httpServletResponse.setStatus(HttpStatus.OK.value());
            return false;
        }
        return super.preHandle(request, response);
    }

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

Customize MyRealm

Customize permissions and authentication logic:

@Configuration
public class MyRealm extends AuthorizingRealm {
    @Resource
    private UserService userService;


    /**
     * This method must be overridden or Shiro will fail
     */
    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JWTToken;
    }

    /**
     * This method is invoked only when it is necessary to detect user permissions, such as checkRole,checkPermission
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JWTUtil.getUsername(principals.toString());
        // Get user role privilege information based on user name
        UserDTO user = userService.getByUseName(username);
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();

        // Get Roles
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // Get permissions
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));
        return simpleAuthorizationInfo;
    }

    /**
     * Get permissions
     *
     * @param permissionList
     * @return
     */
    private List<String> getPermissionList(List<Permission> permissionList) {
        List<String> permissions = new ArrayList<>(permissionList.size());
        for (Permission permission : permissionList) {
            if (StringUtils.isNotBlank(permission.getPerCode())) {
                permissions.add(permission.getPerCode());
            }
        }
        return permissions;
    }

    /**
     * Get Role Name
     *
     * @param roleList
     * @return
     */
    private List<String> getRoleNameList(List<Role> roleList) {
        List<String> roleNames = new ArrayList<>(roleList.size());
        for (Role role : roleList) {
            roleNames.add(role.getName());
        }
        return roleNames;
    }

    /**
     * By default, this method is used to verify whether the user name is correct or not, and the error throws an exception.
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken auth) throws AuthenticationException {
        String token = (String) auth.getCredentials();
        // Decrypt to get username for comparison with database
        String username = JWTUtil.getUsername(token);
        if (username == null) {
            throw new AuthenticationException("token invalid");
        }

        UserDTO userBean = userService.getByUseName(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");
    }
}

Interpretation of MyRealm

Customizing Realm is the key to achieving permissions. MyRealm inherits the AuthorizingRealm abstract class of the Shiro Framework and then overrides the doGetAuthorizationInfo method, in which we first take the user name from the token carried by the client, and then find out in the database the roles and permissions that the user has. Roles and privileges are then placed in SimpleAuthorizationInfo objects, respectively. The following:

 // Get Roles
        List<String> roleNames = getRoleNameList(user.getRoleList());
        for (String roleName : roleNames) {
            simpleAuthorizationInfo.addRole(roleName);
        }

        // Get permissions
        List<String> permissions = getPermissionList(user.getPermissionList());
        simpleAuthorizationInfo.addStringPermissions(new HashSet<>(permissions));

Shiro Notes

1.RequiresAuthentication:

The class, instance, and method labeled with this annotation must be authenticated in the current session when accessed or invoked.
2.RequiresGuest:

The classes, instances, and methods labeled with this annotation can be "gust" when accessed or invoked without requiring authentication or having records in the original session.
3.RequiresPermissions:

The method labeled by this annotation can only be executed if the current Subject requires certain privileges. If the current Subject does not have this permission, the method will not be executed.
4.RequiresRoles:

The current Subject must have all the specified roles to access the methods labeled by the annotation. If the Subject does not have all the specified roles at the same time on that day, the method will not execute and an AuthorizationException exception will be thrown.
5.RequiresUser:

The current Subject must be the user of the application in order to access or invoke the classes, instances, and methods labeled by the annotation.
RequiresRoles and RequiresPermissions are commonly used.
These notes are in order of use:

RequiresRoles-->RequiresPermissions-->RequiresAuthentication-->RequiresUser-->RequiresGuest
 If there is more than one comment, the previous pass will continue to check the latter, otherwise return directly

@RequiresRoles(value = {"admin","guest"},logical = Logical.OR)
   The purpose of this note is to carry token The role in must be admin,guest One of
   If the logical = Logical.OR Change to logical = Logical.AND,Then the effect of this comment becomes that the role must include both admin and guest Yes.
   
@RequiresPermissions(logical = Logical.OR, value = {"user:view", "user:edit"})
    and@RequiresRoles Similarly, the value of the permission is placed in value In, the relationship between values is used Logical.OR and Logical.AND control

Code Testing

Sign in

Access resources without token

Carry token access

token requiring guest role but admin

Guest privilege guest token

Insufficient privileges

Permission to access


Text Link
Shiro&JWT Implements User Login and Rights Management

Keywords: Shiro Spring Boot jwt

Added by daena76 on Thu, 21 Oct 2021 16:42:37 +0300